query.go

  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()