Skip to content

Commit 6859f27

Browse files
authored
Merge pull request #430 from Ackar/nullable-types
Add support for null vs missing input value
2 parents beb923f + fced4f6 commit 6859f27

File tree

3 files changed

+376
-20
lines changed

3 files changed

+376
-20
lines changed

graphql_test.go

Lines changed: 183 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2997,7 +2997,7 @@ func TestInput(t *testing.T) {
29972997
})
29982998
}
29992999

3000-
type inputArgumentsHello struct {}
3000+
type inputArgumentsHello struct{}
30013001

30023002
type inputArgumentsScalarMismatch1 struct{}
30033003

@@ -3755,3 +3755,185 @@ func TestPointerReturnForNonNull(t *testing.T) {
37553755
},
37563756
})
37573757
}
3758+
3759+
type nullableInput struct {
3760+
String graphql.NullString
3761+
Int graphql.NullInt
3762+
Bool graphql.NullBool
3763+
Time graphql.NullTime
3764+
Float graphql.NullFloat
3765+
}
3766+
3767+
type nullableResult struct {
3768+
String string
3769+
Int string
3770+
Bool string
3771+
Time string
3772+
Float string
3773+
}
3774+
3775+
type nullableResolver struct {
3776+
}
3777+
3778+
func (r *nullableResolver) TestNullables(args struct {
3779+
Input *nullableInput
3780+
}) nullableResult {
3781+
var res nullableResult
3782+
if args.Input.String.Set {
3783+
if args.Input.String.Value == nil {
3784+
res.String = "<nil>"
3785+
} else {
3786+
res.String = *args.Input.String.Value
3787+
}
3788+
}
3789+
3790+
if args.Input.Int.Set {
3791+
if args.Input.Int.Value == nil {
3792+
res.Int = "<nil>"
3793+
} else {
3794+
res.Int = fmt.Sprintf("%d", *args.Input.Int.Value)
3795+
}
3796+
}
3797+
3798+
if args.Input.Float.Set {
3799+
if args.Input.Float.Value == nil {
3800+
res.Float = "<nil>"
3801+
} else {
3802+
res.Float = fmt.Sprintf("%.2f", *args.Input.Float.Value)
3803+
}
3804+
}
3805+
3806+
if args.Input.Bool.Set {
3807+
if args.Input.Bool.Value == nil {
3808+
res.Bool = "<nil>"
3809+
} else {
3810+
res.Bool = fmt.Sprintf("%t", *args.Input.Bool.Value)
3811+
}
3812+
}
3813+
3814+
if args.Input.Time.Set {
3815+
if args.Input.Time.Value == nil {
3816+
res.Time = "<nil>"
3817+
} else {
3818+
res.Time = args.Input.Time.Value.Format(time.RFC3339)
3819+
}
3820+
}
3821+
3822+
return res
3823+
}
3824+
3825+
func TestNullable(t *testing.T) {
3826+
schema := `
3827+
scalar Time
3828+
3829+
input MyInput {
3830+
string: String
3831+
int: Int
3832+
float: Float
3833+
bool: Boolean
3834+
time: Time
3835+
}
3836+
3837+
type Result {
3838+
string: String!
3839+
int: String!
3840+
float: String!
3841+
bool: String!
3842+
time: String!
3843+
}
3844+
3845+
type Query {
3846+
testNullables(input: MyInput): Result!
3847+
}
3848+
`
3849+
3850+
gqltesting.RunTests(t, []*gqltesting.Test{
3851+
{
3852+
Schema: graphql.MustParseSchema(schema, &nullableResolver{}, graphql.UseFieldResolvers()),
3853+
Query: `
3854+
query {
3855+
testNullables(input: {
3856+
string: "test"
3857+
int: 1234
3858+
float: 42.42
3859+
bool: true
3860+
time: "2021-01-02T15:04:05Z"
3861+
}) {
3862+
string
3863+
int
3864+
float
3865+
bool
3866+
time
3867+
}
3868+
}
3869+
`,
3870+
ExpectedResult: `
3871+
{
3872+
"testNullables": {
3873+
"string": "test",
3874+
"int": "1234",
3875+
"float": "42.42",
3876+
"bool": "true",
3877+
"time": "2021-01-02T15:04:05Z"
3878+
}
3879+
}
3880+
`,
3881+
},
3882+
{
3883+
Schema: graphql.MustParseSchema(schema, &nullableResolver{}, graphql.UseFieldResolvers()),
3884+
Query: `
3885+
query {
3886+
testNullables(input: {
3887+
string: null
3888+
int: null
3889+
float: null
3890+
bool: null
3891+
time: null
3892+
}) {
3893+
string
3894+
int
3895+
float
3896+
bool
3897+
time
3898+
}
3899+
}
3900+
`,
3901+
ExpectedResult: `
3902+
{
3903+
"testNullables": {
3904+
"string": "<nil>",
3905+
"int": "<nil>",
3906+
"float": "<nil>",
3907+
"bool": "<nil>",
3908+
"time": "<nil>"
3909+
}
3910+
}
3911+
`,
3912+
},
3913+
{
3914+
Schema: graphql.MustParseSchema(schema, &nullableResolver{}, graphql.UseFieldResolvers()),
3915+
Query: `
3916+
query {
3917+
testNullables(input: {}) {
3918+
string
3919+
int
3920+
float
3921+
bool
3922+
time
3923+
}
3924+
}
3925+
`,
3926+
ExpectedResult: `
3927+
{
3928+
"testNullables": {
3929+
"string": "",
3930+
"int": "",
3931+
"float": "",
3932+
"bool": "",
3933+
"time": ""
3934+
}
3935+
}
3936+
`,
3937+
},
3938+
})
3939+
}

