Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for null vs missing input value #430

Merged
merged 1 commit into from
Mar 5, 2021
Merged
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
184 changes: 183 additions & 1 deletion graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2997,7 +2997,7 @@ func TestInput(t *testing.T) {
})
}

type inputArgumentsHello struct {}
type inputArgumentsHello struct{}

type inputArgumentsScalarMismatch1 struct{}

Expand Down Expand Up @@ -3755,3 +3755,185 @@ func TestPointerReturnForNonNull(t *testing.T) {
},
})
}

type nullableInput struct {
String graphql.NullString
Int graphql.NullInt
Bool graphql.NullBool
Time graphql.NullTime
Float graphql.NullFloat
}

type nullableResult struct {
String string
Int string
Bool string
Time string
Float string
}

type nullableResolver struct {
}

func (r *nullableResolver) TestNullables(args struct {
Input *nullableInput
}) nullableResult {
var res nullableResult
if args.Input.String.Set {
if args.Input.String.Value == nil {
res.String = "<nil>"
} else {
res.String = *args.Input.String.Value
}
}

if args.Input.Int.Set {
if args.Input.Int.Value == nil {
res.Int = "<nil>"
} else {
res.Int = fmt.Sprintf("%d", *args.Input.Int.Value)
}
}

if args.Input.Float.Set {
if args.Input.Float.Value == nil {
res.Float = "<nil>"
} else {
res.Float = fmt.Sprintf("%.2f", *args.Input.Float.Value)
}
}

if args.Input.Bool.Set {
if args.Input.Bool.Value == nil {
res.Bool = "<nil>"
} else {
res.Bool = fmt.Sprintf("%t", *args.Input.Bool.Value)
}
}

if args.Input.Time.Set {
if args.Input.Time.Value == nil {
res.Time = "<nil>"
} else {
res.Time = args.Input.Time.Value.Format(time.RFC3339)
}
}

return res
}

func TestNullable(t *testing.T) {
schema := `
scalar Time

input MyInput {
string: String
int: Int
float: Float
bool: Boolean
time: Time
}

type Result {
string: String!
int: String!
float: String!
bool: String!
time: String!
}

type Query {
testNullables(input: MyInput): Result!
}
`

gqltesting.RunTests(t, []*gqltesting.Test{
{
Schema: graphql.MustParseSchema(schema, &nullableResolver{}, graphql.UseFieldResolvers()),
Query: `
query {
testNullables(input: {
string: "test"
int: 1234
float: 42.42
bool: true
time: "2021-01-02T15:04:05Z"
}) {
string
int
float
bool
time
}
}
`,
ExpectedResult: `
{
"testNullables": {
"string": "test",
"int": "1234",
"float": "42.42",
"bool": "true",
"time": "2021-01-02T15:04:05Z"
}
}
`,
},
{
Schema: graphql.MustParseSchema(schema, &nullableResolver{}, graphql.UseFieldResolvers()),
Query: `
query {
testNullables(input: {
string: null
int: null
float: null
bool: null
time: null
}) {
string
int
float
bool
time
}
}
`,
ExpectedResult: `
{
"testNullables": {
"string": "<nil>",
"int": "<nil>",
"float": "<nil>",
"bool": "<nil>",
"time": "<nil>"
}
}
`,
},
{
Schema: graphql.MustParseSchema(schema, &nullableResolver{}, graphql.UseFieldResolvers()),
Query: `
query {
testNullables(input: {}) {
string
int
float
bool
time
}
}
`,
ExpectedResult: `
{
"testNullables": {
"string": "",
"int": "",
"float": "",
"bool": "",
"time": ""
}
}
`,
},
})
}
62 changes: 43 additions & 19 deletions internal/exec/packer/packer.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,24 +78,37 @@ func (b *Builder) assignPacker(target *packer, schemaType common.Type, reflectTy
func (b *Builder) makePacker(schemaType common.Type, reflectType reflect.Type) (packer, error) {
t, nonNull := unwrapNonNull(schemaType)
if !nonNull {
if reflectType.Kind() != reflect.Ptr {
return nil, fmt.Errorf("%s is not a pointer", reflectType)
}
elemType := reflectType.Elem()
addPtr := true
if _, ok := t.(*schema.InputObject); ok {
elemType = reflectType // keep pointer for input objects
addPtr = false
}
elem, err := b.makeNonNullPacker(t, elemType)
if err != nil {
return nil, err
if reflectType.Kind() == reflect.Ptr {
elemType := reflectType.Elem()
addPtr := true
if _, ok := t.(*schema.InputObject); ok {
elemType = reflectType // keep pointer for input objects
addPtr = false
}
elem, err := b.makeNonNullPacker(t, elemType)
if err != nil {
return nil, err
}
return &nullPacker{
elemPacker: elem,
valueType: reflectType,
addPtr: addPtr,
}, nil
} else if isNullable(reflectType) {
elemType := reflectType
addPtr := false
elem, err := b.makeNonNullPacker(t, elemType)
if err != nil {
return nil, err
}
return &nullPacker{
elemPacker: elem,
valueType: reflectType,
addPtr: addPtr,
}, nil
} else {
return nil, fmt.Errorf("%s is not a pointer or a nullable type", reflectType)
}
return &nullPacker{
elemPacker: elem,
valueType: reflectType,
addPtr: addPtr,
}, nil
}

return b.makeNonNullPacker(t, reflectType)
Expand Down Expand Up @@ -266,7 +279,7 @@ type nullPacker struct {
}

func (p *nullPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
if value == nil && !isNullable(p.valueType) {
return reflect.Zero(p.valueType), nil
}

Expand Down Expand Up @@ -305,7 +318,7 @@ type unmarshalerPacker struct {
}

func (p *unmarshalerPacker) Pack(value interface{}) (reflect.Value, error) {
if value == nil {
if value == nil && !isNullable(p.ValueType) {
return reflect.Value{}, errors.Errorf("got null for non-null")
}

Expand Down Expand Up @@ -369,3 +382,14 @@ func unwrapNonNull(t common.Type) (common.Type, bool) {
func stripUnderscore(s string) string {
return strings.Replace(s, "_", "", -1)
}

// NullUnmarshaller is an unmarshaller that can handle a nil input
type NullUnmarshaller interface {
Unmarshaler
Nullable()
}

func isNullable(t reflect.Type) bool {
_, ok := reflect.New(t).Interface().(NullUnmarshaller)
return ok
}
Loading