ff790e1c06
There are a bunch of places in genqlient where we just kind of hope you don't have ridiculous casing conflicts in your schema. Apparently with enum values there are actual schemas that have this problem! Now we have an option to disable. I ended up putting it all under `casing` instead of in `bindings` so we can clearly document the list of algorithms we support, and so we can have an `all_enums` value if you're working with a schema with a lot of this; in the future we may want to add similar behavior for types/fields, add more possible algorithms (e.g. to make things unexported), etc. I added tests for the new feature, although the way the tests are set up it wasn't convenient to do so for a schema where this is actually required. I also added a check for case conflicts that points you to this option. (I don't want to do it automatically for reasons described in the issue; mainly it just seemed a bit too magical.) Fixes #265.
194 lines
8.2 KiB
Go
194 lines
8.2 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 [issue #12]).
|
|
// TODO(benkraft): We should probably at least try to detect it and bail.
|
|
//
|
|
// [issue #12]: https://github.com/Khan/genqlient/issues/12
|
|
//
|
|
// 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 (
|
|
"fmt"
|
|
"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})
|
|
}
|
|
|
|
func (casing *Casing) enumValueName(goTypeName string, enum *ast.Definition, val *ast.EnumValueDefinition) string {
|
|
switch algo := casing.forEnum(enum.Name); algo {
|
|
case CasingDefault:
|
|
return goTypeName + goConstName(val.Name)
|
|
case CasingRaw:
|
|
return goTypeName + "_" + val.Name
|
|
default:
|
|
// Should already be caught by validation.
|
|
panic(fmt.Sprintf("unknown casing algorithm %s", algo))
|
|
}
|
|
}
|