pax_global_header00006660000000000000000000000064144703173520014520gustar00rootroot0000000000000052 comment=8873fe0204b939dcf565bc9fc3c4805ca01c1a24 golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/000077500000000000000000000000001447031735200231725ustar00rootroot00000000000000golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/.build.yml000066400000000000000000000004121447031735200250670ustar00rootroot00000000000000image: alpine/latest packages: - go sources: - https://git.sr.ht/~emersion/gqlclient tasks: - build: | cd gqlclient go build -v ./... - test: | cd gqlclient go test -v ./... - gofmt: | cd gqlclient test -z $(gofmt -l .) golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/LICENSE000066400000000000000000000020351447031735200241770ustar00rootroot00000000000000Copyright (c) 2020 Simon Ser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/README.md000066400000000000000000000050231447031735200244510ustar00rootroot00000000000000# gqlclient [![godocs.io](https://godocs.io/git.sr.ht/~emersion/gqlclient?status.svg)](https://godocs.io/git.sr.ht/~emersion/gqlclient) [![builds.sr.ht status](https://builds.sr.ht/~emersion/gqlclient/commits.svg)](https://builds.sr.ht/~emersion/gqlclient/commits?) A GraphQL client and code generator for Go. ## Usage gqlclient can be used as a thin GraphQL client, and can be augmented with code generation. See the [GoDoc examples] for direct usage. ### GraphQL schema code generation The code generator can parse a GraphQL schema and generate Go types. For instance, the following schema: ```graphqls type Train { name: String! maxSpeed: Int! weight: Int! linesServed: [String!]! } ``` and the following `gqlclientgen` invocation: ```sh gqlclientgen -s schema.graphqls -o gql.go -n rail ``` will generate the following Go type: ```go type Train struct { Name string MaxSpeed int32 Weight int32 LinesServed []string } ``` which can then be used in a GraphQL query: ```go op := gqlclient.NewOperation(`query { train(name: "Shinkansen E5") { name maxSpeed linesServed } }`) var data struct { Train rail.Train } if err := c.Execute(ctx, op, &data); err != nil { log.Fatal(err) } log.Print(data.Train) ``` ### GraphQL query code generation The code generator can also parse a GraphQL query document and generate Go functions. For instance, the following query document: ```graphql query fetchTrain($name: String!) { train(name: $name) { maxSpeed linesServed } } ``` and the following `gqlclientgen` invocation: ```sh gqlclientgen -s schema.graphqls -q queries.graphql -o gql.go -n rail ``` will generate the following function: ```go func FetchTrain(client *gqlclient.Client, ctx context.Context, name string) (Train, error) ``` which can then be used to execute the query: ```go train, err := rail.FetchTrain(c, ctx, "Shinkansen E5") if err != nil { log.Fatal(err) } log.Print(train) ``` ### GraphQL schema introspection gqlclient also supports fetching GraphQL schemas through GraphQL introspection. For instance, the following `gqlintrospect` invocation will fetch the GraphQL schema of the `https://example.com/query` GraphQL endpoint: ```sh gqlintrospect https://example.com/query > schema.graphqls ``` ## Contributing Send patches on the [mailing list]. Discuss in [#emersion on Libera Chat][IRC channel]. ## License MIT [GoDoc examples]: https://godocs.io/git.sr.ht/~emersion/gqlclient#example-Client-Execute [mailing list]: https://lists.sr.ht/~emersion/gqlclient-dev [IRC channel]: ircs://irc.libera.chat/#emersion golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/client.go000066400000000000000000000073231447031735200250040ustar00rootroot00000000000000package gqlclient import ( "bytes" "context" "encoding/json" "fmt" "io" "mime" "net/http" ) // Client is a GraphQL HTTP client. type Client struct { endpoint string http *http.Client } // New creates a new GraphQL client with the specified endpoint. // // If hc is nil, http.DefaultClient is used. func New(endpoint string, hc *http.Client) *Client { if hc == nil { hc = http.DefaultClient } return &Client{ endpoint: endpoint, http: hc, } } // Operation describes a GraphQL operation. // // An operation is a query with variables. type Operation struct { query string vars map[string]interface{} uploads map[string]Upload } // NewOperation creates a new GraphQL operation. func NewOperation(query string) *Operation { return &Operation{query: query} } // Var defines a new variable. // // If the variable is already defined, Var panics. func (op *Operation) Var(k string, v interface{}) { if op.vars == nil { op.vars = make(map[string]interface{}) op.uploads = make(map[string]Upload) } if _, ok := op.vars[k]; ok { panic(fmt.Sprintf("gqlclient: called Operation.Var twice on %q", k)) } op.vars[k] = v // TODO: support more deeply nested uploads switch v := v.(type) { case Upload: op.uploads[k] = v case *Upload: if v != nil { op.uploads[k] = *v } case []Upload: for i, upload := range v { upload := upload // copy op.uploads[fmt.Sprintf("%v.%v", k, i)] = upload } case []*Upload: for i, upload := range v { if upload != nil { op.uploads[fmt.Sprintf("%v.%v", k, i)] = *upload } } } } // Execute sends the operation to the GraphQL server. // // The data returned by the server will be decoded into the data argument. func (c *Client) Execute(ctx context.Context, op *Operation, data interface{}) error { reqData := struct { Query string `json:"query"` Vars map[string]interface{} `json:"variables"` }{ Query: op.query, Vars: op.vars, } var reqBuf bytes.Buffer if err := json.NewEncoder(&reqBuf).Encode(&reqData); err != nil { return fmt.Errorf("failed to encode request payload: %v", err) } var reqBody io.Reader var contentType string if len(op.uploads) > 0 { pr, pw := io.Pipe() defer pr.Close() reqBody = pr contentType = writeMultipart(pw, op.uploads, &reqBuf) } else { reqBody = &reqBuf contentType = "application/json; charset=utf-8" } // io.TeeReader(reqBody, os.Stderr) req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, reqBody) if err != nil { return fmt.Errorf("failed to create HTTP request: %v", err) } req.Header.Set("Content-Type", contentType) req.Header.Set("Accept", "application/json") resp, err := c.http.Do(req) if err != nil { return fmt.Errorf("HTTP request failed: %v", err) } defer resp.Body.Close() contentType = resp.Header.Get("Content-Type") if contentType == "" { contentType = "text/plain" } mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { return fmt.Errorf("invalid Content-Type %q: %v", contentType, err) } else if mediaType != "application/json" { if resp.StatusCode/100 != 2 { return &HTTPError{ StatusCode: resp.StatusCode, statusText: resp.Status, } } return fmt.Errorf("invalid Content-Type %q: expected application/json", contentType) } respData := struct { Data interface{} Errors []Error }{Data: data} // io.TeeReader(resp.Body, os.Stderr) if err := json.NewDecoder(resp.Body).Decode(&respData); err != nil { return fmt.Errorf("failed to decode response payload: %v", err) } if len(respData.Errors) > 0 { err = joinErrors(respData.Errors) } if resp.StatusCode/100 != 2 { err = &HTTPError{ StatusCode: resp.StatusCode, statusText: resp.Status, err: err, } } return err } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/000077500000000000000000000000001447031735200237355ustar00rootroot00000000000000golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/gqlclient/000077500000000000000000000000001447031735200257175ustar00rootroot00000000000000golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/gqlclient/main.go000066400000000000000000000056401447031735200271770ustar00rootroot00000000000000package main import ( "bytes" "context" "encoding/json" "flag" "fmt" "io" "log" "mime" "net/http" "os" "path/filepath" "strings" "time" "git.sr.ht/~emersion/gqlclient" ) type stringSliceFlag []string func (v *stringSliceFlag) String() string { return fmt.Sprint([]string(*v)) } func (v *stringSliceFlag) Set(s string) error { *v = append(*v, s) return nil } func splitKeyValue(kv string) (string, string) { parts := strings.SplitN(kv, "=", 2) if len(parts) != 2 { log.Fatalf("in variable definition %q: missing equal sign", kv) } return parts[0], parts[1] } type transport struct { http.RoundTripper header http.Header } func (tr *transport) RoundTrip(req *http.Request) (*http.Response, error) { for k, values := range tr.header { for _, v := range values { req.Header.Add(k, v) } } return tr.RoundTripper.RoundTrip(req) } func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var rawVars, jsonVars, fileVars, header []string flag.Var((*stringSliceFlag)(&rawVars), "v", "set raw variable") flag.Var((*stringSliceFlag)(&jsonVars), "j", "set JSON variable") flag.Var((*stringSliceFlag)(&fileVars), "f", "set file variable") flag.Var((*stringSliceFlag)(&header), "H", "set HTTP header") flag.Parse() endpoint := flag.Arg(0) if endpoint == "" { log.Fatalf("missing endpoint") } b, err := io.ReadAll(os.Stdin) if err != nil { log.Fatalf("failed to read GraphQL query from stdin: %v", err) } query := string(b) op := gqlclient.NewOperation(query) for _, kv := range rawVars { k, v := splitKeyValue(kv) op.Var(k, v) } for _, kv := range jsonVars { k, raw := splitKeyValue(kv) var v interface{} if err := json.Unmarshal([]byte(raw), &v); err != nil { log.Fatalf("in variable definition %q: invalid JSON: %v", kv, err) } op.Var(k, json.RawMessage(raw)) } for _, kv := range fileVars { k, filename := splitKeyValue(kv) f, err := os.Open(filename) if err != nil { log.Fatalf("in variable definition %q: failed to open input file: %v", kv, err) } defer f.Close() t := mime.TypeByExtension(filename) if t == "" { t = "application/octet-stream" } op.Var(k, gqlclient.Upload{ Filename: filepath.Base(filename), MIMEType: t, Body: f, }) } tr := transport{ RoundTripper: http.DefaultTransport, header: make(http.Header), } httpClient := http.Client{Transport: &tr} gqlClient := gqlclient.New(endpoint, &httpClient) for _, kv := range header { parts := strings.SplitN(kv, ":", 2) if len(parts) != 2 { log.Fatalf("in header definition %q: missing colon", kv) } tr.header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) } var data json.RawMessage if err := gqlClient.Execute(ctx, op, &data); err != nil { log.Fatal(err) } r := bytes.NewReader([]byte(data)) if _, err := io.Copy(os.Stdout, r); err != nil { log.Fatalf("failed to write response: %v", err) } } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/gqlclientgen/000077500000000000000000000000001447031735200264115ustar00rootroot00000000000000golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/gqlclientgen/main.go000066400000000000000000000274031447031735200276720ustar00rootroot00000000000000package main import ( "flag" "fmt" "log" "os" "path/filepath" "sort" "strings" "github.com/dave/jennifer/jen" "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" "github.com/vektah/gqlparser/v2/formatter" ) const usage = `usage: gqlclientgen -s -o [options...] Generate Go types and helpers for the specified GraphQL schema. Options: -s GraphQL schema, can be specified multiple times. Required. -q GraphQL query document, can be specified multiple times. -o Output filename for generated Go code. Required. -n Go package name, defaults to the dirname of the output file. -d Omit deprecated fields and enum values ` type stringSliceFlag []string func (v *stringSliceFlag) String() string { return fmt.Sprint([]string(*v)) } func (v *stringSliceFlag) Set(s string) error { *v = append(*v, s) return nil } const gqlclient = "git.sr.ht/~emersion/gqlclient" func genDescription(s string) jen.Code { if s == "" { return jen.Null() } s = "// " + strings.ReplaceAll(s, "\n", "\n// ") return jen.Comment(s).Line() } func genType(schema *ast.Schema, t *ast.Type) jen.Code { var prefix []jen.Code toplevel := true for t.Elem != nil { prefix = append(prefix, jen.Index()) toplevel = false t = t.Elem } def, ok := schema.Types[t.NamedType] if !ok { panic(fmt.Sprintf("unknown type name %q", t.NamedType)) } var gen jen.Code switch def.Name { // Standard types case "Int": gen = jen.Int32() case "Float": gen = jen.Float64() case "String": gen = jen.String() case "Boolean": gen = jen.Bool() case "ID": gen = jen.String() // Convenience types case "Time": gen = jen.Qual(gqlclient, "Time") case "Map": gen = jen.Map(jen.String()).Interface() case "Upload": gen = jen.Qual(gqlclient, "Upload") case "Any": gen = jen.Interface() default: if def.BuiltIn { panic(fmt.Sprintf("unsupported built-in type: %s", def.Name)) } gen = jen.Id(def.Name) } if !t.NonNull { switch def.Name { case "ID", "Time", "Map", "Any": // These don't need a pointer, they have a recognizable zero value default: prefix = append(prefix, jen.Op("*")) } } else if toplevel { switch def.Kind { case ast.Object, ast.Interface: // Required to deal with recursive types prefix = append(prefix, jen.Op("*")) } } return jen.Add(prefix...).Add(gen) } func hasDeprecated(list ast.DirectiveList) bool { return list.ForName("deprecated") != nil } func genDef(schema *ast.Schema, def *ast.Definition, omitDeprecated bool) *jen.Statement { switch def.Kind { case ast.Scalar: switch def.Name { case "Time", "Map", "Upload", "Any": // Convenience types return nil default: return jen.Type().Id(def.Name).String() } case ast.Enum: var defs []jen.Code for _, val := range def.EnumValues { if omitDeprecated && hasDeprecated(val.Directives) { continue } nameWords := strings.Split(strings.ToLower(val.Name), "_") for i := range nameWords { nameWords[i] = strings.Title(nameWords[i]) } name := strings.Join(nameWords, "") desc := genDescription(val.Description) defs = append(defs, jen.Add(desc).Id(def.Name+name).Id(def.Name).Op("=").Lit(val.Name), ) } return jen.Add( jen.Type().Id(def.Name).String(), jen.Line(), jen.Const().Defs(defs...), ) case ast.Object, ast.InputObject: var fields []jen.Code for _, field := range def.Fields { if omitDeprecated && hasDeprecated(field.Directives) { continue } if field.Name == "__schema" || field.Name == "__type" { continue // TODO } name := strings.Title(field.Name) jsonTag := field.Name if !field.Type.NonNull { jsonTag += ",omitempty" } tag := jen.Tag(map[string]string{"json": jsonTag}) desc := genDescription(field.Description) fields = append(fields, jen.Add(desc).Id(name).Add(genType(schema, field.Type)).Add(tag), ) } return jen.Type().Id(def.Name).Struct(fields...) case ast.Interface, ast.Union: possibleTypes := schema.GetPossibleTypes(def) var typeNames []string for _, typ := range possibleTypes { typeNames = append(typeNames, typ.Name) } var fields []jen.Code for _, field := range def.Fields { if omitDeprecated && hasDeprecated(field.Directives) { continue } if field.Name == "__schema" || field.Name == "__type" { continue // TODO } name := strings.Title(field.Name) jsonTag := field.Name if !field.Type.NonNull { jsonTag += ",omitempty" } tag := jen.Tag(map[string]string{"json": jsonTag}) desc := genDescription(field.Description) fields = append(fields, jen.Add(desc).Id(name).Add(genType(schema, field.Type)).Add(tag), ) } if len(fields) > 0 { fields = append(fields, jen.Line()) } fields = append(fields, jen.Comment("Underlying value of the GraphQL "+strings.ToLower(string(def.Kind))), jen.Id("Value").Id(def.Name+"Value").Tag(map[string]string{"json": "-"}), ) var cases []jen.Code for _, typ := range possibleTypes { cases = append(cases, jen.Case(jen.Lit(typ.Name)).Block( jen.Id("base").Dot("Value").Op("=").New(jen.Id(typ.Name)), )) } errPrefix := fmt.Sprintf("gqlclient: %v %v: ", strings.ToLower(string(def.Kind)), def.Name) switch def.Kind { case ast.Interface: cases = append(cases, jen.Case(jen.Lit("")).Block( jen.Return(jen.Nil()), )) case ast.Union: cases = append(cases, jen.Case(jen.Lit("")).Block( jen.Return(jen.Qual("fmt", "Errorf").Call(jen.Lit(errPrefix+"missing __typename field"))), )) } cases = append(cases, jen.Default().Block( jen.Return(jen.Qual("fmt", "Errorf").Call(jen.Lit(errPrefix+"unknown __typename %q"), jen.Id("data").Dot("TypeName"))), ), ) var stmts []jen.Code stmts = append(stmts, jen.Type().Id(def.Name).Struct(fields...)) stmts = append(stmts, jen.Line()) stmts = append(stmts, jen.Func().Params( jen.Id("base").Op("*").Id(def.Name), ).Id("UnmarshalJSON").Params( jen.Id("b").Index().Byte(), ).Params( jen.Id("error"), ).Block( jen.Type().Id("Raw").Id(def.Name), jen.Var().Id("data").Struct( jen.Op("*").Id("Raw"), jen.Id("TypeName").String().Tag(map[string]string{"json": "__typename"}), ), jen.Id("data").Dot("Raw").Op("=").Parens(jen.Op("*").Id("Raw")).Parens(jen.Id("base")), jen.Id("err").Op(":=").Qual("encoding/json", "Unmarshal").Call( jen.Id("b"), jen.Op("&").Id("data"), ), jen.If(jen.Id("err").Op("!=").Nil()).Block(jen.Return(jen.Id("err"))), jen.Switch(jen.Id("data").Dot("TypeName")).Block(cases...), jen.Return(jen.Qual("encoding/json", "Unmarshal").Call( jen.Id("b"), jen.Id("base").Dot("Value"), )), )) stmts = append(stmts, jen.Line()) stmts = append(stmts, jen.Comment(def.Name+"Value is one of: "+strings.Join(typeNames, " | ")).Line()) stmts = append(stmts, jen.Type().Id(def.Name+"Value").Interface( jen.Id("is"+def.Name).Params(), )) return jen.Add(stmts...) default: panic(fmt.Sprintf("unsupported definition kind: %s", def.Kind)) } } func collectFragments(frags map[*ast.FragmentDefinition]struct{}, selSet ast.SelectionSet) { for _, sel := range selSet { switch sel := sel.(type) { case *ast.Field: if sel.Name != sel.Alias { panic(fmt.Sprintf("field aliases aren't supported")) } collectFragments(frags, sel.SelectionSet) case *ast.FragmentSpread: frags[sel.Definition] = struct{}{} collectFragments(frags, sel.Definition.SelectionSet) case *ast.InlineFragment: collectFragments(frags, sel.SelectionSet) default: panic(fmt.Sprintf("unsupported selection type: %T", sel)) } } } func genOp(schema *ast.Schema, op *ast.OperationDefinition) *jen.Statement { frags := make(map[*ast.FragmentDefinition]struct{}) collectFragments(frags, op.SelectionSet) var fragList ast.FragmentDefinitionList for frag := range frags { fragList = append(fragList, frag) } var query ast.QueryDocument query.Operations = ast.OperationList{op} query.Fragments = fragList var sb strings.Builder formatter.NewFormatter(&sb).FormatQueryDocument(&query) queryStr := sb.String() var stmts, in, out, ret, dataFields []jen.Code in = append(in, jen.Id("client").Op("*").Qual(gqlclient, "Client")) in = append(in, jen.Id("ctx").Qual("context", "Context")) stmts = append(stmts, jen.Id("op").Op(":=").Qual(gqlclient, "NewOperation").Call(jen.Lit(queryStr))) for _, v := range op.VariableDefinitions { in = append(in, jen.Id(v.Variable).Add(genType(schema, v.Type))) stmts = append(stmts, jen.Id("op").Dot("Var").Call( jen.Lit(v.Variable), jen.Id(v.Variable), )) } for _, sel := range op.SelectionSet { field, ok := sel.(*ast.Field) if !ok { panic(fmt.Sprintf("unsupported selection %T", sel)) } typ := genType(schema, field.Definition.Type) out = append(out, jen.Id(field.Name).Add(typ)) ret = append(ret, jen.Id("respData").Dot(strings.Title(field.Name))) dataFields = append(dataFields, jen.Id(strings.Title(field.Name)).Add(typ)) } out = append(out, jen.Id("err").Id("error")) ret = append(ret, jen.Id("err")) stmts = append( stmts, jen.Var().Id("respData").Struct(dataFields...), jen.Id("err").Op("=").Id("client").Dot("Execute").Call( jen.Id("ctx"), jen.Id("op"), jen.Op("&").Id("respData"), ), ) stmts = append(stmts, jen.Return(ret...)) name := strings.Title(op.Name) return jen.Func().Id(name).Params(in...).Params(out...).Block(stmts...) } func main() { var schemaFilenames, queryFilenames []string var pkgName, outputFilename string var omitDeprecated bool flag.Var((*stringSliceFlag)(&schemaFilenames), "s", "schema filename") flag.Var((*stringSliceFlag)(&queryFilenames), "q", "query filename") flag.StringVar(&pkgName, "n", "", "package name") flag.StringVar(&outputFilename, "o", "", "output filename") flag.BoolVar(&omitDeprecated, "d", false, "omit deprecated fields") flag.Usage = func() { fmt.Fprint(os.Stderr, usage) } flag.Parse() if len(schemaFilenames) == 0 || outputFilename == "" || len(flag.Args()) > 0 { flag.Usage() os.Exit(1) } if pkgName == "" { abs, err := filepath.Abs(outputFilename) if err != nil { log.Fatalf("failed to get absolute output filename: %v", err) } pkgName = filepath.Base(filepath.Dir(abs)) } var sources []*ast.Source for _, filename := range schemaFilenames { b, err := os.ReadFile(filename) if err != nil { log.Fatalf("failed to load schema %q: %v", filename, err) } sources = append(sources, &ast.Source{Name: filename, Input: string(b)}) } schema, gqlErr := gqlparser.LoadSchema(sources...) if gqlErr != nil { log.Fatalf("failed to parse schema: %v", gqlErr) } var queries []*ast.QueryDocument for _, filename := range queryFilenames { b, err := os.ReadFile(filename) if err != nil { log.Fatalf("failed to load query %q: %v", filename, err) } q, gqlErr := gqlparser.LoadQuery(schema, string(b)) if gqlErr != nil { log.Fatalf("failed to parse query %q: %v", filename, gqlErr) } queries = append(queries, q) } f := jen.NewFile(pkgName) f.HeaderComment("Code generated by gqlclientgen - DO NOT EDIT.") var typeNames []string for _, def := range schema.Types { if def.BuiltIn || def == schema.Query || def == schema.Mutation || def == schema.Subscription { continue } typeNames = append(typeNames, def.Name) } sort.Strings(typeNames) for _, name := range typeNames { def := schema.Types[name] stmt := genDef(schema, def, omitDeprecated) if stmt != nil { f.Add(genDescription(def.Description), stmt).Line() } for _, typ := range schema.GetImplements(def) { f.Func().Params(genType(schema, ast.NamedType(def.Name, nil))).Id("is" + typ.Name).Params().Block().Line() } } for _, q := range queries { for _, op := range q.Operations { f.Add(genOp(schema, op)).Line() } } if err := f.Save(outputFilename); err != nil { log.Fatalf("failed to save output file: %v", err) } } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/gqlintrospect/000077500000000000000000000000001447031735200266335ustar00rootroot00000000000000golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/gqlintrospect/main.go000066400000000000000000000153351447031735200301150ustar00rootroot00000000000000package main import ( "context" "flag" "fmt" "log" "net/http" "os" "strings" "time" "git.sr.ht/~emersion/gqlclient" ) const usage = `usage: gqlintrospect Fetch the GraphQL schema of the specifed GraphQL endpoint. Options: -H Set an HTTP header. Can be specified multiple times. ` // The query used to determine type information const query = ` query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } } } fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef } } fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue } fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } } } ` type stringSliceFlag []string func (v *stringSliceFlag) String() string { return fmt.Sprint([]string(*v)) } func (v *stringSliceFlag) Set(s string) error { *v = append(*v, s) return nil } type transport struct { http.RoundTripper header http.Header } func (tr *transport) RoundTrip(req *http.Request) (*http.Response, error) { for k, values := range tr.header { for _, v := range values { req.Header.Add(k, v) } } return tr.RoundTripper.RoundTrip(req) } func typeKeyword(t Type) string { switch t.Kind { case TypeKindObject: return "type" case TypeKindInterface: return "interface" default: panic("unreachable") } } func typeReference(t Type) string { var modifiers []TypeKind ofType := &t for ofType.OfType != nil { switch ofType.Kind { case TypeKindList, TypeKindNonNull: modifiers = append(modifiers, ofType.Kind) default: panic("invalid type") } ofType = ofType.OfType } if ofType.Name == nil { panic("invalid type") } typeName := *ofType.Name if len(modifiers) > 0 { for i := len(modifiers) - 1; i >= 0; i-- { switch modifiers[i] { case TypeKindList: typeName = "[" + typeName + "]" case TypeKindNonNull: typeName += "!" } } } return typeName } func quoteString(s string) string { s = strings.Replace(s, `\`, `\\`, -1) s = strings.Replace(s, `"`, `\"`, -1) return fmt.Sprintf(`"%s"`, s) } func printDescription(desc string, prefix string) { if !strings.Contains(desc, "\n") { fmt.Printf("%s%s\n", prefix, quoteString(desc)) } else { desc = strings.Replace(desc, `"""`, `\"""`, -1) fmt.Printf("%s\"\"\"\n", prefix) for _, line := range strings.Split(desc, "\n") { fmt.Printf("%s%s\n", prefix, line) } fmt.Printf("%s\"\"\"\n", prefix) } } func printDeprecated(reason *string) { fmt.Print(" @deprecated") if reason != nil { fmt.Printf("(reason: %s)", quoteString(*reason)) } } func printType(t Type) { if t.Description != nil && *t.Description != "" { printDescription(*t.Description, "") } switch t.Kind { case TypeKindScalar: fmt.Printf("scalar %s\n\n", *t.Name) case TypeKindUnion: fmt.Printf("union %s = ", *t.Name) for idx, i := range t.PossibleTypes { if idx > 0 { fmt.Print(" | ") } fmt.Printf("%s", typeReference(i)) } fmt.Print("\n\n") case TypeKindEnum: fmt.Printf("enum %s {\n", *t.Name) for _, e := range t.EnumValues { if e.Description != nil && *e.Description != "" { printDescription(*e.Description, "\t") } fmt.Printf(" %s", e.Name) if e.IsDeprecated { printDeprecated(e.DeprecationReason) } fmt.Println() } fmt.Print("}\n\n") case TypeKindInputObject: fmt.Printf("input %s {\n", *t.Name) for _, f := range t.InputFields { if f.Description != nil && *f.Description != "" { printDescription(*f.Description, "\t") } fmt.Printf(" %s: %s", f.Name, typeReference(*f.Type)) if f.DefaultValue != nil { fmt.Printf(" = %s", *f.DefaultValue) } fmt.Println() } fmt.Print("}\n\n") case TypeKindObject, TypeKindInterface: fmt.Printf("%s %s", typeKeyword(t), *t.Name) if len(t.Interfaces) > 0 { fmt.Printf(" implements ") for idx, i := range t.Interfaces { if idx > 0 { fmt.Print(" & ") } fmt.Printf(typeReference(i)) } } fmt.Print(" {\n") for _, f := range t.Fields { if f.Description != nil && *f.Description != "" { printDescription(*f.Description, "\t") } fmt.Printf(" %s", f.Name) if len(f.Args) > 0 { fmt.Print("(") for idx, a := range f.Args { if idx > 0 { fmt.Print(", ") } fmt.Printf("%s: %s", a.Name, typeReference(*a.Type)) if a.DefaultValue != nil { fmt.Printf(" = %s", *a.DefaultValue) } } fmt.Print(")") } fmt.Printf(": %s", typeReference(*f.Type)) if f.IsDeprecated { printDeprecated(f.DeprecationReason) } fmt.Println() } fmt.Print("}\n\n") } } var builtInScalars = map[string]bool{ "Int": true, "Float": true, "String": true, "Boolean": true, "ID": true, } func isBuiltInType(t Type) bool { return strings.HasPrefix(*t.Name, "__") || builtInScalars[*t.Name] } func main() { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() var header []string flag.Var((*stringSliceFlag)(&header), "H", "set HTTP header") flag.Usage = func() { fmt.Fprint(os.Stderr, usage) } flag.Parse() endpoint := flag.Arg(0) if endpoint == "" { flag.Usage() os.Exit(1) } op := gqlclient.NewOperation(query) tr := transport{ RoundTripper: http.DefaultTransport, header: make(http.Header), } httpClient := http.Client{Transport: &tr} gqlClient := gqlclient.New(endpoint, &httpClient) for _, kv := range header { parts := strings.SplitN(kv, ":", 2) if len(parts) != 2 { log.Fatalf("in header definition %q: missing colon", kv) } tr.header.Add(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) } var data struct { Schema Schema `json:"__schema"` } if err := gqlClient.Execute(ctx, op, &data); err != nil { log.Fatal(err) } for _, t := range data.Schema.Types { if isBuiltInType(t) { continue } printType(t) } } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/cmd/gqlintrospect/types.go000066400000000000000000000072061447031735200303330ustar00rootroot00000000000000package main type Directive struct { Name string `json:"name"` Description *string `json:"description,omitempty"` Locations []DirectiveLocation `json:"locations"` Args []InputValue `json:"args"` IsRepeatable bool `json:"isRepeatable"` } type DirectiveLocation string const ( DirectiveLocationQuery DirectiveLocation = "QUERY" DirectiveLocationMutation DirectiveLocation = "MUTATION" DirectiveLocationSubscription DirectiveLocation = "SUBSCRIPTION" DirectiveLocationField DirectiveLocation = "FIELD" DirectiveLocationFragmentDefinition DirectiveLocation = "FRAGMENT_DEFINITION" DirectiveLocationFragmentSpread DirectiveLocation = "FRAGMENT_SPREAD" DirectiveLocationInlineFragment DirectiveLocation = "INLINE_FRAGMENT" DirectiveLocationVariableDefinition DirectiveLocation = "VARIABLE_DEFINITION" DirectiveLocationSchema DirectiveLocation = "SCHEMA" DirectiveLocationScalar DirectiveLocation = "SCALAR" DirectiveLocationObject DirectiveLocation = "OBJECT" DirectiveLocationFieldDefinition DirectiveLocation = "FIELD_DEFINITION" DirectiveLocationArgumentDefinition DirectiveLocation = "ARGUMENT_DEFINITION" DirectiveLocationInterface DirectiveLocation = "INTERFACE" DirectiveLocationUnion DirectiveLocation = "UNION" DirectiveLocationEnum DirectiveLocation = "ENUM" DirectiveLocationEnumValue DirectiveLocation = "ENUM_VALUE" DirectiveLocationInputObject DirectiveLocation = "INPUT_OBJECT" DirectiveLocationInputFieldDefinition DirectiveLocation = "INPUT_FIELD_DEFINITION" ) type EnumValue struct { Name string `json:"name"` Description *string `json:"description,omitempty"` IsDeprecated bool `json:"isDeprecated"` DeprecationReason *string `json:"deprecationReason,omitempty"` } type Field struct { Name string `json:"name"` Description *string `json:"description,omitempty"` Args []InputValue `json:"args"` Type *Type `json:"type"` IsDeprecated bool `json:"isDeprecated"` DeprecationReason *string `json:"deprecationReason,omitempty"` } type InputValue struct { Name string `json:"name"` Description *string `json:"description,omitempty"` Type *Type `json:"type"` DefaultValue *string `json:"defaultValue,omitempty"` } type Schema struct { Types []Type `json:"types"` QueryType *Type `json:"queryType"` MutationType *Type `json:"mutationType,omitempty"` SubscriptionType *Type `json:"subscriptionType,omitempty"` Directives []Directive `json:"directives"` } type Type struct { Kind TypeKind `json:"kind"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Fields []Field `json:"fields,omitempty"` Interfaces []Type `json:"interfaces,omitempty"` PossibleTypes []Type `json:"possibleTypes,omitempty"` EnumValues []EnumValue `json:"enumValues,omitempty"` InputFields []InputValue `json:"inputFields,omitempty"` OfType *Type `json:"ofType,omitempty"` } type TypeKind string const ( TypeKindScalar TypeKind = "SCALAR" TypeKindObject TypeKind = "OBJECT" TypeKindInterface TypeKind = "INTERFACE" TypeKindUnion TypeKind = "UNION" TypeKindEnum TypeKind = "ENUM" TypeKindInputObject TypeKind = "INPUT_OBJECT" TypeKindList TypeKind = "LIST" TypeKindNonNull TypeKind = "NON_NULL" ) golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/error.go000066400000000000000000000014351447031735200246550ustar00rootroot00000000000000package gqlclient import ( "encoding/json" ) // ErrorLocation describes an error location in a GraphQL document. // // Line and column numbers start from 1. type ErrorLocation struct { Line, Column int } // Error is a GraphQL error. type Error struct { Message string Locations []ErrorLocation Path []interface{} Extensions json.RawMessage } func (err *Error) Error() string { return "gqlclient: server failure: " + err.Message } // HTTPError is an HTTP response error. type HTTPError struct { StatusCode int statusText string err error } func (err *HTTPError) Error() string { s := "gqlclient: HTTP server error (" + err.statusText + ")" if err.err != nil { s += ": " + err.err.Error() } return s } func (err *HTTPError) Unwrap() error { return err.err } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/error_go1.19.go000066400000000000000000000001401447031735200256430ustar00rootroot00000000000000//go:build !go1.20 package gqlclient func joinErrors(errs []Error) error { return &errs[0] } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/error_go1.20.go000066400000000000000000000003111447031735200256330ustar00rootroot00000000000000//go:build go1.20 package gqlclient import ( "errors" ) func joinErrors(errs []Error) error { l := make([]error, len(errs)) for i := range errs { l[i] = &errs[i] } return errors.Join(l...) } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/example_test.go000066400000000000000000000021601447031735200262120ustar00rootroot00000000000000package gqlclient_test import ( "context" "log" "strings" "git.sr.ht/~emersion/gqlclient" ) func ExampleClient_Execute() { var ctx context.Context var c *gqlclient.Client op := gqlclient.NewOperation(`query { me { name } }`) var data struct { Me struct { Name string } } if err := c.Execute(ctx, op, &data); err != nil { log.Fatal(err) } log.Print(data) } func ExampleClient_Execute_vars() { var ctx context.Context var c *gqlclient.Client op := gqlclient.NewOperation(`query ($name: String!) { user(username: $name) { age } }`) op.Var("name", "emersion") var data struct { User struct { Age int } } if err := c.Execute(ctx, op, &data); err != nil { log.Fatal(err) } log.Print(data) } func ExampleClient_Execute_upload() { var ctx context.Context var c *gqlclient.Client op := gqlclient.NewOperation(`mutation ($file: Upload!) { send(file: $file) }`) op.Var("file", gqlclient.Upload{ Filename: "gopher.txt", MIMEType: "text/plain", Body: strings.NewReader("Hello, 世界"), }) if err := c.Execute(ctx, op, nil); err != nil { log.Fatal(err) } } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/go.mod000066400000000000000000000002771447031735200243060ustar00rootroot00000000000000module git.sr.ht/~emersion/gqlclient go 1.17 require ( github.com/dave/jennifer v1.7.0 github.com/vektah/gqlparser/v2 v2.5.8 ) require github.com/agnivade/levenshtein v1.1.1 // indirect golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/go.sum000066400000000000000000000053161447031735200243320ustar00rootroot00000000000000github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/dave/jennifer v1.7.0 h1:uRbSBH9UTS64yXbh4FrMHfgfY762RD+C7bUPKODpSJE= github.com/dave/jennifer v1.7.0/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/vektah/gqlparser/v2 v2.5.8 h1:pm6WOnGdzFOCfcQo9L3+xzW51mKrlwTEg4Wr7AH1JW4= github.com/vektah/gqlparser/v2 v2.5.8/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/time.go000066400000000000000000000010171447031735200244560ustar00rootroot00000000000000package gqlclient import ( "encoding/json" "time" ) const timeLayout = time.RFC3339Nano type Time struct { time.Time } func (t Time) MarshalJSON() ([]byte, error) { var v interface{} if !t.IsZero() { v = t.Format(timeLayout) } return json.Marshal(v) } func (t *Time) UnmarshalJSON(b []byte) error { if string(b) == "null" { t.Time = time.Time{} return nil } var s string if err := json.Unmarshal(b, &s); err != nil { return err } var err error t.Time, err = time.Parse(timeLayout, s) return err } golang-sourcehut-emersion-gqlclient-0.0~git20230820.8873fe0/upload.go000066400000000000000000000043721447031735200250130ustar00rootroot00000000000000package gqlclient import ( "encoding/json" "fmt" "io" "mime" "mime/multipart" "net/textproto" ) // Upload is a file upload. // // See the GraphQL multipart request specification for details: // https://github.com/jaydenseric/graphql-multipart-request-spec type Upload struct { Filename string MIMEType string Body io.Reader } // MarshalJSON implements json.Marshaler. func (Upload) MarshalJSON() ([]byte, error) { return json.Marshal(nil) } func writeMultipart(pw *io.PipeWriter, uploads map[string]Upload, operations io.Reader) (contentType string) { mw := multipart.NewWriter(pw) go func() { defer pw.Close() defer mw.Close() mapData := make(map[string][]string) for k := range uploads { mapData[k] = []string{"variables." + k} } h := make(textproto.MIMEHeader) h.Set("Content-Disposition", `form-data; name="operations"`) h.Set("Content-Type", "application/json") w, err := mw.CreatePart(h) if err != nil { pw.CloseWithError(fmt.Errorf("failed to create operations part: %v", err)) return } if _, err := io.Copy(w, operations); err != nil { pw.CloseWithError(fmt.Errorf("failed to write operations part: %v", err)) return } h = make(textproto.MIMEHeader) h.Set("Content-Disposition", `form-data; name="map"`) h.Set("Content-Type", "application/json") w, err = mw.CreatePart(h) if err != nil { pw.CloseWithError(fmt.Errorf("failed to create map part: %v", err)) return } if err := json.NewEncoder(w).Encode(mapData); err != nil { pw.CloseWithError(fmt.Errorf("failed to write map part: %v", err)) return } for k, upload := range uploads { dispParams := map[string]string{"name": k} if upload.Filename != "" { dispParams["filename"] = upload.Filename } h := make(textproto.MIMEHeader) h.Set("Content-Disposition", mime.FormatMediaType("form-data", dispParams)) if upload.MIMEType != "" { h.Set("Content-Type", upload.MIMEType) } w, err := mw.CreatePart(h) if err != nil { pw.CloseWithError(fmt.Errorf("failed to create upload %q part: %v", k, err)) return } if _, err := io.Copy(w, upload.Body); err != nil { pw.CloseWithError(fmt.Errorf("failed to write upload %q part: %v", k, err)) return } } }() return mw.FormDataContentType() }