ab1aaed845
## Summary: We typically name our types `OperationFieldTypeFieldType`, but if a type's name matches the preceding field-name, we omit the type-name. In #71 I changed the behavior such that we no longer do that in the case where the type's name matches some suffix of the name-so-far that's longer than just the leaf field-name. This was semi-intentional; I assumed it didn't matter and would be more predictable this way. But it turns out that was a feature, both in the sense that almost any change to the type-name-generator is breaking, and in the sense that it made the names uglier. Plus, now that we have better conflict-detection (#94), the possibility that some tricksy type-names could cause problems is no longer as much of an issue, so we can be a little less careful here. (Although I think this is no less safe than before; the field-names are the important part.) So in this commit I revert the change. Specifically, this comes up a lot at Khan where we do ``` mutation ForcePhantom { forcePhantom { # type: ForcePhantom error { ... } # type: ForcePhantomError } } ``` Before #71, and again after this change, we'll generate `ForcePhantomForcePhantomError` for `error`; before we'd generate `ForcePhantomForcePhantomErrorForcePhantomError`. Issue: https://github.com/Khan/genqlient/issues/109 ## Test plan: make tesc Author: benjaminjkraft Reviewers: csilvers, aberkan, dnerdy, jvoll, mahtabsabet, MiguelCastillo, StevenACoffman Required Reviewers: Approved By: csilvers Checks: ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint, ✅ Test (1.17), ✅ Test (1.16), ✅ Test (1.15), ✅ Test (1.14), ✅ Lint Pull Request URL: https://github.com/Khan/genqlient/pull/110
179 lines
7.8 KiB
Go
179 lines
7.8 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 docs/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 implementation? When it's a suffix, the answer is both: we generate
|
|
// both MyFieldMyInterface and MyFieldMyImplementation, and the latter, in Go,
|
|
// implements the former. (See docs/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 name-so-far ends with this type's name, omit the
|
|
// type-name (see the "shortened" case in the top-of-file comment).
|
|
strings.HasSuffix(joinPrefixList(prefix), 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))
|
|
}
|
|
|
|
// Like makeTypeName, but append typeName unconditionally.
|
|
//
|
|
// This is used for when you specify a type-name for a field of interface
|
|
// type; we use YourName for the interface, but need to do YourNameImplName for
|
|
// the implementations.
|
|
func makeLongTypeName(prefix *prefixList, typeName string) string {
|
|
typeName = upperFirst(typeName)
|
|
return joinPrefixList(&prefixList{typeName, prefix})
|
|
}
|