Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions argument_coercion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package graphql_test

import (
"encoding/json"
"sort"
"testing"

"github.com/tailor-platform/graphql"
"github.com/tailor-platform/graphql/testutil"
)

// Serialises p.Args so tests can tell "absent", "null", and "value" apart.
func probeArgs(p graphql.ResolveParams) (interface{}, error) {
keys := make([]string, 0, len(p.Args))
for k := range p.Args {
keys = append(keys, k)
}
sort.Strings(keys)
out := map[string]interface{}{"keys": keys}
for _, k := range keys {
v := p.Args[k]
if v == nil {
out[k] = "null"
} else {
out[k] = v
}
}
b, _ := json.Marshal(out)
return string(b), nil
}

var coercionProbeInputObject = graphql.NewInputObject(graphql.InputObjectConfig{
Name: "CoercionProbeInput",
Fields: graphql.InputObjectConfigFieldMap{
"a": &graphql.InputObjectFieldConfig{Type: graphql.String},
"b": &graphql.InputObjectFieldConfig{Type: graphql.String},
},
})

var coercionProbeType = graphql.NewObject(graphql.ObjectConfig{
Name: "CoercionProbeQuery",
Fields: graphql.Fields{
"probe": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"a": &graphql.ArgumentConfig{Type: graphql.String},
"b": &graphql.ArgumentConfig{Type: graphql.String},
},
Resolve: probeArgs,
},
"probeObject": &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
"input": &graphql.ArgumentConfig{Type: coercionProbeInputObject},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
obj, _ := p.Args["input"].(map[string]interface{})
keys := make([]string, 0, len(obj))
for k := range obj {
keys = append(keys, k)
}
sort.Strings(keys)
b, _ := json.Marshal(map[string]interface{}{
"keys": keys,
"obj": obj,
})
return string(b), nil
},
},
},
})

var coercionProbeSchema, _ = graphql.NewSchema(graphql.SchemaConfig{Query: coercionProbeType})

func runProbe(t *testing.T, field, doc string, vars map[string]interface{}, want string) {
t.Helper()
parsed := testutil.TestParse(t, doc)
result := testutil.TestExecute(t, graphql.ExecuteParams{
Schema: coercionProbeSchema,
AST: parsed,
Args: vars,
})
if len(result.Errors) > 0 {
t.Fatalf("unexpected errors: %v", result.Errors)
}
data, _ := result.Data.(map[string]interface{})
got, _ := data[field].(string)
if got != want {
t.Fatalf("probe mismatch\n got: %s\n want: %s", got, want)
}
}

func TestArgumentCoercion_ScalarVariable_PreservesThreeStates(t *testing.T) {
doc := `query Probe($a: String, $b: String) { probe(a: $a, b: $b) }`

t.Run("variable omitted -> argument absent", func(t *testing.T) {
runProbe(t, "probe", doc,
map[string]interface{}{"a": "x"},
`{"a":"x","keys":["a"]}`)
})
t.Run("variable explicitly null -> argument present as null", func(t *testing.T) {
runProbe(t, "probe", doc,
map[string]interface{}{"a": "x", "b": nil},
`{"a":"x","b":"null","keys":["a","b"]}`)
})
t.Run("variable with value -> argument present with value", func(t *testing.T) {
runProbe(t, "probe", doc,
map[string]interface{}{"a": "x", "b": "y"},
`{"a":"x","b":"y","keys":["a","b"]}`)
})
}

func TestArgumentCoercion_InputObjectVariable_PreservesThreeStates(t *testing.T) {
doc := `query Probe($a: String, $b: String) { probeObject(input: {a: $a, b: $b}) }`

t.Run("nested variable omitted -> field absent in object", func(t *testing.T) {
runProbe(t, "probeObject", doc,
map[string]interface{}{"a": "x"},
`{"keys":["a"],"obj":{"a":"x"}}`)
})
t.Run("nested variable explicitly null -> field present as null", func(t *testing.T) {
runProbe(t, "probeObject", doc,
map[string]interface{}{"a": "x", "b": nil},
`{"keys":["a","b"],"obj":{"a":"x","b":null}}`)
})
t.Run("nested variable with value -> field present with value", func(t *testing.T) {
runProbe(t, "probeObject", doc,
map[string]interface{}{"a": "x", "b": "y"},
`{"keys":["a","b"],"obj":{"a":"x","b":"y"}}`)
})
}