internal/exec/packer/packer.go

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,37 @@ func (b *Builder) assignPacker(target *packer, schemaType common.Type, reflectTy
7878
func (b *Builder) makePacker(schemaType common.Type, reflectType reflect.Type) (packer, error) {
7979
t, nonNull := unwrapNonNull(schemaType)
8080
if !nonNull {
81-
if reflectType.Kind() != reflect.Ptr {
82-
return nil, fmt.Errorf("%s is not a pointer", reflectType)
83-
}
84-
elemType := reflectType.Elem()
85-
addPtr := true
86-
if _, ok := t.(*schema.InputObject); ok {
87-
elemType = reflectType // keep pointer for input objects
88-
addPtr = false
89-
}
90-
elem, err := b.makeNonNullPacker(t, elemType)
91-
if err != nil {
92-
return nil, err
81+
if reflectType.Kind() == reflect.Ptr {
82+
elemType := reflectType.Elem()
83+
addPtr := true
84+
if _, ok := t.(*schema.InputObject); ok {
85+
elemType = reflectType // keep pointer for input objects
86+
addPtr = false
87+
}
88+
elem, err := b.makeNonNullPacker(t, elemType)
89+
if err != nil {
90+
return nil, err
91+
}
92+
return &nullPacker{
93+
elemPacker: elem,
94+
valueType: reflectType,
95+
addPtr: addPtr,
96+
}, nil
97+
} else if isNullable(reflectType) {
98+
elemType := reflectType
99+
addPtr := false
100+
elem, err := b.makeNonNullPacker(t, elemType)
101+
if err != nil {
102+
return nil, err
103+
}
104+
return &nullPacker{
105+
elemPacker: elem,
106+
valueType: reflectType,
107+
addPtr: addPtr,
108+
}, nil
109+
} else {
110+
return nil, fmt.Errorf("%s is not a pointer or a nullable type", reflectType)
93111
}
94-
return &nullPacker{
95-
elemPacker: elem,
96-
valueType: reflectType,
97-
addPtr: addPtr,
98-
}, nil
99112
}
100113

101114
return b.makeNonNullPacker(t, reflectType)
@@ -266,7 +279,7 @@ type nullPacker struct {
266279
}
267280

268281
func (p *nullPacker) Pack(value interface{}) (reflect.Value, error) {
269-
if value == nil {
282+
if value == nil && !isNullable(p.valueType) {
270283
return reflect.Zero(p.valueType), nil
271284
}
272285

@@ -305,7 +318,7 @@ type unmarshalerPacker struct {
305318
}
306319

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

@@ -369,3 +382,14 @@ func unwrapNonNull(t common.Type) (common.Type, bool) {
369382
func stripUnderscore(s string) string {
370383
return strings.Replace(s, "_", "", -1)
371384
}
385+
386+
// NullUnmarshaller is an unmarshaller that can handle a nil input
387+
type NullUnmarshaller interface {
388+
Unmarshaler
389+
Nullable()
390+
}
391+
392+
func isNullable(t reflect.Type) bool {
393+
_, ok := reflect.New(t).Interface().(NullUnmarshaller)
394+
return ok
395+
}

0 commit comments

Comments
 (0)