Skip to content

Commit

Permalink
schema package additions (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
ccbrown committed Mar 24, 2020
1 parent 996c55b commit aa70097
Show file tree
Hide file tree
Showing 12 changed files with 101,235 additions and 8 deletions.
2 changes: 1 addition & 1 deletion graphql/schema/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ var IDType = &ScalarType{
},
}

var builtinTypes = map[string]*ScalarType{
var BuiltInTypes = map[string]*ScalarType{
"Int": IntType,
"Float": FloatType,
"String": StringType,
Expand Down
8 changes: 8 additions & 0 deletions graphql/schema/input_object_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ type InputObjectType struct {
// Otherwise the objects will remain as maps. This function is called after all fields are fully
// coerced.
InputCoercion func(map[string]interface{}) (interface{}, error)

// Normally input objects only need to be coerced from inputs. However, if an argument of this
// type is given a default value, we need to be able to do the reverse in order to serialize it
// for introspection queries.
//
// For most use-cases, this function is optional. If it is required, but nil, you will get an
// error when you attempt to create the schema.
ResultCoercion func(interface{}) (map[string]interface{}, error)
}

func (t *InputObjectType) String() string {
Expand Down
5 changes: 5 additions & 0 deletions graphql/schema/input_value_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ func (d *InputValueDefinition) shallowValidate() error {
} else if !d.Type.IsInputType() {
return fmt.Errorf("%v cannot be used as an input value type", d.Type)
}
if d.DefaultValue != nil && d.DefaultValue != Null {
if obj, ok := d.Type.(*InputObjectType); ok && obj.ResultCoercion == nil {
return fmt.Errorf("assigning a default value to a %v requires it to define a result coercion function", d.Type)
}
}
return nil
}
7 changes: 3 additions & 4 deletions graphql/schema/introspection/introspection.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package introspection

import (
"encoding/json"
"fmt"

"github.com/ccbrown/api-fu/graphql/schema"
Expand Down Expand Up @@ -522,9 +521,9 @@ var InputValueType = &schema.ObjectType{
"defaultValue": &schema.FieldDefinition{
Type: schema.StringType,
Resolve: func(ctx *schema.FieldContext) (interface{}, error) {
if v := ctx.Object.(inputValue).Definition.DefaultValue; v != nil {
b, err := json.Marshal(v)
return string(b), err
def := ctx.Object.(inputValue).Definition
if v := def.DefaultValue; v != nil {
return marshalValue(def.Type, v)
}
return nil, nil
},
Expand Down
3 changes: 2 additions & 1 deletion graphql/schema/introspection/introspection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import (
"context"
"testing"

"github.com/stretchr/testify/require"

"github.com/ccbrown/api-fu/graphql/executor"
"github.com/ccbrown/api-fu/graphql/parser"
"github.com/ccbrown/api-fu/graphql/schema"
"github.com/ccbrown/api-fu/graphql/schema/introspection"
"github.com/stretchr/testify/require"
)

var petType = &schema.InterfaceType{
Expand Down
60 changes: 60 additions & 0 deletions graphql/schema/introspection/marshal_value.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package introspection

import (
"encoding/json"
"fmt"
"reflect"
"strings"

"github.com/ccbrown/api-fu/graphql/schema"
)

// Used to marshal input types for default value introspection.
func marshalValue(t schema.Type, v interface{}) (string, error) {
if v == schema.Null {
return "null", nil
}

switch t := t.(type) {
case *schema.ScalarType:
b, err := json.Marshal(v)
return string(b), err
case *schema.ListType:
v := reflect.ValueOf(v)
if v.Kind() != reflect.Slice {
return "", fmt.Errorf("default value is not a slice")
}
parts := make([]string, v.Len())
for i := range parts {
s, err := marshalValue(t.Type, v.Index(i).Interface())
if err != nil {
return "", err
}
parts[i] = s
}
return "[" + strings.Join(parts, ", ") + "]", nil
case *schema.InputObjectType:
if t.ResultCoercion == nil {
return "", fmt.Errorf("%v cannot be serialized", t.Name)
}
kv, err := t.ResultCoercion(v)
if err != nil {
return "", err
}
parts := make([]string, 0, len(kv))
for k, v := range kv {
s, err := marshalValue(t.Fields[k].Type, v)
if err != nil {
return "", err
}
parts = append(parts, k+": "+s)
}
return "{" + strings.Join(parts, ", ") + "}", nil
case *schema.EnumType:
return t.CoerceResult(v)
case *schema.NonNullType:
return marshalValue(t.Type, v)
default:
return "", fmt.Errorf("unsupported value type: %T", t)
}
}
57 changes: 57 additions & 0 deletions graphql/schema/introspection/marshal_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package introspection

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ccbrown/api-fu/graphql/schema"
)

func TestMarshalValue(t *testing.T) {
for name, tc := range map[string]struct {
Type schema.Type
Value interface{}
Expected string
}{
"InputObject": {
Type: &schema.InputObjectType{
Fields: map[string]*schema.InputValueDefinition{
"foo": &schema.InputValueDefinition{
Type: schema.IntType,
},
},
ResultCoercion: func(v interface{}) (map[string]interface{}, error) {
return map[string]interface{}{
"foo": v,
}, nil
},
},
Value: 1,
Expected: "{foo: 1}",
},
"List": {
Type: schema.NewListType(schema.IntType),
Value: []int{1, 2},
Expected: "[1, 2]",
},
"Enum": {
Type: &schema.EnumType{
Name: "FooBarEnum",
Values: map[string]*schema.EnumValueDefinition{
"FOO": &schema.EnumValueDefinition{Value: 1},
"BAR": &schema.EnumValueDefinition{Value: 2},
},
},
Value: 1,
Expected: "FOO",
},
} {
t.Run(name, func(t *testing.T) {
s, err := marshalValue(tc.Type, tc.Value)
require.NoError(t, err)
assert.Equal(t, tc.Expected, s)
})
}
}
Loading

0 comments on commit aa70097

Please sign in to comment.