Skip to content

Commit

Permalink
Merge pull request #430 from Ackar/nullable-types
Browse files Browse the repository at this point in the history
Add support for null vs missing input value
  • Loading branch information
pavelnikolov authored Mar 5, 2021
2 parents beb923f + fced4f6 commit 6859f27
Show file tree
Hide file tree
Showing 3 changed files with 376 additions and 20 deletions.
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

0 comments on commit 6859f27

Please sign in to comment.