diff --git a/NOTES.md b/NOTES.md index 83754a1..c3e4177 100644 --- a/NOTES.md +++ b/NOTES.md @@ -7,7 +7,7 @@ - [x] Make `gquil help ` show help instead of an error - [x] Make help text link back to a path for feedback, issues -- [ ] Add a CONTRIBUTING.md file with info about development, processes +- [x] Add a CONTRIBUTING.md file with info about development, processes - [ ] Add examples to the in-tool documentation - [ ] Add a manpage @@ -18,7 +18,7 @@ ## Argument handling -- [ ] Make it possible to read header values from a file ala `curl -H @filename` +- [x] Make it possible to read header values from a file ala `curl -H @filename` ## Error handling diff --git a/pkg/commands/generate_sdl.go b/pkg/commands/generate_sdl.go index f35ce2b..07f0be9 100644 --- a/pkg/commands/generate_sdl.go +++ b/pkg/commands/generate_sdl.go @@ -15,7 +15,7 @@ import ( type GenerateSDLCmd struct { Endpoint string `arg:"" help:"The GraphQL introspection endpoint URL to fetch from."` - Headers []string `name:"header" short:"H" help:"Set headers on the introspection request. Format: : ."` + Headers []string `name:"header" short:"H" help:"Set custom headers on the introspection request, e.g. for authentication. Format: : . May be specified multiple times. Header values may be read from a file with the syntax @, e.g. --header @my-headers.txt."` Trace bool `name:"trace" help:"Dump the introspection HTTP request and response to stderr for debugging."` OutputOptions @@ -32,7 +32,7 @@ Example: --header 'origin: https://docs.developer.yelp.com' \ https://api.yelp.com/v3/graphql -Note that since GraphQL's introspection schema does not expose information about the application sites of most directives, the generated SDL will lack any applied directives (with the exception of @deprecated, which is exposed via the introspection system) +Note that since GraphQL's introspection schema does not expose information about the application sites of most directives, the generated SDL will lack any applied directives (with the exception of @deprecated, which is exposed via the introspection system). If your GraphQL endpoint requires authentication or other special headers, you can set custom headers on the issued request using the --header flag.` } @@ -48,7 +48,12 @@ func (c *GenerateSDLCmd) Run(ctx Context) error { traceOut = os.Stderr } - client := introspection.NewClient(c.Endpoint, parseHeaders(c.Headers), sv, traceOut) + headers, err := parseHeaders(c.Headers) + if err != nil { + return fmt.Errorf("failed to parse custom header: %w", err) + } + + client := introspection.NewClient(c.Endpoint, headers, sv, traceOut) s, err := client.FetchSchemaAst() if err != nil { return err @@ -77,13 +82,63 @@ func (c *GenerateSDLCmd) Run(ctx Context) error { return nil } -func parseHeaders(raw []string) http.Header { +func parseHeaders(raw []string) (http.Header, error) { result := http.Header{} for _, rawHeader := range raw { - parts := strings.SplitN(rawHeader, ":", 2) - key := parts[0] - value := strings.TrimLeft(parts[1], " ") - result[key] = append(result[key], value) + parsedHeaders, err := parseHeaderValue(rawHeader) + if err != nil { + return nil, err + } + for key, vals := range parsedHeaders { + for _, val := range vals { + result.Add(key, val) + } + } + } + return result, nil +} + +func parseHeaderValue(raw string) (http.Header, error) { + if strings.HasPrefix(raw, "@") { + return parseHeadersFromFile(strings.TrimPrefix(raw, "@")) + } + + key, val, err := parseHeaderString(raw) + if err != nil { + return nil, err + } + return http.Header{ + key: []string{val}, + }, nil +} + +func parseHeadersFromFile(path string) (http.Header, error) { + raw, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + result := http.Header{} + lines := strings.Split(string(raw), "\n") + for _, line := range lines { + if line == "" { + continue + } + key, val, err := parseHeaderString(line) + if err != nil { + return nil, err + } + result.Add(key, val) + } + return result, nil +} + +func parseHeaderString(raw string) (string, string, error) { + parts := strings.SplitN(raw, ":", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid header value '%s', expected format ': '", raw) } - return result + key := parts[0] + value := strings.TrimLeft(parts[1], " ") + return key, value, nil } diff --git a/pkg/introspection/client.go b/pkg/introspection/client.go index ff8bb65..6e95822 100644 --- a/pkg/introspection/client.go +++ b/pkg/introspection/client.go @@ -120,11 +120,7 @@ func (c *Client) issueQuery(query string, vars map[string]any, operation string) return nil, fmt.Errorf("failed to create introspection request: %w", err) } - for k, vs := range c.headers { - for _, v := range vs { - req.Header.Set(k, v) - } - } + req.Header = c.headers if c.traceOut != nil { requestDump, err := httputil.DumpRequestOut(req, true)