diff --git a/example/starwars/starwars.go b/example/starwars/starwars.go index aa1b51e23c4..23eb068dec0 100644 --- a/example/starwars/starwars.go +++ b/example/starwars/starwars.go @@ -8,6 +8,8 @@ import ( "fmt" "strconv" "strings" + + graphql "github.com/neelance/graphql-go" ) var Schema = ` @@ -142,47 +144,47 @@ var Schema = ` ` type human struct { - ID string + ID graphql.ID Name string - Friends []string + Friends []graphql.ID AppearsIn []string Height float64 Mass int - Starships []string + Starships []graphql.ID } var humans = []*human{ { ID: "1000", Name: "Luke Skywalker", - Friends: []string{"1002", "1003", "2000", "2001"}, + Friends: []graphql.ID{"1002", "1003", "2000", "2001"}, AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, Height: 1.72, Mass: 77, - Starships: []string{"3001", "3003"}, + Starships: []graphql.ID{"3001", "3003"}, }, { ID: "1001", Name: "Darth Vader", - Friends: []string{"1004"}, + Friends: []graphql.ID{"1004"}, AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, Height: 2.02, Mass: 136, - Starships: []string{"3002"}, + Starships: []graphql.ID{"3002"}, }, { ID: "1002", Name: "Han Solo", - Friends: []string{"1000", "1003", "2001"}, + Friends: []graphql.ID{"1000", "1003", "2001"}, AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, Height: 1.8, Mass: 80, - Starships: []string{"3000", "3003"}, + Starships: []graphql.ID{"3000", "3003"}, }, { ID: "1003", Name: "Leia Organa", - Friends: []string{"1000", "1002", "2000", "2001"}, + Friends: []graphql.ID{"1000", "1002", "2000", "2001"}, AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, Height: 1.5, Mass: 49, @@ -190,14 +192,14 @@ var humans = []*human{ { ID: "1004", Name: "Wilhuff Tarkin", - Friends: []string{"1001"}, + Friends: []graphql.ID{"1001"}, AppearsIn: []string{"NEWHOPE"}, Height: 1.8, Mass: 0, }, } -var humanData = make(map[string]*human) +var humanData = make(map[graphql.ID]*human) func init() { for _, h := range humans { @@ -206,9 +208,9 @@ func init() { } type droid struct { - ID string + ID graphql.ID Name string - Friends []string + Friends []graphql.ID AppearsIn []string PrimaryFunction string } @@ -217,20 +219,20 @@ var droids = []*droid{ { ID: "2000", Name: "C-3PO", - Friends: []string{"1000", "1002", "1003", "2001"}, + Friends: []graphql.ID{"1000", "1002", "1003", "2001"}, AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, PrimaryFunction: "Protocol", }, { ID: "2001", Name: "R2-D2", - Friends: []string{"1000", "1002", "1003"}, + Friends: []graphql.ID{"1000", "1002", "1003"}, AppearsIn: []string{"NEWHOPE", "EMPIRE", "JEDI"}, PrimaryFunction: "Astromech", }, } -var droidData = make(map[string]*droid) +var droidData = make(map[graphql.ID]*droid) func init() { for _, d := range droids { @@ -239,7 +241,7 @@ func init() { } type starship struct { - ID string + ID graphql.ID Name string Length float64 } @@ -267,7 +269,7 @@ var starships = []*starship{ }, } -var starshipData = make(map[string]*starship) +var starshipData = make(map[graphql.ID]*starship) func init() { for _, s := range starships { @@ -319,7 +321,7 @@ func (r *Resolver) Search(args *struct{ Text string }) []searchResultResolver { return l } -func (r *Resolver) Character(args *struct{ ID string }) characterResolver { +func (r *Resolver) Character(args *struct{ ID graphql.ID }) characterResolver { if h := humanData[args.ID]; h != nil { return &humanResolver{h: h} } @@ -329,21 +331,21 @@ func (r *Resolver) Character(args *struct{ ID string }) characterResolver { return nil } -func (r *Resolver) Human(args *struct{ ID string }) *humanResolver { +func (r *Resolver) Human(args *struct{ ID graphql.ID }) *humanResolver { if h := humanData[args.ID]; h != nil { return &humanResolver{h: h} } return nil } -func (r *Resolver) Droid(args *struct{ ID string }) *droidResolver { +func (r *Resolver) Droid(args *struct{ ID graphql.ID }) *droidResolver { if d := droidData[args.ID]; d != nil { return &droidResolver{d: d} } return nil } -func (r *Resolver) Starship(args *struct{ ID string }) *starshipResolver { +func (r *Resolver) Starship(args *struct{ ID graphql.ID }) *starshipResolver { if s := starshipData[args.ID]; s != nil { return &starshipResolver{s: s} } @@ -364,11 +366,11 @@ func (r *Resolver) CreateReview(args *struct { type friendsConenctionArgs struct { First *int32 - After *string + After *graphql.ID } type characterResolver interface { - ID() string + ID() graphql.ID Name() string Friends() *[]characterResolver FriendsConnection(*friendsConenctionArgs) (*friendsConnectionResolver, error) @@ -396,7 +398,7 @@ type humanResolver struct { h *human } -func (r *humanResolver) ID() string { +func (r *humanResolver) ID() graphql.ID { return r.h.ID } @@ -445,7 +447,7 @@ type droidResolver struct { d *droid } -func (r *droidResolver) ID() string { +func (r *droidResolver) ID() graphql.ID { return r.d.ID } @@ -481,7 +483,7 @@ type starshipResolver struct { s *starship } -func (r *starshipResolver) ID() string { +func (r *starshipResolver) ID() graphql.ID { return r.s.ID } @@ -514,7 +516,7 @@ func convertLength(meters float64, unit string) float64 { } } -func resolveCharacters(ids []string) *[]characterResolver { +func resolveCharacters(ids []graphql.ID) *[]characterResolver { var characters []characterResolver for _, id := range ids { if c := resolveCharacter(id); c != nil { @@ -524,7 +526,7 @@ func resolveCharacters(ids []string) *[]characterResolver { return &characters } -func resolveCharacter(id string) characterResolver { +func resolveCharacter(id graphql.ID) characterResolver { if h, ok := humanData[id]; ok { return &humanResolver{h: h} } @@ -547,15 +549,15 @@ func (r *reviewResolver) Commentary() *string { } type friendsConnectionResolver struct { - ids []string + ids []graphql.ID from int to int } -func newFriendsConnectionResolver(ids []string, args *friendsConenctionArgs) (*friendsConnectionResolver, error) { +func newFriendsConnectionResolver(ids []graphql.ID, args *friendsConenctionArgs) (*friendsConnectionResolver, error) { from := 0 if args.After != nil { - b, err := base64.StdEncoding.DecodeString(*args.After) + b, err := base64.StdEncoding.DecodeString(string(*args.After)) if err != nil { return nil, err } @@ -608,16 +610,16 @@ func (r *friendsConnectionResolver) PageInfo() *pageInfoResolver { } } -func encodeCursor(i int) string { - return base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("cursor%d", i+1))) +func encodeCursor(i int) graphql.ID { + return graphql.ID(base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("cursor%d", i+1)))) } type friendsEdgeResolver struct { - cursor string - id string + cursor graphql.ID + id graphql.ID } -func (r *friendsEdgeResolver) Cursor() string { +func (r *friendsEdgeResolver) Cursor() graphql.ID { return r.cursor } @@ -626,16 +628,16 @@ func (r *friendsEdgeResolver) Node() characterResolver { } type pageInfoResolver struct { - startCursor string - endCursor string + startCursor graphql.ID + endCursor graphql.ID hasNextPage bool } -func (r *pageInfoResolver) StartCursor() *string { +func (r *pageInfoResolver) StartCursor() *graphql.ID { return &r.startCursor } -func (r *pageInfoResolver) EndCursor() *string { +func (r *pageInfoResolver) EndCursor() *graphql.ID { return &r.endCursor } diff --git a/graphql.go b/graphql.go index 567c7a5eb80..1740440200e 100644 --- a/graphql.go +++ b/graphql.go @@ -3,6 +3,7 @@ package graphql import ( "context" "encoding/json" + "fmt" opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" @@ -15,6 +16,8 @@ import ( "github.com/neelance/graphql-go/internal/schema" ) +type ID string + func ParseSchema(schemaString string, resolver interface{}) (*Schema, error) { b := New() if err := b.Parse(schemaString); err != nil { @@ -30,6 +33,16 @@ type SchemaBuilder struct { func New() *SchemaBuilder { s := schema.New() exec.AddBuiltinScalars(s) + exec.AddCustomScalar(s, "ID", reflect.TypeOf(ID("")), func(input interface{}) (interface{}, error) { + switch input := input.(type) { + case ID: + return input, nil + case string: + return ID(input), nil + default: + return nil, fmt.Errorf("wrong type") + } + }) return &SchemaBuilder{ schema: s, } diff --git a/graphql_test.go b/graphql_test.go index 77df34a851f..2c1523bd827 100644 --- a/graphql_test.go +++ b/graphql_test.go @@ -1,4 +1,4 @@ -package graphql +package graphql_test import ( "bytes" @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/neelance/graphql-go" "github.com/neelance/graphql-go/example/starwars" ) @@ -43,7 +44,7 @@ func (r *timeResolver) AddHour(args *struct{ Time time.Time }) time.Time { var tests = []struct { name string - setup func(b *SchemaBuilder) + setup func(b *graphql.SchemaBuilder) schema string variables map[string]interface{} resolver interface{} @@ -1081,8 +1082,8 @@ var tests = []struct { { name: "Time", - setup: func(b *SchemaBuilder) { - b.AddCustomScalar("Time", Time) + setup: func(b *graphql.SchemaBuilder) { + b.AddCustomScalar("Time", graphql.Time) }, schema: ` schema { @@ -1115,7 +1116,7 @@ var tests = []struct { func TestAll(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - b := New() + b := graphql.New() if test.setup != nil { test.setup(b) } diff --git a/internal/common/values.go b/internal/common/values.go index a6a1c157f01..aed1a8fce5e 100644 --- a/internal/common/values.go +++ b/internal/common/values.go @@ -30,7 +30,7 @@ func ParseInputValue(l *lexer.Lexer) *InputValue { } type Value interface { - Eval(vars map[string]interface{}) interface{} + isValue() } type Variable struct { @@ -41,13 +41,8 @@ type Literal struct { Value interface{} } -func (v *Variable) Eval(vars map[string]interface{}) interface{} { - return vars[v.Name] -} - -func (l *Literal) Eval(vars map[string]interface{}) interface{} { - return l.Value -} +func (*Variable) isValue() {} +func (*Literal) isValue() {} func ParseValue(l *lexer.Lexer, constOnly bool) Value { if !constOnly && l.Peek() == '$' { diff --git a/internal/exec/exec.go b/internal/exec/exec.go index e9dfd4f0a84..272c253b177 100644 --- a/internal/exec/exec.go +++ b/internal/exec/exec.go @@ -211,13 +211,13 @@ func makeFieldExec(s *schema.Schema, f *schema.Field, m reflect.Method, methodIn in = in[1:] } - var argsExec *inputObjectExec + var argsPacker *structPacker if len(f.Args.Fields) > 0 { if len(in) == 0 { return nil, fmt.Errorf("must have parameter for field arguments") } var err error - argsExec, err = makeInputObjectExec(s, &f.Args, in[0]) + argsPacker, err = makeStructPacker(s, &f.Args, in[0]) if err != nil { return nil, err } @@ -243,7 +243,7 @@ func makeFieldExec(s *schema.Schema, f *schema.Field, m reflect.Method, methodIn field: f, methodIndex: methodIndex, hasContext: hasContext, - argsExec: argsExec, + argsPacker: argsPacker, hasError: hasError, } if err := makeExec(&fe.valueExec, s, f.Type, m.Type.Out(0), typeRefMap); err != nil { @@ -252,16 +252,16 @@ func makeFieldExec(s *schema.Schema, f *schema.Field, m reflect.Method, methodIn return fe, nil } -func makeInputObjectExec(s *schema.Schema, obj *common.InputMap, typ reflect.Type) (*inputObjectExec, error) { +func makeStructPacker(s *schema.Schema, obj *common.InputMap, typ reflect.Type) (*structPacker, error) { if typ.Kind() != reflect.Ptr || typ.Elem().Kind() != reflect.Struct { return nil, fmt.Errorf("expected pointer to struct, got %s", typ) } structType := typ.Elem() - var fields []*inputFieldExec + var fields []*structPackerField defaultStruct := reflect.New(structType).Elem() for _, f := range obj.Fields { - fe := &inputFieldExec{ + fe := &structPackerField{ name: f.Name, } @@ -290,7 +290,7 @@ func makeInputObjectExec(s *schema.Schema, obj *common.InputMap, typ reflect.Typ if err := expectType(sf.Type, want); err != nil { return nil, err } - fe.exec = &scalarInputExec{ + fe.fieldPacker = &valuePacker{ nonNull: nonNull, } case *schema.Enum: @@ -301,31 +301,28 @@ func makeInputObjectExec(s *schema.Schema, obj *common.InputMap, typ reflect.Typ if err := expectType(sf.Type, want); err != nil { return nil, err } - fe.exec = &scalarInputExec{ + fe.fieldPacker = &valuePacker{ nonNull: nonNull, } case *schema.InputObject: - e, err := makeInputObjectExec(s, &ft.InputMap, sf.Type) + e, err := makeStructPacker(s, &ft.InputMap, sf.Type) if err != nil { return nil, err } - fe.exec = e + fe.fieldPacker = e default: panic("TODO") } if f.Default != nil { - defaultValue, err := coerceValue(f.Type, f.Default.Eval(nil)) - if err != nil { - return nil, err - } - defaultStruct.FieldByIndex(fe.fieldIndex).Set(fe.exec.eval(defaultValue)) + defaultValue := fe.fieldPacker.pack(evalValue(f.Type, f.Default, nil)) + defaultStruct.FieldByIndex(fe.fieldIndex).Set(defaultValue) } fields = append(fields, fe) } - return &inputObjectExec{ + return &structPacker{ structType: structType, defaultStruct: defaultStruct, fields: fields, @@ -391,7 +388,7 @@ func ExecuteRequest(ctx context.Context, e *Exec, document *query.Document, oper return nil, []*errors.QueryError{errors.Errorf("%s", err)} } - coercedVariables, err := coerceInputObject(&op.Vars, variables) + coercedVariables, err := coerceMap(&op.Vars, variables) if err != nil { return nil, []*errors.QueryError{errors.Errorf("%s", err)} } @@ -442,37 +439,6 @@ func getOperation(document *query.Document, operationName string) (*query.Operat return op, nil } -func coerceInputObject(io *common.InputMap, variables map[string]interface{}) (map[string]interface{}, error) { - coerced := make(map[string]interface{}) - for _, iv := range io.Fields { - value, ok := variables[iv.Name] - if !ok { - if iv.Default == nil { - return nil, errors.Errorf("missing %q", iv.Name) - } - coerced[iv.Name] = iv.Default.Eval(nil) - continue - } - c, err := coerceValue(iv.Type, value) - if err != nil { - return nil, err - } - coerced[iv.Name] = c - } - return coerced, nil -} - -func coerceValue(typ common.Type, value interface{}) (interface{}, error) { - t, _ := unwrapNonNull(typ) - switch t := t.(type) { - case *scalar: - return t.coerceInput(value) - case *schema.InputObject: - return coerceInputObject(&t.InputMap, value.(map[string]interface{})) - } - return value, nil -} - type iExec interface { exec(ctx context.Context, r *request, selSet *query.SelectionSet, resolver reflect.Value, serially bool) interface{} } @@ -573,7 +539,7 @@ func (e *objectExec) execSelectionSet(ctx context.Context, r *request, selSet *q addResult(f.Alias, introspectSchema(ctx, r, f.SelSet)) case "__type": - addResult(f.Alias, introspectType(ctx, r, f.Arguments["name"].Eval(r.vars).(string), f.SelSet)) + addResult(f.Alias, introspectType(ctx, r, evalValue(stringScalar, f.Arguments["name"], r.vars).(string), f.SelSet)) default: fe, ok := e.fields[f.Name] @@ -632,13 +598,13 @@ type fieldExec struct { field *schema.Field methodIndex int hasContext bool - argsExec *inputObjectExec + argsPacker *structPacker hasError bool valueExec iExec } func (e *fieldExec) execField(ctx context.Context, r *request, f *query.Field, resolver reflect.Value, addResult addResultFn) { - trivial := !e.hasContext && e.argsExec == nil && !e.hasError + trivial := !e.hasContext && e.argsPacker == nil && !e.hasError var span opentracing.Span if !trivial { @@ -652,14 +618,14 @@ func (e *fieldExec) execField(ctx context.Context, r *request, f *query.Field, r in = append(in, reflect.ValueOf(ctx)) } - if e.argsExec != nil { + if e.argsPacker != nil { values := make(map[string]interface{}) for name, arg := range f.Arguments { - v := arg.Eval(r.vars) + v := evalValue(e.field.Args.Fields[name].Type, arg, r.vars) values[name] = v span.SetTag(name, v) } - in = append(in, e.argsExec.eval(values)) + in = append(in, e.argsPacker.pack(values)) } m := resolver.Method(e.methodIndex) @@ -682,39 +648,90 @@ type typeAssertExec struct { typeExec iExec } -type inputObjectExec struct { - structType reflect.Type - defaultStruct reflect.Value - fields []*inputFieldExec +func evalValue(t common.Type, v common.Value, vars map[string]interface{}) interface{} { + switch v := v.(type) { + case *common.Variable: + return vars[v.Name] + case *common.Literal: + coerced, err := coerceValue(t, v.Value) + if err != nil { + panic(err) // TODO proper error handling + } + return coerced + default: + panic("unreachable") + } } -type inputExec interface { - eval(value interface{}) reflect.Value +func coerceMap(io *common.InputMap, m map[string]interface{}) (map[string]interface{}, error) { + coerced := make(map[string]interface{}) + for _, iv := range io.Fields { + value, ok := m[iv.Name] + if !ok { + if iv.Default == nil { + return nil, errors.Errorf("missing %q", iv.Name) + } + coerced[iv.Name] = evalValue(iv.Type, iv.Default, nil) + continue + } + c, err := coerceValue(iv.Type, value) + if err != nil { + return nil, err + } + coerced[iv.Name] = c + } + return coerced, nil +} + +func coerceValue(typ common.Type, value interface{}) (interface{}, error) { + t, _ := unwrapNonNull(typ) + switch t := t.(type) { + case *scalar: + v, err := t.coerceInput(value) + if v == nil { + return nil, fmt.Errorf("could not convert %#v (%T) to %s: %s", value, value, t.name, err) + } + return v, nil + case *schema.InputObject: + return coerceMap(&t.InputMap, value.(map[string]interface{})) + } + return value, nil +} + +type packer interface { + pack(value interface{}) reflect.Value +} + +type structPacker struct { + structType reflect.Type + defaultStruct reflect.Value + fields []*structPackerField } -type inputFieldExec struct { - name string - fieldIndex []int - exec inputExec +type structPackerField struct { + name string + fieldIndex []int + fieldPacker packer } -func (e *inputObjectExec) eval(value interface{}) reflect.Value { +func (e *structPacker) pack(value interface{}) reflect.Value { values := value.(map[string]interface{}) v := reflect.New(e.structType) v.Elem().Set(e.defaultStruct) for _, f := range e.fields { if value, ok := values[f.name]; ok { - v.Elem().FieldByIndex(f.fieldIndex).Set(f.exec.eval(value)) + fv := f.fieldPacker.pack(value) + v.Elem().FieldByIndex(f.fieldIndex).Set(fv) } } return v } -type scalarInputExec struct { +type valuePacker struct { nonNull bool } -func (e *scalarInputExec) eval(value interface{}) reflect.Value { +func (e *valuePacker) pack(value interface{}) reflect.Value { v := reflect.ValueOf(value) if !e.nonNull { p := reflect.New(v.Type()) @@ -726,12 +743,12 @@ func (e *scalarInputExec) eval(value interface{}) reflect.Value { func skipByDirective(r *request, d map[string]*query.Directive) bool { if skip, ok := d["skip"]; ok { - if skip.Arguments["if"].Eval(r.vars).(bool) { + if evalValue(booleanScalar, skip.Arguments["if"], r.vars).(bool) { return true } } if include, ok := d["include"]; ok { - if !include.Arguments["if"].Eval(r.vars).(bool) { + if !evalValue(booleanScalar, include.Arguments["if"], r.vars).(bool) { return true } } diff --git a/internal/exec/scalar.go b/internal/exec/scalar.go index 89d6cf45999..241adb74e65 100644 --- a/internal/exec/scalar.go +++ b/internal/exec/scalar.go @@ -17,48 +17,47 @@ type scalar struct { func (*scalar) Kind() string { return "SCALAR" } func (t *scalar) TypeName() string { return t.name } -var builtinScalars = []*scalar{ - &scalar{ - name: "Int", - reflectType: reflect.TypeOf(int32(0)), - coerceInput: func(input interface{}) (interface{}, error) { - i := input.(int) - if i < math.MinInt32 || i > math.MaxInt32 { - return nil, fmt.Errorf("not a 32-bit integer: %d", i) - } - return int32(i), nil - }, - }, - &scalar{ - name: "Float", - reflectType: reflect.TypeOf(float64(0)), - coerceInput: func(input interface{}) (interface{}, error) { - return input, nil // TODO - }, +var intScalar = &scalar{ + name: "Int", + reflectType: reflect.TypeOf(int32(0)), + coerceInput: func(input interface{}) (interface{}, error) { + i := input.(int) + if i < math.MinInt32 || i > math.MaxInt32 { + return nil, fmt.Errorf("not a 32-bit integer: %d", i) + } + return int32(i), nil }, - &scalar{ - name: "String", - reflectType: reflect.TypeOf(""), - coerceInput: func(input interface{}) (interface{}, error) { - return input, nil // TODO - }, +} +var floatScalar = &scalar{ + name: "Float", + reflectType: reflect.TypeOf(float64(0)), + coerceInput: func(input interface{}) (interface{}, error) { + return input, nil // TODO }, - &scalar{ - name: "Boolean", - reflectType: reflect.TypeOf(true), - coerceInput: func(input interface{}) (interface{}, error) { - return input, nil // TODO - }, +} +var stringScalar = &scalar{ + name: "String", + reflectType: reflect.TypeOf(""), + coerceInput: func(input interface{}) (interface{}, error) { + return input, nil // TODO }, - &scalar{ - name: "ID", - reflectType: reflect.TypeOf(""), - coerceInput: func(input interface{}) (interface{}, error) { - return input, nil // TODO - }, +} +var booleanScalar = &scalar{ + name: "Boolean", + reflectType: reflect.TypeOf(true), + coerceInput: func(input interface{}) (interface{}, error) { + return input, nil // TODO }, } +var builtinScalars = []*scalar{ + intScalar, + floatScalar, + stringScalar, + booleanScalar, + // ID is defined in package "graphql" +} + func AddBuiltinScalars(s *schema.Schema) { for _, scalar := range builtinScalars { s.Types[scalar.name] = scalar diff --git a/internal/lexer/lexer.go b/internal/lexer/lexer.go index de543bec468..76c02e3d031 100644 --- a/internal/lexer/lexer.go +++ b/internal/lexer/lexer.go @@ -2,7 +2,6 @@ package lexer import ( "fmt" - "math" "strconv" "text/scanner" @@ -72,14 +71,11 @@ func (l *Lexer) ConsumeKeyword(keyword string) { l.Consume() } -func (l *Lexer) ConsumeInt() int32 { +func (l *Lexer) ConsumeInt() int { text := l.sc.TokenText() l.ConsumeToken(scanner.Int) value, _ := strconv.Atoi(text) - if value < math.MinInt32 || value > math.MaxInt32 { - l.SyntaxError(fmt.Sprintf("not a 32-bit integer: %d", value)) - } - return int32(value) + return value } func (l *Lexer) ConsumeFloat() float64 { diff --git a/time.go b/time.go index 7055faaf119..7668851f909 100644 --- a/time.go +++ b/time.go @@ -18,7 +18,7 @@ var Time = &ScalarConfig{ case int: return time.Unix(int64(input), 0), nil default: - return nil, fmt.Errorf("could not convert %v (%t) to time", input, input) + return nil, fmt.Errorf("wrong type") } }, }