diff --git a/integration/integration_test.go b/integration/integration_test.go index 76c4204..e8b9734 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -202,7 +202,7 @@ func TestMain(t *testing.T) { t.Fatalf("unexpected body: %s", req.body) } - res = runFetch(t, fetchPath, server.URL, "--json", "--data", `{"key":"val"}`) + res = runFetch(t, fetchPath, server.URL, "--json", `{"key":"val"}`) assertExitCode(t, 0, res) req = <-chReq if req.body != `{"key":"val"}` { @@ -212,7 +212,7 @@ func TestMain(t *testing.T) { t.Fatalf("unexpected content-type: %s", h) } - res = runFetch(t, fetchPath, server.URL, "--xml", "--data", ``) + res = runFetch(t, fetchPath, server.URL, "--xml", ``) assertExitCode(t, 0, res) req = <-chReq if req.body != `` { diff --git a/internal/cli/app.go b/internal/cli/app.go index d0cdd18..1552dc0 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -28,13 +28,13 @@ type App struct { Edit bool Form []core.KeyVal Help bool - JSON bool + JSON io.Reader Method string Multipart []core.KeyVal Output string Update bool Version bool - XML bool + XML io.Reader } func (a *App) PrintHelp(p *core.Printer) { @@ -75,8 +75,7 @@ func (a *App) CLI() *CLI { }, ExclusiveFlags: [][]string{ {"aws-sigv4", "basic", "bearer"}, - {"data", "form", "multipart"}, - {"form", "json", "multipart", "xml"}, + {"data", "form", "json", "multipart", "xml"}, }, Flags: []Flag{ { @@ -199,28 +198,11 @@ func (a *App) CLI() *CLI { return a.Data != nil }, Fn: func(value string) error { - switch { - case len(value) == 0 || value[0] != '@': - a.Data = strings.NewReader(value) - case value == "@": - a.Data = os.Stdin - default: - f, err := os.Open(value[1:]) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("file does not exist: '%s'", value[1:]) - } - return err - } - info, err := f.Stat() - if err != nil { - return err - } - if info.IsDir() { - return fmt.Errorf("file is a directory: '%s'", value[1:]) - } - a.Data = f + r, err := requestBody(value) + if err != nil { + return err } + a.Data = r return nil }, }, @@ -354,7 +336,7 @@ func (a *App) CLI() *CLI { Short: "", Long: "insecure", Args: "", - Description: "Accept invalid TLS certificates - DANGER!", + Description: "Accept invalid TLS certs (!)", Default: "", IsSet: func() bool { return a.Cfg.Insecure != nil @@ -368,14 +350,18 @@ func (a *App) CLI() *CLI { { Short: "j", Long: "json", - Args: "", - Description: "Set the content-type to application/json", + Args: "[@]VALUE", + Description: "Send a JSON request body", Default: "", IsSet: func() bool { - return a.JSON + return a.JSON != nil }, Fn: func(value string) error { - a.JSON = true + r, err := requestBody(value) + if err != nil { + return err + } + a.JSON = r return nil }, }, @@ -440,7 +426,7 @@ func (a *App) CLI() *CLI { Short: "", Long: "no-pager", Args: "", - Description: "Avoid using a pager for the response body", + Description: "Avoid using a pager for the output", Default: "", IsSet: func() bool { return a.Cfg.NoPager != nil @@ -523,7 +509,7 @@ func (a *App) CLI() *CLI { Short: "t", Long: "timeout", Args: "SECONDS", - Description: "Timeout in seconds applied to the request", + Description: "Timeout applied to the request", Default: "", IsSet: func() bool { return a.Cfg.Timeout != nil @@ -595,14 +581,18 @@ func (a *App) CLI() *CLI { { Short: "x", Long: "xml", - Args: "", - Description: "Set the content-type to application/xml", + Args: "[@]VALUE", + Description: "Send an XML request body", Default: "", IsSet: func() bool { - return a.XML + return a.XML != nil }, Fn: func(value string) error { - a.XML = true + r, err := requestBody(value) + if err != nil { + return err + } + a.XML = r return nil }, }, @@ -610,6 +600,31 @@ func (a *App) CLI() *CLI { } } +func requestBody(value string) (io.Reader, error) { + switch { + case len(value) == 0 || value[0] != '@': + return strings.NewReader(value), nil + case value == "@-": + return os.Stdin, nil + default: + f, err := os.Open(value[1:]) + if err != nil { + if os.IsNotExist(err) { + return nil, fileNotExistsError(value[1:]) + } + return nil, err + } + info, err := f.Stat() + if err != nil { + return nil, err + } + if info.IsDir() { + return nil, fileIsDirError(value[1:]) + } + return f, nil + } +} + func cut(s, sep string) (string, string, bool) { key, val, ok := strings.Cut(s, sep) key = strings.TrimSpace(key) @@ -617,11 +632,39 @@ func cut(s, sep string) (string, string, bool) { return key, val, ok } +type fileNotExistsError string + +func (err fileNotExistsError) Error() string { + return fmt.Sprintf("file '%s' does not exist", string(err)) +} + +func (err fileNotExistsError) PrintTo(p *core.Printer) { + p.WriteString("file '") + p.Set(core.Dim) + p.WriteString(string(err)) + p.Reset() + p.WriteString("' does not exist") +} + type MissingEnvVarError struct { EnvVar string Flag string } +type fileIsDirError string + +func (err fileIsDirError) Error() string { + return fmt.Sprintf("file '%s' is a directory", string(err)) +} + +func (err fileIsDirError) PrintTo(p *core.Printer) { + p.WriteString("file '") + p.Set(core.Dim) + p.WriteString(string(err)) + p.Reset() + p.WriteString("' is a directory") +} + func missingEnvVarErr(envVar, flag string) *MissingEnvVarError { return &MissingEnvVarError{ EnvVar: envVar, diff --git a/internal/client/client.go b/internal/client/client.go index dec53ae..b07e332 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -100,17 +100,17 @@ type RequestConfig struct { AWSSigV4 *aws.Config Basic *core.KeyVal Bearer string - Body io.Reader + Data io.Reader Form []core.KeyVal Headers []core.KeyVal HTTP core.HTTPVersion - JSON bool + JSON io.Reader Method string Multipart *multipart.Multipart NoEncode bool QueryParams []core.KeyVal URL *url.URL - XML bool + XML io.Reader } // NewRequest returns an *http.Request given the provided configuration. @@ -125,15 +125,22 @@ func (c *Client) NewRequest(ctx context.Context, cfg RequestConfig) (*http.Reque } // Set any form or multipart bodies. + var body io.Reader switch { + case cfg.Data != nil: + body = cfg.Data case len(cfg.Form) > 0: q := make(url.Values, len(cfg.Form)) for _, f := range cfg.Form { q.Add(f.Key, f.Val) } - cfg.Body = strings.NewReader(q.Encode()) + body = strings.NewReader(q.Encode()) + case cfg.JSON != nil: + body = cfg.JSON case cfg.Multipart != nil: - cfg.Body = cfg.Multipart + body = cfg.Multipart + case cfg.XML != nil: + body = cfg.XML } // If no scheme was provided, use various heuristics to choose between @@ -153,7 +160,7 @@ func (c *Client) NewRequest(ctx context.Context, cfg RequestConfig) (*http.Reque } // Create the initial HTTP request. - req, err := http.NewRequestWithContext(ctx, cfg.Method, cfg.URL.String(), cfg.Body) + req, err := http.NewRequestWithContext(ctx, cfg.Method, cfg.URL.String(), body) if err != nil { return nil, err } @@ -181,9 +188,9 @@ func (c *Client) NewRequest(ctx context.Context, cfg RequestConfig) (*http.Reque req.Header.Set("Content-Type", "application/x-www-form-urlencoded") case cfg.Multipart != nil: req.Header.Set("Content-Type", cfg.Multipart.ContentType()) - case cfg.JSON: + case cfg.JSON != nil: req.Header.Set("Content-Type", "application/json") - case cfg.XML: + case cfg.XML != nil: req.Header.Set("Content-Type", "application/xml") } diff --git a/internal/fetch/fetch.go b/internal/fetch/fetch.go index c31aca1..1fb6546 100644 --- a/internal/fetch/fetch.go +++ b/internal/fetch/fetch.go @@ -35,35 +35,34 @@ const ( ) type Request struct { + AWSSigv4 *aws.Config + Basic *core.KeyVal + Bearer string + Data io.Reader DNSServer *url.URL DryRun bool Edit bool + Form []core.KeyVal Format core.Format + Headers []core.KeyVal HTTP core.HTTPVersion IgnoreStatus bool Insecure bool + JSON io.Reader NoEncode bool NoPager bool + Method string + Multipart *multipart.Multipart Output string PrinterHandle *core.Handle + Proxy *url.URL + QueryParams []core.KeyVal Redirects *int + Timeout time.Duration TLS uint16 + URL *url.URL Verbosity core.Verbosity - - Method string - URL *url.URL - Body io.Reader - Form []core.KeyVal - Multipart *multipart.Multipart - Headers []core.KeyVal - QueryParams []core.KeyVal - AWSSigv4 *aws.Config - Basic *core.KeyVal - Bearer string - JSON bool - XML bool - Proxy *url.URL - Timeout time.Duration + XML io.Reader } func Fetch(ctx context.Context, r *Request) int { @@ -103,7 +102,7 @@ func fetch(ctx context.Context, r *Request) (int, error) { AWSSigV4: r.AWSSigv4, Basic: r.Basic, Bearer: r.Bearer, - Body: r.Body, + Data: r.Data, Form: r.Form, Headers: r.Headers, HTTP: r.HTTP, @@ -126,9 +125,9 @@ func fetch(ctx context.Context, r *Request) (int, error) { if r.Edit { var extension string switch { - case r.JSON: + case r.JSON != nil: extension = ".json" - case r.XML: + case r.XML != nil: extension = ".xml" } diff --git a/main.go b/main.go index 9e426d0..6797222 100644 --- a/main.go +++ b/main.go @@ -109,35 +109,34 @@ func main() { // Make the HTTP request using the parsed configuration. req := fetch.Request{ + AWSSigv4: app.AWSSigv4, + Basic: app.Basic, + Bearer: app.Bearer, + Data: app.Data, DNSServer: app.Cfg.DNSServer, DryRun: app.DryRun, Edit: app.Edit, + Form: app.Form, Format: app.Cfg.Format, + Headers: app.Cfg.Headers, HTTP: app.Cfg.HTTP, IgnoreStatus: getValue(app.Cfg.IgnoreStatus), Insecure: getValue(app.Cfg.Insecure), + JSON: app.JSON, + Method: app.Method, + Multipart: multipart.NewMultipart(app.Multipart), NoEncode: getValue(app.Cfg.NoEncode), NoPager: getValue(app.Cfg.NoPager), Output: app.Output, PrinterHandle: printerHandle, + Proxy: app.Cfg.Proxy, + QueryParams: app.Cfg.QueryParams, Redirects: app.Cfg.Redirects, + Timeout: getValue(app.Cfg.Timeout), TLS: getValue(app.Cfg.TLS), + URL: app.URL, Verbosity: verbosity, - - Method: app.Method, - URL: app.URL, - Body: app.Data, - Form: app.Form, - Multipart: multipart.NewMultipart(app.Multipart), - Headers: app.Cfg.Headers, - QueryParams: app.Cfg.QueryParams, - AWSSigv4: app.AWSSigv4, - Basic: app.Basic, - Bearer: app.Bearer, - JSON: app.JSON, - XML: app.XML, - Proxy: app.Cfg.Proxy, - Timeout: getValue(app.Cfg.Timeout), + XML: app.XML, } status := fetch.Fetch(ctx, &req) os.Exit(status)