1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
package graphql
import (
"bytes"
"encoding/json"
"io"
"reflect"
"sort"
"github.com/cli/shurcooL-graphql/ident"
)
func constructQuery(v any, variables map[string]any, queryName string) string {
query := query(v)
if len(variables) > 0 {
return "query" + queryNameFormat(queryName) + "(" + queryArguments(variables) + ")" + query
} else if queryName != "" {
return "query" + queryNameFormat(queryName) + query
}
return query
}
func constructMutation(v any, variables map[string]any, queryName string) string {
query := query(v)
if len(variables) > 0 {
return "mutation" + queryNameFormat(queryName) + "(" + queryArguments(variables) + ")" + query
}
return "mutation" + queryNameFormat(queryName) + query
}
func queryNameFormat(n string) string {
if n != "" {
return " " + n
}
return n
}
// queryArguments constructs a minified arguments string for variables.
//
// E.g., map[string]any{"a": Int(123), "b": NewBoolean(true)} -> "$a:Int!$b:Boolean".
func queryArguments(variables map[string]any) string {
// Sort keys in order to produce deterministic output for testing purposes.
// TODO: If tests can be made to work with non-deterministic output, then no need to sort.
keys := make([]string, 0, len(variables))
for k := range variables {
keys = append(keys, k)
}
sort.Strings(keys)
var buf bytes.Buffer
for _, k := range keys {
_, _ = io.WriteString(&buf, "$")
_, _ = io.WriteString(&buf, k)
_, _ = io.WriteString(&buf, ":")
writeArgumentType(&buf, reflect.TypeOf(variables[k]), true)
// Don't insert a comma here.
// Commas in GraphQL are insignificant, and we want minified output.
// See https://spec.graphql.org/October2021/#sec-Insignificant-Commas.
}
return buf.String()
}
// writeArgumentType writes a minified GraphQL type for t to w.
// Argument value indicates whether t is a value (required) type or pointer (optional) type.
// If value is true, then "!" is written at the end of t.
func writeArgumentType(w io.Writer, t reflect.Type, value bool) {
if t.Kind() == reflect.Ptr {
// Pointer is an optional type, so no "!" at the end of the pointer's underlying type.
writeArgumentType(w, t.Elem(), false)
return
}
switch t.Kind() {
case reflect.Slice, reflect.Array:
// List. E.g., "[Int]".
_, _ = io.WriteString(w, "[")
writeArgumentType(w, t.Elem(), true)
_, _ = io.WriteString(w, "]")
default:
// Named type. E.g., "Int".
name := t.Name()
if name == "string" { // HACK: Workaround for https://github.com/shurcooL/githubv4/issues/12.
name = "ID"
}
_, _ = io.WriteString(w, name)
}
if value {
// Value is a required type, so add "!" to the end.
_, _ = io.WriteString(w, "!")
}
}
// query uses writeQuery to recursively construct
// a minified query string from the provided struct v.
//
// E.g., struct{Foo Int, BarBaz *Boolean} -> "{foo,barBaz}".
func query(v any) string {
var buf bytes.Buffer
writeQuery(&buf, reflect.TypeOf(v), false)
return buf.String()
}
// writeQuery writes a minified query for t to w.
// If inline is true, the struct fields of t are inlined into parent struct.
func writeQuery(w io.Writer, t reflect.Type, inline bool) {
switch t.Kind() {
case reflect.Ptr, reflect.Slice:
writeQuery(w, t.Elem(), false)
case reflect.Struct:
// If the type implements json.Unmarshaler, it's a scalar. Don't expand it.
if reflect.PtrTo(t).Implements(jsonUnmarshaler) {
return
}
if !inline {
_, _ = io.WriteString(w, "{")
}
for i := 0; i < t.NumField(); i++ {
if i != 0 {
_, _ = io.WriteString(w, ",")
}
f := t.Field(i)
value, ok := f.Tag.Lookup("graphql")
inlineField := f.Anonymous && !ok
if !inlineField {
if ok {
_, _ = io.WriteString(w, value)
} else {
_, _ = io.WriteString(w, ident.ParseMixedCaps(f.Name).ToLowerCamelCase())
}
}
writeQuery(w, f.Type, inlineField)
}
if !inline {
_, _ = io.WriteString(w, "}")
}
}
}
var jsonUnmarshaler = reflect.TypeOf((*json.Unmarshaler)(nil)).Elem()
|