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

Add "generic" option to the "optional" configuration for handling nullable types #252

Merged
merged 5 commits into from
May 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 == "" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

maybe also complain 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