func TestArgumentCoercion_InputObjectLiteral_OmittedFieldStaysAbsent(t *testing.T) {
doc := `{ probeObject(input: {a: "x"}) }`
runProbe(t, "probeObject", doc, nil,
`{"keys":["a"],"obj":{"a":"x"}}`)
}
70 changes: 33 additions & 37 deletions values.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ func getVariableValues(
continue
}
varName := defAST.Variable.Name.Value
if varValue, err := getVariableValue(schema, defAST, inputs[varName]); err != nil {
input, provided := inputs[varName]
varValue, err := getVariableValue(schema, defAST, input)
if err != nil {
return values, err
} else {
}
if provided || defAST.DefaultValue != nil {
values[varName] = varValue
}
}
Expand All @@ -50,28 +53,33 @@ func getArgumentValues(
}
results := map[string]interface{}{}
for _, argDef := range argDefs {
var (
tmp interface{}
value ast.Value
isUndefined bool
)
if tmpValue, ok := argASTMap[argDef.PrivateName]; ok {
value = tmpValue.Value
} else {
isUndefined = true
}
if tmp = valueFromAST(value, argDef.Type, variableValues); isNullish(tmp) {
var value ast.Value
argAST, ok := argASTMap[argDef.PrivateName]
if ok {
value = argAST.Value
}
isUndefined := !ok || isUnprovidedVariable(value, variableValues)
tmp := valueFromAST(value, argDef.Type, variableValues)
if isNullish(tmp) {
tmp = argDef.DefaultValue
}
if !isUndefined && tmp == nil {
results[argDef.PrivateName] = nil
} else if !isNullish(tmp) {
if !isUndefined || !isNullish(tmp) {
results[argDef.PrivateName] = tmp
}
}
return results
}

// Returns true if value is a reference to a variable the caller did not supply.
func isUnprovidedVariable(value ast.Value, variables map[string]interface{}) bool {
v, ok := value.(*ast.Variable)
if !ok || v.Name == nil {
return false
}
_, provided := variables[v.Name.Value]
return !provided
}

// Given a variable definition, and any value of input, return a value which
// adheres to the variable definition, or throw an error.
func getVariableValue(schema Schema, definitionAST *ast.VariableDefinition, input interface{}) (interface{}, error) {
Expand Down Expand Up @@ -381,37 +389,25 @@ func valueFromAST(valueAST ast.Value, ttype Input, variables map[string]interfac
}
return append(values, valueFromAST(valueAST, ttype.OfType, variables))
case *InputObject:
var (
ok bool
ov *ast.ObjectValue
of *ast.ObjectField
)
if ov, ok = valueAST.(*ast.ObjectValue); !ok {
ov, ok := valueAST.(*ast.ObjectValue)
if !ok {
return nil
}
fieldASTs := map[string]*ast.ObjectField{}
for _, of = range ov.Fields {
for _, of := range ov.Fields {
if of == nil || of.Name == nil {
continue
}
fieldASTs[of.Name.Value] = of
}
obj := map[string]interface{}{}
for name, field := range ttype.Fields() {
var (
value interface{}
isUndefined bool
)
if of, ok = fieldASTs[name]; ok {
value = valueFromAST(of.Value, field.Type, variables)
} else {
isUndefined = true
value = field.DefaultValue
}
if !isUndefined && value == nil {
obj[name] = nil
} else if !isNullish(value) {
obj[name] = value
of, ok := fieldASTs[name]
supplied := ok && !isUnprovidedVariable(of.Value, variables)
if supplied {
obj[name] = valueFromAST(of.Value, field.Type, variables)
} else if !isNullish(field.DefaultValue) {
obj[name] = field.DefaultValue
}
}
return obj
Expand Down
Loading