Skip to content

Commit

Permalink
Add "generic" option to the "optional" configuration for handling nul…
Browse files Browse the repository at this point in the history
…lable types (#252)

This is an implementation for #251, it adds a new `"generic"` option for
the `"optional"` configuration, and a companion type
`"optional_generic_type"` which is a fully qualified type with a
placeholder `%` for the generic parameter.

Co-authored-by: Dylan R. Johnston <[email protected]>
Co-authored-by: Ben Kraft <[email protected]>
  • Loading branch information
3 people authored May 6, 2023
1 parent 94b6a71 commit c61d7ac
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 12 deletions.
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ When releasing a new version:

### New features:

- The new `optional: generic` allows using a generic type to represent optionality. See the [documentation](genqlient.yaml) for details.

### Bug fixes:

## v0.6.0
Expand Down
11 changes: 11 additions & 0 deletions docs/genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,19 @@ use_extensions: boolean
# pointers-to-slices, so the GraphQL type `[String]` will map to the Go
# type `[]*string`, not `*[]*string`; GraphQL null and empty list simply
# map to Go nil- and empty-slice.
# - generic: optional fields are generated as type parameters to a generic type
# specified by `optional_generic_type`. E.g. fields with GraphQL type `String`
# will map to the Go type `generic.Type[string]`. This is useful if you have a
# type that mimics the behavior of Option<A> or Maybe<A> in other languages like
# Rust, Java, or Haskell.
optional: value

# Only used when `optional: generic` is set. `example.Type` must be a fully qualified
# generic type with only one generic parameter e.g. atomic.Value[string].
# It must also implement the `encoding/json.Marshaler` and `encoding/json.Unmarshaler`
# interface if you want it to serialize / deserialize properly.
optional_generic_type: github.com/organisation/repository/example.Type

# A map from GraphQL type name to Go fully-qualified type name to override
# the Go type genqlient will use for this GraphQL type.
#
Expand Down
35 changes: 23 additions & 12 deletions generate/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,19 @@ type Config struct {
// The following fields are documented in the [genqlient.yaml docs].
//
// [genqlient.yaml docs]: https://github.com/Khan/genqlient/blob/main/docs/genqlient.yaml
Schema StringList `yaml:"schema"`
Operations StringList `yaml:"operations"`
Generated string `yaml:"generated"`
Package string `yaml:"package"`
ExportOperations string `yaml:"export_operations"`
ContextType string `yaml:"context_type"`
ClientGetter string `yaml:"client_getter"`
Bindings map[string]*TypeBinding `yaml:"bindings"`
PackageBindings []*PackageBinding `yaml:"package_bindings"`
Optional string `yaml:"optional"`
StructReferences bool `yaml:"use_struct_references"`
Extensions bool `yaml:"use_extensions"`
Schema StringList `yaml:"schema"`
Operations StringList `yaml:"operations"`
Generated string `yaml:"generated"`
Package string `yaml:"package"`
ExportOperations string `yaml:"export_operations"`
ContextType string `yaml:"context_type"`
ClientGetter string `yaml:"client_getter"`
Bindings map[string]*TypeBinding `yaml:"bindings"`
PackageBindings []*PackageBinding `yaml:"package_bindings"`
Optional string `yaml:"optional"`
OptionalGenericType string `yaml:"optional_generic_type"`
StructReferences bool `yaml:"use_struct_references"`
Extensions bool `yaml:"use_extensions"`

// Set to true to use features that aren't fully ready to use.
//
Expand Down Expand Up @@ -99,6 +100,16 @@ func (c *Config) ValidateAndFillDefaults(baseDir string) error {
c.ContextType = "context.Context"
}

if c.Optional != "" && c.Optional != "value" && c.Optional != "pointer" && c.Optional != "generic" {
return errorf(nil, "optional must be one of: 'value' (default), 'pointer', or 'generic'")
}

if c.Optional == "generic" && c.OptionalGenericType == "" {
return errorf(nil, "if optional is set to 'generic', optional_generic_type must be set to the fully"+
"qualified name of a type with a single generic parameter"+
"\nExample: \"github.com/Org/Repo/optional.Value\"")
}

if c.Package == "" {
abs, err := filepath.Abs(c.Generated)
if err != nil {
Expand Down
11 changes: 11 additions & 0 deletions generate/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,17 @@ func (g *generator) convertType(
// options work, recursing here isn't as connvenient.)
// Note this does []*T or [][]*T, not e.g. *[][]T. See #16.
goTyp = &goPointerType{goTyp}
} else if !typ.NonNull && g.Config.Optional == "generic" {
var genericRef string
genericRef, err = g.ref(g.Config.OptionalGenericType)
if err != nil {
return nil, err
}

goTyp = &goGenericType{
GoGenericRef: genericRef,
Elem: goTyp,
}
}
return goTyp, err
}
Expand Down
5 changes: 5 additions & 0 deletions generate/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,11 @@ func TestGenerateWithConfig(t *testing.T) {
Generated: "generated.go",
Optional: "pointer",
}},
{"OptionalGeneric", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{
Generated: "generated.go",
Optional: "generic",
OptionalGenericType: "github.com/Khan/genqlient/internal/testutil.Option",
}},
}

sourceFilename := "SimpleQuery.graphql"
Expand Down

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

16 changes: 16 additions & 0 deletions generate/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var (
_ goType = (*goEnumType)(nil)
_ goType = (*goStructType)(nil)
_ goType = (*goInterfaceType)(nil)
_ goType = (*goGenericType)(nil)
)

type (
Expand All @@ -80,6 +81,12 @@ type (
// user (perhaps to handle nulls explicitly, or to avoid copying large
// structures).
goPointerType struct{ Elem goType }
// goGenericType represent the Go type GoGenericRef[Elem], used when requested by the
// user to box nullable data without using pointers or sentinel values
goGenericType struct {
GoGenericRef string
Elem goType
}
)

// Opaque types are defined by the user; pointers and slices need no definition
Expand All @@ -91,21 +98,27 @@ func (typ *goTypenameForBuiltinType) WriteDefinition(w io.Writer, g *generator)
}
func (typ *goSliceType) WriteDefinition(io.Writer, *generator) error { return nil }
func (typ *goPointerType) WriteDefinition(io.Writer, *generator) error { return nil }
func (typ *goGenericType) WriteDefinition(io.Writer, *generator) error { return nil }

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 *goGenericType) Reference() string {
return fmt.Sprintf("%s[%s]", typ.GoGenericRef, typ.Elem.Reference())
}

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 *goGenericType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() }

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() }
func (typ *goGenericType) 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 @@ -529,6 +542,7 @@ 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 *goGenericType) 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 }
Expand All @@ -537,6 +551,7 @@ 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 *goGenericType) 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 }
Expand All @@ -545,6 +560,7 @@ 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 *goGenericType) IsPointer() bool { return false }
func (typ *goEnumType) IsPointer() bool { return false }
func (typ *goStructType) IsPointer() bool { return false }
func (typ *goInterfaceType) IsPointer() bool { return false }
Expand Down
Loading

0 comments on commit c61d7ac

Please sign in to comment.