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

Conversation

csilvers
Copy link
Member

@csilvers csilvers commented Oct 4, 2021

Summary:

This lets you write code like:

query x {
   # @genqlient(typename: "MyString")
   someStringField
}

and genqlient will do

typename MyString string
type x struct {
   someStringField MyString
}

This was not difficult to implement, though it required introducing a
new identifier type. The main difficulty I had was weird test
failures, that it turns out was due to the tests putting a bunch of
fields on the same line, so that the genqlient directive on the
previous line applied to all of them, accidentally. This became a
problem when typename suddenly started being respected for builtin
types! I fixed it by just spreading out the queries a bit.

Fixes #130

Test plan:

make check

@dnerdy
Copy link
Contributor

dnerdy commented Oct 5, 2021

@csilvers did you try using bind to map builtin scalars to nonstandard types? I believe bind is meant to address this case (whereas typename is meant to override the generated name for a type). See

# 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.
and
# If set, this argument or field will use the given Go type instead of a
# genqlient-generated type.
#
# The value should be the fully-qualified type name to use for the field,
# for example:
# time.Time
# map[string]interface{}
# []github.com/you/yourpkg/subpkg.MyType
# Note that the type is the type of the whole field, e.g. if your field in
# GraphQL has type `[DateTime]`, you'd do
# # @genqlient(bind: "[]time.Time")
# (But you're not required to; if you want to map to some type DateList,
# you can do that, as long as its UnmarshalJSON method can accept a list
# of datetimes.)
#
# 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
# genqlient-generated type.
bind: String
.

@csilvers
Copy link
Member Author

csilvers commented Oct 5, 2021

The difference is that bind requires you to define the type yourself, which can be inconvenient. (In this case we'd need a separate package just to define the type name.) typename lets genqlient define it in the genqlient package.

Copy link
Contributor

@dnerdy dnerdy left a comment

Choose a reason for hiding this comment

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

Got it. Okay, this looks reasonable for that case. Could you add an entry to the change log?

### New features:

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

case ast.Scalar:
if builtinTypes[def.Name] != "" {
// In this case, the user asked for a custom Go type-name
// for a built-in type, e.g. `typename MyString string`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// for a built-in type, e.g. `typename MyString string`.
// for a built-in type, e.g. `type MyString string`.

// In this case, the user asked for a custom Go type-name
// for a built-in type, e.g. `typename MyString string`.
goType := &goTypenameForBuiltinType{
GoTypename: name,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think GoTypeName would be more consistent with the rest of the code.

// 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!)

@csilvers
Copy link
Member Author

csilvers commented Oct 5, 2021

I'm still not sure this is a good idea. It seems less than ideal to have two ways to override basic types, one using bind and one using typename. In general Ben is not a big fan of bind, because it turns off all the type-checking that genqlient normally does, so I am leaning toward this being a better approach. I'll change the documentation to be clearer that typename and bind can both be used to replace built-in types, but typename is preferred.

This lets you write code like:
```
query x {
   # @genqlient(typename: "MyString")
   someStringField
}
```
and genqlient will do
```
typename MyString string
type x struct {
   someStringField MyString
}
```

This was not difficult to implement, though it required introducing a
new identifier type.  The main difficulty I had was weird test
failures, that it turns out was due to the tests putting a bunch of
fields on the same line, so that the genqlient directive on the
previous line applied to all of them, accidentally.  This became a
problem when `typename` suddenly started being respected for builtin
types!  I fixed it by just spreading out the queries a bit.

Fixes #130
@csilvers csilvers force-pushed the typename-basic-types branch from 66448b5 to c2a8f15 Compare October 5, 2021 04:29
@csilvers
Copy link
Member Author

csilvers commented Oct 5, 2021

Documentation added -- if it looks ok can you approve?

Copy link
Member

@StevenACoffman StevenACoffman left a comment

Choose a reason for hiding this comment

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

LGTM now!

@csilvers csilvers merged commit a52e556 into main Oct 5, 2021
@csilvers csilvers deleted the typename-basic-types branch October 5, 2021 15:49
Copy link
Collaborator

@benjaminjkraft benjaminjkraft left a comment

Choose a reason for hiding this comment

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

Neat idea! I worry a little bit this will make people want to add methods to the type, although it's not really genqlient that's stopping that (but rather the -- probably wise -- Khan convention that generated code is in its own package).

@@ -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).

// 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
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!)

benjaminjkraft added a commit that referenced this pull request Feb 10, 2022
In #133, Craig added support for a new use of typename, where it applies
to a scalar and means that genqlient should generate a named type, e.g.
`# @genqlient(typename: "MyString")` on a node of type string will
generate and use `type MyString string`.  But this gets a bit confusing
if you mix it with `bind`; should
`typename: "MyString", bind: "int32"` generate `type MyString int32`, or
should one override the other, or what?  Of course in practice you're
not likely to write that all in one place, but you could via a global
binding, or a `for` directive, and in that case probably it was a
mistake.  In #138, we looked at making them work together correctly, but
it added complexity and got even more confusing.

So instead, here, we just ban it; we can always add it back if it proves
useful.  (Or, you can make the `typename` win over a global binding by
locally unbinding it via `bind: "-"`.)  This required changes in
surprisingly many places; I already knew the directive-validation code
was due for a refactor but that will happen some other day.  The tests
show that it works, in any case.

Interestingly, this problem actually could have arisen for a struct
binding already, before #133.  But all the same reasons it's confusing
seem to apply, so I just banned it there too.  This is technically a
breaking change although I doubt anyone will hit it.

Test plan: make check
benjaminjkraft added a commit that referenced this pull request Feb 10, 2022
In #133, Craig added support for a new use of typename, where it applies
to a scalar and means that genqlient should generate a named type, e.g.
`# @genqlient(typename: "MyString")` on a node of type string will
generate and use `type MyString string`.  But this gets a bit confusing
if you mix it with `bind`; should
`typename: "MyString", bind: "int32"` generate `type MyString int32`, or
should one override the other, or what?  Of course in practice you're
not likely to write that all in one place, but you could via a global
binding, or a `for` directive, and in that case probably it was a
mistake.  In #138, we looked at making them work together correctly, but
it added complexity and got even more confusing.

So instead, here, we just ban it; we can always add it back if it proves
useful.  (Or, you can make the `typename` win over a global binding by
locally unbinding it via `bind: "-"`.)  This required changes in
surprisingly many places; I already knew the directive-validation code
was due for a refactor but that will happen some other day.  The tests
show that it works, in any case.

Interestingly, this problem actually could have arisen for a struct
binding already, before #133.  But all the same reasons it's confusing
seem to apply, so I just banned it there too.  This is technically a
breaking change although I doubt anyone will hit it.

Test plan: make check
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

typename doesn't work on basic types.
4 participants