Files
genqlient/generate/names.go
T
Ben KraftandGitHub 6fdb170b99 Fix type-naming in the presence of interfaces, and refactor it a lot (#71)
## Summary:
When adding support for interfaces, I did not do the type-names as I
intended: they came out to be `MyFieldMyType`, not
`MyInterfaceMyFieldMyType`, which is inconsistent, but not strictly
wrong.  But once supporting fragments, this is also now incorrect.
(Exactly why is described in the comments inline.)  In this commit, in
any case, I fix it.

To do that, I finally did the last of the refactors I've been hoping to
do but unable to successfully implement, which is to make the type-name
and type-name-prefix management clearer.  In the past it was kind of
spread out, and each caller would have to pass the right name into
`convertDefinition`, which go quite unwieldy.  Now, the case that really
wanted that -- the operation toplevel -- just does it own thing; and the
main name-generation code  is factored out into a separate file with
tests, and with a long comment that goes into all the details of the
algorithm that the design-doc didn't cover.  (I even had some fun using
a linked list to implement the prefix-stack!)

This allowed me to fix the above bug fairly easily -- actually the fix
was pretty much automatic once I understood how to organize things.
There is one change which is that if your query name is unexported, we
no longer do the same with the input-type names; it's unclear to me if
anyone will actually care about this behavior (Khan always makes the
queries exported) but if they did it was very inconsistent (only at the
query toplevel, and only for input-objects, not enums), so we can
reimplement it properly if that comes up.  As a bonus fix, we now better
handle the case where your type-names are lowercase, which is legal if
nonstandard GraphQL.

Issue: https://github.com/Khan/genqlient/issues/8

## Test plan:
make tesc


Author: benjaminjkraft

Reviewers: dnerdy, benjaminjkraft, aberkan, MiguelCastillo

Required Reviewers: 

Approved by: dnerdy

Checks:  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint,  Test (1.17),  Test (1.16),  Test (1.15),  Test (1.14),  Test (1.13),  Lint

Pull request URL: https://github.com/Khan/genqlient/pull/71
2021-08-30 10:50:11 -07:00

169 lines
7.4 KiB
Go

