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