diff --git a/ast/path.go b/ast/path.go new file mode 100644 index 00000000..9af16843 --- /dev/null +++ b/ast/path.go @@ -0,0 +1,67 @@ +package ast + +import ( + "bytes" + "encoding/json" + "fmt" +) + +var _ json.Unmarshaler = (*Path)(nil) + +type Path []PathElement + +type PathElement interface { + isPathElement() +} + +var _ PathElement = PathIndex(0) +var _ PathElement = PathName("") + +func (path Path) String() string { + var str bytes.Buffer + for i, v := range path { + switch v := v.(type) { + case PathIndex: + str.WriteString(fmt.Sprintf("[%d]", v)) + case PathName: + if i != 0 { + str.WriteByte('.') + } + str.WriteString(string(v)) + default: + panic(fmt.Sprintf("unknown type: %T", v)) + } + } + return str.String() +} + +func (path *Path) UnmarshalJSON(b []byte) error { + var vs []interface{} + err := json.Unmarshal(b, &vs) + if err != nil { + return err + } + + *path = make([]PathElement, 0, len(vs)) + for _, v := range vs { + switch v := v.(type) { + case string: + *path = append(*path, PathName(v)) + case int: + *path = append(*path, PathIndex(v)) + case float64: + *path = append(*path, PathIndex(int(v))) + default: + return fmt.Errorf("unknown path element type: %T", v) + } + } + return nil +} + +type PathIndex int + +func (_ PathIndex) isPathElement() {} + +type PathName string + +func (_ PathName) isPathElement() {} diff --git a/ast/path_test.go b/ast/path_test.go new file mode 100644 index 00000000..e077563e --- /dev/null +++ b/ast/path_test.go @@ -0,0 +1,96 @@ +package ast + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPath_String(t *testing.T) { + type Spec struct { + Value Path + Expected string + } + specs := []*Spec{ + { + Value: Path{PathName("a"), PathIndex(2), PathName("c")}, + Expected: "a[2].c", + }, + { + Value: Path{}, + Expected: ``, + }, + { + Value: Path{PathIndex(1), PathName("b")}, + Expected: `[1].b`, + }, + } + + for _, spec := range specs { + t.Run(spec.Value.String(), func(t *testing.T) { + require.Equal(t, spec.Expected, spec.Value.String()) + }) + } +} + +func TestPath_MarshalJSON(t *testing.T) { + type Spec struct { + Value Path + Expected string + } + specs := []*Spec{ + { + Value: Path{PathName("a"), PathIndex(2), PathName("c")}, + Expected: `["a",2,"c"]`, + }, + { + Value: Path{}, + Expected: `[]`, + }, + { + Value: Path{PathIndex(1), PathName("b")}, + Expected: `[1,"b"]`, + }, + } + + for _, spec := range specs { + t.Run(spec.Value.String(), func(t *testing.T) { + b, err := json.Marshal(spec.Value) + require.Nil(t, err) + + require.Equal(t, spec.Expected, string(b)) + }) + } +} + +func TestPath_UnmarshalJSON(t *testing.T) { + type Spec struct { + Value string + Expected Path + } + specs := []*Spec{ + { + Value: `["a",2,"c"]`, + Expected: Path{PathName("a"), PathIndex(2), PathName("c")}, + }, + { + Value: `[]`, + Expected: Path{}, + }, + { + Value: `[1,"b"]`, + Expected: Path{PathIndex(1), PathName("b")}, + }, + } + + for _, spec := range specs { + t.Run(spec.Value, func(t *testing.T) { + var path Path + err := json.Unmarshal([]byte(spec.Value), &path) + require.Nil(t, err) + + require.Equal(t, spec.Expected, path) + }) + } +} diff --git a/ast/source.go b/ast/source.go index 08c66689..2949f83f 100644 --- a/ast/source.go +++ b/ast/source.go @@ -3,13 +3,13 @@ package ast // Source covers a single *.graphql file type Source struct { // Name is the filename of the source - Name string + Name string // Input is the actual contents of the source file - Input string + Input string // BuiltIn indicate whether the source is a part of the specification BuiltIn bool } - + type Position struct { Start int // The starting position, in runes, of this token in the input. End int // The end position, in runes, of this token in the input. diff --git a/gqlerror/error.go b/gqlerror/error.go index c4c0847a..62d3dd97 100644 --- a/gqlerror/error.go +++ b/gqlerror/error.go @@ -11,7 +11,7 @@ import ( // Error is the standard graphql error type described in https://facebook.github.io/graphql/draft/#sec-Errors type Error struct { Message string `json:"message"` - Path []interface{} `json:"path,omitempty"` + Path ast.Path `json:"path,omitempty"` Locations []Location `json:"locations,omitempty"` Extensions map[string]interface{} `json:"extensions,omitempty"` Rule string `json:"-"` @@ -63,20 +63,7 @@ func (err *Error) Error() string { } func (err Error) pathString() string { - var str bytes.Buffer - for i, v := range err.Path { - - switch v := v.(type) { - case int, int64: - str.WriteString(fmt.Sprintf("[%d]", v)) - default: - if i != 0 { - str.WriteByte('.') - } - str.WriteString(fmt.Sprint(v)) - } - } - return str.String() + return err.Path.String() } func (errs List) Error() string { @@ -88,7 +75,7 @@ func (errs List) Error() string { return buf.String() } -func WrapPath(path []interface{}, err error) *Error { +func WrapPath(path ast.Path, err error) *Error { return &Error{ Message: err.Error(), Path: path, @@ -101,7 +88,7 @@ func Errorf(message string, args ...interface{}) *Error { } } -func ErrorPathf(path []interface{}, message string, args ...interface{}) *Error { +func ErrorPathf(path ast.Path, message string, args ...interface{}) *Error { return &Error{ Message: fmt.Sprintf(message, args...), Path: path, diff --git a/gqlerror/error_test.go b/gqlerror/error_test.go index 1d5fb784..68dd5efe 100644 --- a/gqlerror/error_test.go +++ b/gqlerror/error_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/require" + "github.com/vektah/gqlparser/ast" ) func TestErrorFormatting(t *testing.T) { @@ -22,7 +23,7 @@ func TestErrorFormatting(t *testing.T) { }) t.Run("with path", func(t *testing.T) { - err := ErrorPathf([]interface{}{"a", 1, "b"}, "kabloom") + err := ErrorPathf(ast.Path{ast.PathName("a"), ast.PathIndex(1), ast.PathName("b")}, "kabloom") require.Equal(t, `input: a[1].b kabloom`, err.Error()) }) diff --git a/validator/vars.go b/validator/vars.go index 40af5461..d109ad8c 100644 --- a/validator/vars.go +++ b/validator/vars.go @@ -17,12 +17,12 @@ func VariableValues(schema *ast.Schema, op *ast.OperationDefinition, variables m coercedVars := map[string]interface{}{} validator := varValidator{ - path: []interface{}{"variable"}, + path: ast.Path{ast.PathName("variable")}, schema: schema, } for _, v := range op.VariableDefinitions { - validator.path = append(validator.path, v.Variable) + validator.path = append(validator.path, ast.PathName(v.Variable)) if !v.Definition.IsInputType() { return nil, gqlerror.ErrorPathf(validator.path, "must an input type") @@ -69,7 +69,7 @@ func VariableValues(schema *ast.Schema, op *ast.OperationDefinition, variables m } type varValidator struct { - path []interface{} + path ast.Path schema *ast.Schema } @@ -87,7 +87,7 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) *gqlerr for i := 0; i < val.Len(); i++ { resetPath() - v.path = append(v.path, i) + v.path = append(v.path, ast.PathIndex(i)) field := val.Index(i) if field.Kind() == reflect.Ptr || field.Kind() == reflect.Interface { @@ -171,7 +171,7 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) *gqlerr val.MapIndex(name) fieldDef := def.Fields.ForName(name.String()) resetPath() - v.path = append(v.path, name.String()) + v.path = append(v.path, ast.PathName(name.String())) if fieldDef == nil { return gqlerror.ErrorPathf(v.path, "unknown field") @@ -180,7 +180,7 @@ func (v *varValidator) validateVarType(typ *ast.Type, val reflect.Value) *gqlerr for _, fieldDef := range def.Fields { resetPath() - v.path = append(v.path, fieldDef.Name) + v.path = append(v.path, ast.PathName(fieldDef.Name)) field := val.MapIndex(reflect.ValueOf(fieldDef.Name)) if !field.IsValid() {