Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow creating aliases for builtin types, using typename. #133

Merged
merged 3 commits into from
Oct 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ When releasing a new version:

- genqlient now generates getter methods for all fields, even those which do not implement a genqlient-generated interface; this can be useful for callers who wish to define their own interface and have several unrelated genqlient types which have the same fields implement it.
- genqlient config now accepts either a single or multiple schema files for the `schema` field.
- The `typename` option can now be used on basic types (string, int, etc) as well as structs; this can be useful to have genqlient define new types like `type Language string` and use that type for specified fields.

### Bug fixes:

- In certain very rare cases involving duplicate fields in fragment spreads, genqlient would generate code that failed to compile due to duplicate methods not getting promoted; genqlient now generates correct types. (See #126 for a more complete description.)
Expand Down
12 changes: 7 additions & 5 deletions docs/genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,13 @@ client_getter: "github.com/you/yourpkg.GetClient"
# A map from GraphQL type name to Go fully-qualified type name to override
# the Go type genqlient will use for this GraphQL type.
#
# This is primarily used for custom scalars, or to map builtin scalars to
# a nonstandard type. By default, builtin scalars are mapped to the
# obvious Go types (String and ID to string, Int to int, Float to float64,
# and Boolean to bool), but this setting will extend or override those
# mappings.
# This is primarily used for custom scalars, or to map builtin scalars
# to a nonstandard type that is defined elsewhere. By default,
# builtin scalars are mapped to the obvious Go types (String and ID to
# string, Int to int, Float to float64, and Boolean to bool), but this
# setting will extend or override those mappings. (See also
# @genqlient(typename: ...), which can be used to map builtin scalars
# to a nonstandard type that genqlient defines for you.)
#
# genqlient does not validate these types in any way; they must define
# whatever logic is needed (MarshalJSON/UnmarshalJSON or JSON tags) to
Expand Down
24 changes: 24 additions & 0 deletions docs/genqlient_directive.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ directive genqlient(
# you can do that, as long as its UnmarshalJSON method can accept a list
# of datetimes.)
#
# Note that the type you bind to must be defined elsewhere in your code.
# If you want genqlient to create the type definition, use "typename"
# instead.
#
# See bindings in genqlient.yaml for more details; this is effectively to a
# local version of that global setting and should be used with similar care.
# If set to "-", overrides any such global setting and uses a
Expand All @@ -189,6 +193,26 @@ directive genqlient(
# }
# instead of its usual, more verbose type names.
#
# You may also use "typename" on basic types, and Go will create a
# type definition for that basic type. For instance:
# query MyQuery {
# user {
# # @genqlient(typename: "NameType")
# name
# }
# }
# will cause gnqlient to generate:
# type Resp struct {
# User User
# }
# type NameType string
# type User struct {
# Name NameType
# }
# (Compare this to @genqlient(bind: "path/to/pkg.NameType"), which does
# something similar but depends on "NameType" being defined in some
# other package, rather than having genqlient define it for you.)
#
# With great power comes great responsibility: when using typename you'll
# need to avoid comments; genqlient will complain if you use the same
# type-name in multiple places unless they request the exact same fields, or
Expand Down
13 changes: 12 additions & 1 deletion generate/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ func (g *generator) convertDefinition(
}, err
}
goBuiltinName, ok := builtinTypes[def.Name]
if ok {
if ok && options.TypeName == "" {
return &goOpaqueType{GoRef: goBuiltinName, GraphQLName: def.Name}, nil
}

Expand Down Expand Up @@ -479,6 +479,17 @@ func (g *generator) convertDefinition(
return g.addType(goType, goType.GoName, pos)

case ast.Scalar:
if builtinTypes[def.Name] != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle we should probably support this for types via global or local bind as well, if you have an int and you want to bind it to int32 and also use a named type? The best way to do that is probably to do it in the caller convertType, similar to how we do pointer. (Or you could make it a TODO until anyone actually cares.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went to write this TODO and realized I don't understand exactly the use case you're talking about. Can you give a concrete example? bind works with types you define, so you can already do

    type MyInt int32
    ....
    # @genqlient(bind: "path/to.MyInt")
    myfield

can't you?

Copy link
Collaborator

@benjaminjkraft benjaminjkraft Oct 11, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, bind wins over typename, I was worried typename would win over bind which would be more confusing. So it's still the case that if you have in genqlient.yaml:

bindings:
  MyType:
    type: int32

and now you do

# @genqlient(typename: "MyOtherType")
myField

the typename is ignored. Really we should combine the two, i.e.:

type MyOtherType int32

Of course it's not really clear why you would want that, so we could also explicitly error. But it's honestly probably just as easy to make it do the obvious thing. (Initially I was worried we'd do type MyOtherType int, which would be wrong.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean we should emit type MyOtherType int32? I guess I'm not seeing what "the obvious thing" is.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes that's what I meant! Edited.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'll take a stab when I have a moment, or else add a TODO (also when I have a moment :-) )

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, to be clear, that's the obvious thing: define a new alias (from typename) to the bound type (from bind).

// In this case, the user asked for a custom Go type-name
// for a built-in type, e.g. `type MyString string`.
goType := &goTypenameForBuiltinType{
GoTypeName: name,
GoBuiltinName: builtinTypes[def.Name],
GraphQLName: def.Name,
}
return g.addType(goType, goType.GoTypeName, pos)
}

// (If you had an entry in bindings, we would have returned it above.)
return nil, errorf(
pos, `unknown scalar %v: please add it to "bindings" in genqlient.yaml`, def.Name)
Expand Down
9 changes: 7 additions & 2 deletions generate/testdata/errors/ConflictingTypeNames.graphql
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
query ConflictingTypeNames {
# @genqlient(typename: "T")
f { g }
f {
g
}
# @genqlient(typename: "T")
otherF: f { g h }
otherF: f {
g
h
}
}
16 changes: 13 additions & 3 deletions generate/testdata/queries/TypeNames.graphql
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# @genqlient(typename: "Resp")
query TypeNames {
# @genqlient(typename: "User")
user { id name }
user {
id
name
}
# @genqlient(typename: "Item")
randomItem { id name }
randomItem {
id
# @genqlient(typename: "NameType")
name
}
# (ok to reuse the name as long as they match)
# @genqlient(typename: "User")
users { id name }
users {
id
name
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 49 additions & 30 deletions generate/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ type (
GraphQLName string
Marshaler, Unmarshaler string
}
// goTypenameForBuiltinType represents a builtin type that was
// given a different name due to a `typename` directive. We
// create a type like `type MyString string` for it.
goTypenameForBuiltinType struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might also name this goTypeForBuiltinType.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I would have called it goNamedType, but any of these work!)

GoTypeName string
GoBuiltinName string
GraphQLName string
}
// goSliceType represents the Go type []Elem, used to represent GraphQL
// list types.
goSliceType struct{ Elem goType }
Expand All @@ -75,21 +83,29 @@ type (
)

// Opaque types are defined by the user; pointers and slices need no definition
func (typ *goOpaqueType) WriteDefinition(io.Writer, *generator) error { return nil }
func (typ *goOpaqueType) WriteDefinition(io.Writer, *generator) error { return nil }

func (typ *goTypenameForBuiltinType) WriteDefinition(w io.Writer, g *generator) error {
fmt.Fprintf(w, "type %s %s", typ.GoTypeName, typ.GoBuiltinName)
return nil
}
func (typ *goSliceType) WriteDefinition(io.Writer, *generator) error { return nil }
func (typ *goPointerType) WriteDefinition(io.Writer, *generator) error { return nil }

func (typ *goOpaqueType) Reference() string { return typ.GoRef }
func (typ *goSliceType) Reference() string { return "[]" + typ.Elem.Reference() }
func (typ *goPointerType) Reference() string { return "*" + typ.Elem.Reference() }
func (typ *goOpaqueType) Reference() string { return typ.GoRef }
func (typ *goTypenameForBuiltinType) Reference() string { return typ.GoTypeName }
func (typ *goSliceType) Reference() string { return "[]" + typ.Elem.Reference() }
func (typ *goPointerType) Reference() string { return "*" + typ.Elem.Reference() }

func (typ *goOpaqueType) SelectionSet() ast.SelectionSet { return nil }
func (typ *goSliceType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() }
func (typ *goPointerType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() }
func (typ *goOpaqueType) SelectionSet() ast.SelectionSet { return nil }
func (typ *goTypenameForBuiltinType) SelectionSet() ast.SelectionSet { return nil }
func (typ *goSliceType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() }
func (typ *goPointerType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() }

func (typ *goOpaqueType) GraphQLTypeName() string { return typ.GraphQLName }
func (typ *goSliceType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() }
func (typ *goPointerType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() }
func (typ *goOpaqueType) GraphQLTypeName() string { return typ.GraphQLName }
func (typ *goTypenameForBuiltinType) GraphQLTypeName() string { return typ.GraphQLName }
func (typ *goSliceType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() }
func (typ *goPointerType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() }

// goEnumType represents a Go named-string type used to represent a GraphQL
// enum. In this case, we generate both the type (`type T string`) and also a
Expand Down Expand Up @@ -502,26 +518,29 @@ func (typ *goInterfaceType) Reference() string { return typ.GoName
func (typ *goInterfaceType) SelectionSet() ast.SelectionSet { return typ.Selection }
func (typ *goInterfaceType) GraphQLTypeName() string { return typ.GraphQLName }

func (typ *goOpaqueType) Unwrap() goType { return typ }
func (typ *goSliceType) Unwrap() goType { return typ.Elem.Unwrap() }
func (typ *goPointerType) Unwrap() goType { return typ.Elem.Unwrap() }
func (typ *goEnumType) Unwrap() goType { return typ }
func (typ *goStructType) Unwrap() goType { return typ }
func (typ *goInterfaceType) Unwrap() goType { return typ }

func (typ *goOpaqueType) SliceDepth() int { return 0 }
func (typ *goSliceType) SliceDepth() int { return typ.Elem.SliceDepth() + 1 }
func (typ *goPointerType) SliceDepth() int { return 0 }
func (typ *goEnumType) SliceDepth() int { return 0 }
func (typ *goStructType) SliceDepth() int { return 0 }
func (typ *goInterfaceType) SliceDepth() int { return 0 }

func (typ *goOpaqueType) IsPointer() bool { return false }
func (typ *goSliceType) IsPointer() bool { return typ.Elem.IsPointer() }
func (typ *goPointerType) IsPointer() bool { return true }
func (typ *goEnumType) IsPointer() bool { return false }
func (typ *goStructType) IsPointer() bool { return false }
func (typ *goInterfaceType) IsPointer() bool { return false }
func (typ *goOpaqueType) Unwrap() goType { return typ }
func (typ *goTypenameForBuiltinType) Unwrap() goType { return typ }
func (typ *goSliceType) Unwrap() goType { return typ.Elem.Unwrap() }
func (typ *goPointerType) Unwrap() goType { return typ.Elem.Unwrap() }
func (typ *goEnumType) Unwrap() goType { return typ }
func (typ *goStructType) Unwrap() goType { return typ }
func (typ *goInterfaceType) Unwrap() goType { return typ }

func (typ *goOpaqueType) SliceDepth() int { return 0 }
func (typ *goTypenameForBuiltinType) SliceDepth() int { return 0 }
func (typ *goSliceType) SliceDepth() int { return typ.Elem.SliceDepth() + 1 }
func (typ *goPointerType) SliceDepth() int { return 0 }
func (typ *goEnumType) SliceDepth() int { return 0 }
func (typ *goStructType) SliceDepth() int { return 0 }
func (typ *goInterfaceType) SliceDepth() int { return 0 }

func (typ *goOpaqueType) IsPointer() bool { return false }
func (typ *goTypenameForBuiltinType) IsPointer() bool { return false }
func (typ *goSliceType) IsPointer() bool { return typ.Elem.IsPointer() }
func (typ *goPointerType) IsPointer() bool { return true }
func (typ *goEnumType) IsPointer() bool { return false }
func (typ *goStructType) IsPointer() bool { return false }
func (typ *goInterfaceType) IsPointer() bool { return false }

func writeDescription(w io.Writer, desc string) {
if desc != "" {
Expand Down