package generate
// This file generates the names for genqlient's generated types. This is
// somewhat tricky because the names need to be unique, stable, and, to the
// extent possible, human-readable and -writable. See DESIGN.md for an
// overview of the considerations; in short, we need long names.
//
// Specifically, the names we generate are of the form:
// MyOperationMyFieldMyTypeMySubFieldMySubType
// We need the "MyOperation" prefix because different operations may have
// different fields selected in the same "location" within the query. We need
// to include the field-path, because even within the same query, the same
// GraphQL type may have different selections in different locations.
// Including the types along the path is only strictly necessary in the case of
// interfaces, where, in a query like
// query Q {
// f { # type: I
// ... on T { g { h } }
// ... on U { g { h } }
// }
// }
// the type of <response>.f.g may be different depending on whether the
// concrete type is a T or a U; if we simply called the types QFG they'd
// collide. We could in principle omit the types where there are no interfaces
// in sight, but having the last thing in the name be the actual GraphQL type
// name (MySubType in the first example) makes things more readable, and the
// value of consistency seems greater than the value of brevity, given the
// types are quite verbose either way. Note that in all cases the "MyField" is
// the alias of the field -- the name it has in this query -- since you could
// have `query Q { a: f { b }, c: f { d } }` and Q.A and Q.C must have
// different types.
//
// One subtlety in the above description is: is the "MyType" the interface or
// the impelmentation? When it's a suffix, the answer is both: we generate
// both MyFieldMyInterface and MyFieldMyImplementation, and the latter, in Go,
// implements the former. (See DESIGN.md for more.) But as an infix, we use
// the type on which the field is requested. Concretely, the following schema
// and query:
// type Query { f: I }
// interface I { g: G }
// type T implements I { g: G, h: H }
// type U implements I { g: G, h: H }
// type G { g1: String, g2: String }
// type H { h1: String, h2: String, h3: String, h4: String }
//
// query Q {
// f {
// g { g1 g2 }
// ... on T { h { h1 h2 } }
// ... on U { h { h3 h4 } }
// }
// }
// The field g must have type QFIG (not QFTG and QFHG), so that QFI's method
// GetG() can return a consistent type. But the fields h must have types QFTH
// and QFUH (not QFIH), because the two are different: the former has fields h1
// and h2, whereas the latter has fields h3 and h4. So, in summary, since `g`
// is selected in a context of type I, it uses that (interface) type in its
// type-name, and `h` is selected in contexts of types T and U, they use those
// (implementation) types in their type-names.
//
// We do shorten the names in one case: if the name of a field ends with
// the name of its type, we omit the type name, avoiding types like
// MyQueryUserUser when querying a field of type user and value user. Note we
// do not do this for field names, both because it's less common, and because
// in `query Q { user { user { id } } }` we do need both QUser and QUserUser --
// they have different fields.
//
// Note that there are a few potential collisions from this algorithm:
// - When generating Go types for GraphQL interface types, we generate both
// ...MyFieldMyInterfaceType and ...MyFieldMyImplType. If an interface's
// name is a suffix of its implementation's name, and both are suffixes of a
// field of that type, we'll shorten both, resulting in a collision.
// - Names of different casing (e.g. fields `myField` and `MyField`) can
// collide (the first is standard usage but both are legal).
// - We don't put a special character between parts, so fields like
// query Q {
// ab { ... } # type: C
// abc { ... } # type: C
// a { ... } # type: BC
// }
// can collide.
// All cases seem fairly rare in practice; eventually we'll likely allow users
// the ability to specify their own names, which they could use to avoid this
// (see https://github.com/Khan/genqlient/issues/12).
// TODO(benkraft): We should probably at least try to detect it and bail.
//
// To implement all of the above, as we traverse the operation (and schema) in
// convert.go, we keep track of a list of parts to prefix to our type-names.
// The list always ends with a field, not a type; and we extend it when
// traversing fields, to allow for correct handling of the interface case
// discussed above. This file implements the actual maintenance of that
// prefix, and the code to compute the actual type-name from it.
//
// Note that input objects and enums are handled separately (inline in
// convertDefinition) since the same considerations don't apply and their names
// are thus quite simple. We also specially-handle the type of the toplevel
// response object (inline in convertOperation).
import (
"strings"
"github.com/vektah/gqlparser/v2/ast"
)
// Yes, a linked list! Of name-prefixes in *reverse* order, i.e. from end to
// start.
//
// We could use a stack -- it would probably be marginally
// more efficient -- but then the caller would have to know more about how to
// manage it safely. Using a list, and treating it as immutable, makes it
// easy.
type prefixList struct {
head string // the list goes back-to-front, so this is the *last* prefix
tail *prefixList
}
// creates a new one-element list
func newPrefixList(item string) *prefixList {
return &prefixList{head: item}
}
func joinPrefixList(prefix *prefixList) string {
var reversed []string
for ; prefix != nil; prefix = prefix.tail {
reversed = append(reversed, prefix.head)
}
l := len(reversed)
for i := 0; i < l/2; i++ {
reversed[i], reversed[l-1-i] = reversed[l-1-i], reversed[i]
}
return strings.Join(reversed, "")
}
// Given a prefix-list, and the next type-name, compute the prefix-list with
// that type-name added (if applicable). The returned value is not a valid
// prefix-list, since it ends with a type, not a field (see top-of-file
// comment), but it's used to construct both the type-names from the input and
// the next prefix-list.
func typeNameParts(prefix *prefixList, typeName string) *prefixList {
// GraphQL types are conventionally UpperCamelCase, but it's not required;
// our names will look best if they are.
typeName = upperFirst(typeName)
// If the prefix has just one part, that's the operation-name. There's no
// need to add "Query" or "Mutation". (Zero should never happen.)
if prefix == nil || prefix.tail == nil ||
// If the last prefix field ends with this type's name, omit the
// type-name (see the "shortened" case in the top-of-file comment).
strings.HasSuffix(prefix.head, typeName) {
return prefix
}
return &prefixList{typeName, prefix}
}
// Given a prefix-list, and a field, compute the next prefix-list, which will
// be used for that field's selections.
func nextPrefix(prefix *prefixList, field *ast.Field) *prefixList {
// Add the type.
prefix = typeNameParts(prefix, field.ObjectDefinition.Name)
// Add the field (there's no shortening here, see top-of-file comment).
prefix = &prefixList{upperFirst(field.Alias), prefix}
return prefix
}
// Given a prefix-list, and the GraphQL of the current type, compute the name
// we should give it in Go.
func makeTypeName(prefix *prefixList, typeName string) string {
return joinPrefixList(typeNameParts(prefix, typeName))
}