diff --git a/codegen/models_build.go b/codegen/models_build.go index ebe83e384ca..591210c3751 100644 --- a/codegen/models_build.go +++ b/codegen/models_build.go @@ -68,12 +68,7 @@ func (cfg *Config) obj2Model(obj *Object) Model { if field.GoVarName != "" { mf.GoVarName = field.GoVarName } else { - mf.GoVarName = ucFirst(field.GQLName) - if mf.IsScalar { - if mf.GoVarName == "Id" { - mf.GoVarName = "ID" - } - } + mf.GoVarName = field.GoNameExported() } model.Fields = append(model.Fields, mf) diff --git a/codegen/object.go b/codegen/object.go index d38d5e06d10..1afcd73ff54 100644 --- a/codegen/object.go +++ b/codegen/object.go @@ -69,37 +69,57 @@ func (f *Field) IsConcurrent() bool { return f.IsResolver() && !f.Object.DisableConcurrency } +func (f *Field) GoNameExported() string { + return lintName(ucFirst(f.GQLName)) +} + +func (f *Field) GoNameUnexported() string { + return lintName(f.GQLName) +} + func (f *Field) ShortInvocation() string { if !f.IsResolver() { return "" } - shortName := strings.ToUpper(f.GQLName[:1]) + f.GQLName[1:] - return fmt.Sprintf("%s().%s(%s)", f.Object.GQLType, shortName, f.CallArgs()) + return fmt.Sprintf("%s().%s(%s)", f.Object.GQLType, f.GoNameExported(), f.CallArgs()) } func (f *Field) ResolverType() string { if !f.IsResolver() { return "" } - shortName := strings.ToUpper(f.GQLName[:1]) + f.GQLName[1:] - return fmt.Sprintf("%s().%s(%s)", f.Object.GQLType, shortName, f.CallArgs()) + return fmt.Sprintf("%s().%s(%s)", f.Object.GQLType, f.GoNameExported(), f.CallArgs()) } func (f *Field) ShortResolverDeclaration() string { if !f.IsResolver() { return "" } - decl := strings.TrimPrefix(f.ResolverDeclaration(), f.Object.GQLType+"_") - return strings.ToUpper(decl[:1]) + decl[1:] + res := fmt.Sprintf("%s(ctx context.Context", f.GoNameExported()) + + if !f.Object.Root { + res += fmt.Sprintf(", obj *%s", f.Object.FullName()) + } + for _, arg := range f.Args { + res += fmt.Sprintf(", %s %s", arg.GoVarName, arg.Signature()) + } + + result := f.Signature() + if f.Object.Stream { + result = "<-chan " + result + } + + res += fmt.Sprintf(") (%s, error)", result) + return res } func (f *Field) ResolverDeclaration() string { if !f.IsResolver() { return "" } - res := fmt.Sprintf("%s_%s(ctx context.Context", f.Object.GQLType, f.GQLName) + res := fmt.Sprintf("%s_%s(ctx context.Context", f.Object.GQLType, f.GoNameUnexported()) if !f.Object.Root { res += fmt.Sprintf(", obj *%s", f.Object.FullName()) @@ -209,3 +229,117 @@ func ucFirst(s string) string { r[0] = unicode.ToUpper(r[0]) return string(r) } + +// copy from https://github.com/golang/lint/blob/06c8688daad7faa9da5a0c2f163a3d14aac986ca/lint.go#L679 + +// lintName returns a different name if it should be different. +func lintName(name string) (should string) { + // Fast path for simple cases: "_" and all lowercase. + if name == "_" { + return name + } + allLower := true + for _, r := range name { + if !unicode.IsLower(r) { + allLower = false + break + } + } + if allLower { + return name + } + + // Split camelCase at any lower->upper transition, and split on underscores. + // Check each word for common initialisms. + runes := []rune(name) + w, i := 0, 0 // index of start of word, scan + for i+1 <= len(runes) { + eow := false // whether we hit the end of a word + if i+1 == len(runes) { + eow = true + } else if runes[i+1] == '_' { + // underscore; shift the remainder forward over any run of underscores + eow = true + n := 1 + for i+n+1 < len(runes) && runes[i+n+1] == '_' { + n++ + } + + // Leave at most one underscore if the underscore is between two digits + if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) { + n-- + } + + copy(runes[i+1:], runes[i+n+1:]) + runes = runes[:len(runes)-n] + } else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) { + // lower->non-lower + eow = true + } + i++ + if !eow { + continue + } + + // [w,i) is a word. + word := string(runes[w:i]) + if u := strings.ToUpper(word); commonInitialisms[u] { + // Keep consistent case, which is lowercase only at the start. + if w == 0 && unicode.IsLower(runes[w]) { + u = strings.ToLower(u) + } + // All the common initialisms are ASCII, + // so we can replace the bytes exactly. + copy(runes[w:], []rune(u)) + } else if w > 0 && strings.ToLower(word) == word { + // already all lowercase, and not the first word, so uppercase the first character. + runes[w] = unicode.ToUpper(runes[w]) + } + w = i + } + return string(runes) +} + +// commonInitialisms is a set of common initialisms. +// Only add entries that are highly unlikely to be non-initialisms. +// For instance, "ID" is fine (Freudian code is rare), but "AND" is not. +var commonInitialisms = map[string]bool{ + "ACL": true, + "API": true, + "ASCII": true, + "CPU": true, + "CSS": true, + "DNS": true, + "EOF": true, + "GUID": true, + "HTML": true, + "HTTP": true, + "HTTPS": true, + "ID": true, + "IP": true, + "JSON": true, + "LHS": true, + "QPS": true, + "RAM": true, + "RHS": true, + "RPC": true, + "SLA": true, + "SMTP": true, + "SQL": true, + "SSH": true, + "TCP": true, + "TLS": true, + "TTL": true, + "UDP": true, + "UI": true, + "UID": true, + "UUID": true, + "URI": true, + "URL": true, + "UTF8": true, + "VM": true, + "XML": true, + "XMPP": true, + "XSRF": true, + "XSS": true, +} diff --git a/example/config/.gqlgen.yml b/example/config/.gqlgen.yml index 97f3678b521..b7d800a8f4b 100644 --- a/example/config/.gqlgen.yml +++ b/example/config/.gqlgen.yml @@ -15,6 +15,8 @@ resolver: models: Todo: # Object fields: + id: + resolver: true text: govarname: Description NewTodo: # Input diff --git a/example/config/generated.go b/example/config/generated.go index aa2fcb2fc05..df1340fa238 100644 --- a/example/config/generated.go +++ b/example/config/generated.go @@ -29,6 +29,7 @@ type Config struct { type ResolverRoot interface { Mutation() MutationResolver Query() QueryResolver + Todo() TodoResolver } type DirectiveRoot struct { @@ -39,6 +40,9 @@ type MutationResolver interface { type QueryResolver interface { Todos(ctx context.Context) ([]Todo, error) } +type TodoResolver interface { + ID(ctx context.Context, obj *Todo) (string, error) +} type executableSchema struct { resolvers ResolverRoot @@ -277,6 +281,8 @@ func (ec *executionContext) _Todo(ctx context.Context, sel ast.SelectionSet, obj out.Values[i] = graphql.MarshalString("Todo") case "id": out.Values[i] = ec._Todo_id(ctx, field, obj) + case "databaseId": + out.Values[i] = ec._Todo_databaseId(ctx, field, obj) case "text": out.Values[i] = ec._Todo_text(ctx, field, obj) case "done": @@ -292,6 +298,32 @@ func (ec *executionContext) _Todo(ctx context.Context, sel ast.SelectionSet, obj } func (ec *executionContext) _Todo_id(ctx context.Context, field graphql.CollectedField, obj *Todo) graphql.Marshaler { + ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ + Object: "Todo", + Args: nil, + Field: field, + }) + return graphql.Defer(func() (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + userErr := ec.Recover(ctx, r) + ec.Error(ctx, userErr) + ret = graphql.Null + } + }() + + resTmp := ec.FieldMiddleware(ctx, func(ctx context.Context) (interface{}, error) { + return ec.resolvers.Todo().ID(ctx, obj) + }) + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + return graphql.MarshalID(res) + }) +} + +func (ec *executionContext) _Todo_databaseId(ctx context.Context, field graphql.CollectedField, obj *Todo) graphql.Marshaler { rctx := graphql.GetResolverContext(ctx) rctx.Object = "Todo" rctx.Args = nil @@ -299,13 +331,13 @@ func (ec *executionContext) _Todo_id(ctx context.Context, field graphql.Collecte rctx.PushField(field.Alias) defer rctx.Pop() resTmp := ec.FieldMiddleware(ctx, func(ctx context.Context) (interface{}, error) { - return obj.ID, nil + return obj.DatabaseID, nil }) if resTmp == nil { return graphql.Null } - res := resTmp.(string) - return graphql.MarshalID(res) + res := resTmp.(int) + return graphql.MarshalInt(res) } func (ec *executionContext) _Todo_text(ctx context.Context, field graphql.CollectedField, obj *Todo) graphql.Marshaler { @@ -1330,6 +1362,7 @@ var parsedSchema = gqlparser.MustLoadSchema( type Todo { id: ID! + databaseId: Int! text: String! done: Boolean! user: User! @@ -1351,5 +1384,6 @@ input NewTodo { type Mutation { createTodo(input: NewTodo!): Todo! -}`}, +} +`}, ) diff --git a/example/config/model.go b/example/config/model.go index 73330ec1813..52f41135273 100644 --- a/example/config/model.go +++ b/example/config/model.go @@ -1,6 +1,6 @@ package config type User struct { - ID string + ID string FullName string } diff --git a/example/config/models_gen.go b/example/config/models_gen.go index dc8e7849a7e..f0f7ba68795 100644 --- a/example/config/models_gen.go +++ b/example/config/models_gen.go @@ -8,6 +8,7 @@ type NewTodo struct { } type Todo struct { ID string `json:"id"` + DatabaseID int `json:"databaseId"` Description string `json:"text"` Done bool `json:"done"` User User `json:"user"` diff --git a/example/config/resolver.go b/example/config/resolver.go index ef45581d676..46a5c317947 100644 --- a/example/config/resolver.go +++ b/example/config/resolver.go @@ -11,9 +11,9 @@ func New() Config { c := Config{ Resolvers: &Resolver{ todos: []Todo{ - {ID: "TODO:1", Description: "A todo not to forget", Done: false}, - {ID: "TODO:2", Description: "This is the most important", Done: false}, - {ID: "TODO:3", Description: "Please do this or else", Done: false}, + {DatabaseID: 1, Description: "A todo not to forget", Done: false}, + {DatabaseID: 2, Description: "This is the most important", Done: false}, + {DatabaseID: 3, Description: "Please do this or else", Done: false}, }, nextID: 3, }, @@ -32,6 +32,9 @@ func (r *Resolver) Mutation() MutationResolver { func (r *Resolver) Query() QueryResolver { return &queryResolver{r} } +func (r *Resolver) Todo() TodoResolver { + return &todoResolver{r} +} type mutationResolver struct{ *Resolver } @@ -40,7 +43,7 @@ func (r *mutationResolver) CreateTodo(ctx context.Context, input NewTodo) (Todo, r.nextID++ newTodo := Todo{ - ID: fmt.Sprintf("TODO:%d", newID), + DatabaseID: newID, Description: input.Text, } @@ -54,3 +57,15 @@ type queryResolver struct{ *Resolver } func (r *queryResolver) Todos(ctx context.Context) ([]Todo, error) { return r.todos, nil } + +type todoResolver struct{ *Resolver } + +func (r *todoResolver) ID(ctx context.Context, obj *Todo) (string, error) { + if obj.ID != "" { + return obj.ID, nil + } + + obj.ID = fmt.Sprintf("TODO:%d", obj.DatabaseID) + + return obj.ID, nil +} diff --git a/example/config/schema.graphql b/example/config/schema.graphql index f6e1bac5662..470b77252df 100644 --- a/example/config/schema.graphql +++ b/example/config/schema.graphql @@ -4,6 +4,7 @@ type Todo { id: ID! + databaseId: Int! text: String! done: Boolean! user: User! @@ -25,4 +26,4 @@ input NewTodo { type Mutation { createTodo(input: NewTodo!): Todo! -} \ No newline at end of file +} diff --git a/test/generated.go b/test/generated.go index 36268ffa585..3c3678894a0 100644 --- a/test/generated.go +++ b/test/generated.go @@ -45,7 +45,7 @@ type QueryResolver interface { Path(ctx context.Context) ([]*models.Element, error) Date(ctx context.Context, filter models.DateFilter) (bool, error) Viewer(ctx context.Context) (*models.Viewer, error) - JsonEncoding(ctx context.Context) (string, error) + JSONEncoding(ctx context.Context) (string, error) } type UserResolver interface { Likes(ctx context.Context, obj *remote_api.User) ([]string, error) @@ -360,7 +360,7 @@ func (ec *executionContext) _Query_jsonEncoding(ctx context.Context, field graph }() resTmp := ec.FieldMiddleware(ctx, func(ctx context.Context) (interface{}, error) { - return ec.resolvers.Query().JsonEncoding(ctx) + return ec.resolvers.Query().JSONEncoding(ctx) }) if resTmp == nil { return graphql.Null diff --git a/test/resolvers_test.go b/test/resolvers_test.go index 1de3bab1c3a..12293c6a573 100644 --- a/test/resolvers_test.go +++ b/test/resolvers_test.go @@ -158,7 +158,7 @@ func (r *queryResolver) Viewer(ctx context.Context) (*models.Viewer, error) { }, nil } -func (r *queryResolver) JsonEncoding(ctx context.Context) (string, error) { +func (r *queryResolver) JSONEncoding(ctx context.Context) (string, error) { return "\U000fe4ed", nil }