From 4d7589dc978e4e17280944603b837c42266b2192 Mon Sep 17 00:00:00 2001 From: "Dylan R. Johnston" Date: Fri, 10 Feb 2023 10:24:55 +0800 Subject: [PATCH 1/3] Add "generic" option to the "optional" configuration. --- generate/config.go | 25 +++++++++++++------------ generate/convert.go | 8 ++++++++ generate/generate.go | 7 +++++++ generate/types.go | 16 ++++++++++++++++ go.mod | 7 +++++-- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/generate/config.go b/generate/config.go index 3881c976..147ad1f2 100644 --- a/generate/config.go +++ b/generate/config.go @@ -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. // diff --git a/generate/convert.go b/generate/convert.go index caf3754f..4030d638 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -11,6 +11,7 @@ package generate import ( "fmt" + "regexp" "sort" "github.com/vektah/gqlparser/v2/ast" @@ -262,6 +263,13 @@ 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" { + qualifiedTypeName := regexp.MustCompile("[^/]+$").FindString(g.Config.OptionalGenericType) + + goTyp = &goGenericType{ + GenericName: qualifiedTypeName, + Elem: goTyp, + } } return goTyp, err } diff --git a/generate/generate.go b/generate/generate.go index 22ee618d..04504e94 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -388,6 +388,13 @@ func Generate(config *Config) (map[string][]byte, error) { } } + if g.Config.Optional == "generic" { + _, err = g.ref(g.Config.OptionalGenericType) + if err != nil { + return nil, err + } + } + // Now really glue it all together, and format. var buf bytes.Buffer err = g.render("header.go.tmpl", &buf, g) diff --git a/generate/types.go b/generate/types.go index f50df7a3..16a9bb6d 100644 --- a/generate/types.go +++ b/generate/types.go @@ -55,6 +55,7 @@ var ( _ goType = (*goEnumType)(nil) _ goType = (*goStructType)(nil) _ goType = (*goInterfaceType)(nil) + _ goType = (*goGenericType)(nil) ) type ( @@ -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 ELem, used when requested by the + // user (to box null explicitly) + goGenericType struct { + GenericName string + Elem goType + } ) // Opaque types are defined by the user; pointers and slices need no definition @@ -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 strings.Replace(typ.GenericName, "%", typ.Elem.Reference(), 1) +} 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 @@ -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 } @@ -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 } @@ -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 } diff --git a/go.mod b/go.mod index 1531ea44..aa9d9f87 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ go 1.16 require ( github.com/99designs/gqlgen v0.17.2 - github.com/agnivade/levenshtein v1.1.1 // indirect github.com/alexflint/go-arg v1.4.2 github.com/bradleyjkemp/cupaloy/v2 v2.6.0 github.com/stretchr/testify v1.7.0 github.com/vektah/gqlparser/v2 v2.4.5 - golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect golang.org/x/tools v0.1.10 gopkg.in/yaml.v2 v2.4.0 ) + +require ( + github.com/agnivade/levenshtein v1.1.1 // indirect + golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect +) From 778a2350e10f80440edfe8bb3cb0dd730392246c Mon Sep 17 00:00:00 2001 From: "Dylan R. Johnston" Date: Thu, 16 Feb 2023 15:28:32 +0800 Subject: [PATCH 2/3] Address pull request feedback --- docs/genqlient.yaml | 11 ++ generate/config.go | 10 ++ generate/convert.go | 11 +- generate/generate.go | 7 - generate/generate_test.go | 5 + ...ionalGeneric-testdata-queries-generated.go | 153 ++++++++++++++++++ generate/types.go | 10 +- go.mod | 11 +- go.sum | 4 - internal/testutil/types.go | 58 +++++++ 10 files changed, 259 insertions(+), 21 deletions(-) create mode 100644 generate/testdata/snapshots/TestGenerateWithConfig-OptionalGeneric-testdata-queries-generated.go diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml index 3ffa2e38..5e5601bc 100644 --- a/docs/genqlient.yaml +++ b/docs/genqlient.yaml @@ -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 or Maybe 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. # diff --git a/generate/config.go b/generate/config.go index 147ad1f2..c1d3a2dd 100644 --- a/generate/config.go +++ b/generate/config.go @@ -100,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 { diff --git a/generate/convert.go b/generate/convert.go index 4030d638..41eb1238 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -11,7 +11,6 @@ package generate import ( "fmt" - "regexp" "sort" "github.com/vektah/gqlparser/v2/ast" @@ -264,11 +263,15 @@ func (g *generator) convertType( // Note this does []*T or [][]*T, not e.g. *[][]T. See #16. goTyp = &goPointerType{goTyp} } else if !typ.NonNull && g.Config.Optional == "generic" { - qualifiedTypeName := regexp.MustCompile("[^/]+$").FindString(g.Config.OptionalGenericType) + var genericRef string + genericRef, err = g.ref(g.Config.OptionalGenericType) + if err != nil { + return nil, err + } goTyp = &goGenericType{ - GenericName: qualifiedTypeName, - Elem: goTyp, + GoGenericRef: genericRef, + Elem: goTyp, } } return goTyp, err diff --git a/generate/generate.go b/generate/generate.go index 04504e94..22ee618d 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -388,13 +388,6 @@ func Generate(config *Config) (map[string][]byte, error) { } } - if g.Config.Optional == "generic" { - _, err = g.ref(g.Config.OptionalGenericType) - if err != nil { - return nil, err - } - } - // Now really glue it all together, and format. var buf bytes.Buffer err = g.render("header.go.tmpl", &buf, g) diff --git a/generate/generate_test.go b/generate/generate_test.go index c976244b..60433915 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -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" diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-OptionalGeneric-testdata-queries-generated.go b/generate/testdata/snapshots/TestGenerateWithConfig-OptionalGeneric-testdata-queries-generated.go new file mode 100644 index 00000000..24cbd221 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-OptionalGeneric-testdata-queries-generated.go @@ -0,0 +1,153 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package queries + +import ( + "context" + + "github.com/Khan/genqlient/graphql" + "github.com/Khan/genqlient/internal/testutil" +) + +// ListInputQueryResponse is returned by ListInputQuery on success. +type ListInputQueryResponse struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User testutil.Option[ListInputQueryUser] `json:"user"` +} + +// GetUser returns ListInputQueryResponse.User, and is useful for accessing the field via an interface. +func (v *ListInputQueryResponse) GetUser() testutil.Option[ListInputQueryUser] { return v.User } + +// ListInputQueryUser includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type ListInputQueryUser struct { + // id is the user's ID. + // + // It is stable, unique, and opaque, like all good IDs. + Id string `json:"id"` +} + +// GetId returns ListInputQueryUser.Id, and is useful for accessing the field via an interface. +func (v *ListInputQueryUser) GetId() string { return v.Id } + +// QueryWithSlicesResponse is returned by QueryWithSlices on success. +type QueryWithSlicesResponse struct { + // user looks up a user by some stuff. + // + // See UserQueryInput for what stuff is supported. + // If query is null, returns the current user. + User testutil.Option[QueryWithSlicesUser] `json:"user"` +} + +// GetUser returns QueryWithSlicesResponse.User, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesResponse) GetUser() testutil.Option[QueryWithSlicesUser] { return v.User } + +// QueryWithSlicesUser includes the requested fields of the GraphQL type User. +// The GraphQL type's documentation follows. +// +// A User is a user! +type QueryWithSlicesUser struct { + Emails []string `json:"emails"` + EmailsOrNull []string `json:"emailsOrNull"` + EmailsWithNulls []testutil.Option[string] `json:"emailsWithNulls"` + EmailsWithNullsOrNull []testutil.Option[string] `json:"emailsWithNullsOrNull"` +} + +// GetEmails returns QueryWithSlicesUser.Emails, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesUser) GetEmails() []string { return v.Emails } + +// GetEmailsOrNull returns QueryWithSlicesUser.EmailsOrNull, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesUser) GetEmailsOrNull() []string { return v.EmailsOrNull } + +// GetEmailsWithNulls returns QueryWithSlicesUser.EmailsWithNulls, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesUser) GetEmailsWithNulls() []testutil.Option[string] { + return v.EmailsWithNulls +} + +// GetEmailsWithNullsOrNull returns QueryWithSlicesUser.EmailsWithNullsOrNull, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesUser) GetEmailsWithNullsOrNull() []testutil.Option[string] { + return v.EmailsWithNullsOrNull +} + +// __ListInputQueryInput is used internally by genqlient +type __ListInputQueryInput struct { + Names []testutil.Option[string] `json:"names"` +} + +// GetNames returns __ListInputQueryInput.Names, and is useful for accessing the field via an interface. +func (v *__ListInputQueryInput) GetNames() []testutil.Option[string] { return v.Names } + +// The query or mutation executed by ListInputQuery. +const ListInputQuery_Operation = ` +query ListInputQuery ($names: [String]) { + user(query: {names:$names}) { + id + } +} +` + +func ListInputQuery( + ctx context.Context, + client graphql.Client, + names []testutil.Option[string], +) (*ListInputQueryResponse, error) { + req := &graphql.Request{ + OpName: "ListInputQuery", + Query: ListInputQuery_Operation, + Variables: &__ListInputQueryInput{ + Names: names, + }, + } + var err error + + var data ListInputQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + +// The query or mutation executed by QueryWithSlices. +const QueryWithSlices_Operation = ` +query QueryWithSlices { + user { + emails + emailsOrNull + emailsWithNulls + emailsWithNullsOrNull + } +} +` + +func QueryWithSlices( + ctx context.Context, + client graphql.Client, +) (*QueryWithSlicesResponse, error) { + req := &graphql.Request{ + OpName: "QueryWithSlices", + Query: QueryWithSlices_Operation, + } + var err error + + var data QueryWithSlicesResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + diff --git a/generate/types.go b/generate/types.go index 16a9bb6d..07c661c4 100644 --- a/generate/types.go +++ b/generate/types.go @@ -81,11 +81,11 @@ type ( // user (perhaps to handle nulls explicitly, or to avoid copying large // structures). goPointerType struct{ Elem goType } - // goGenericType represent the Go type ELem, used when requested by the - // user (to box null explicitly) + // 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 { - GenericName string - Elem goType + GoGenericRef string + Elem goType } ) @@ -105,7 +105,7 @@ 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 strings.Replace(typ.GenericName, "%", typ.Elem.Reference(), 1) + return fmt.Sprintf("%s[%s]", typ.GoGenericRef, typ.Elem.Reference()) } func (typ *goOpaqueType) SelectionSet() ast.SelectionSet { return nil } diff --git a/go.mod b/go.mod index aa9d9f87..bd298e54 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Khan/genqlient -go 1.16 +go 1.18 require ( github.com/99designs/gqlgen v0.17.2 @@ -14,5 +14,14 @@ require ( require ( github.com/agnivade/levenshtein v1.1.1 // indirect + github.com/alexflint/go-scalar v1.0.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/hashicorp/golang-lru v0.5.0 // indirect + github.com/mitchellh/mapstructure v1.2.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum index e519639c..fff647b6 100644 --- a/go.sum +++ b/go.sum @@ -62,7 +62,6 @@ github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= @@ -70,7 +69,6 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -82,13 +80,11 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/testutil/types.go b/internal/testutil/types.go index c2028598..0fc4330c 100644 --- a/internal/testutil/types.go +++ b/internal/testutil/types.go @@ -2,6 +2,7 @@ package testutil import ( "context" + "encoding/json" "time" "github.com/Khan/genqlient/graphql" @@ -49,3 +50,60 @@ func UnmarshalDate(b []byte, t *time.Time) error { *t, err = time.Parse(`"`+dateFormat+`"`, string(b)) return err } + +type Option[V any] struct { + value V + ok bool +} + +func Some[V any](value V) Option[V] { + return Option[V]{value: value, ok: true} +} + +func None[V any]() Option[V] { + return Option[V]{ok: false} +} + +func (v Option[V]) Unpack() (V, bool) { + return v.value, v.ok +} + +func (v Option[V]) Get(fallback V) V { + if v.ok { + return v.value + } + + return fallback +} + +func FromPtr[V any](ptr *V) Option[V] { + if ptr == nil { + return None[V]() + } + + return Some(*ptr) +} + +func (value Option[V]) MarshalJSON() ([]byte, error) { + if value.ok { + return json.Marshal(value.value) + } else { + return json.Marshal((*V)(nil)) + } +} + +func (value *Option[V]) UnmarshalJSON(data []byte) error { + v := (*V)(nil) + + err := json.Unmarshal(data, &v) + if err != nil { + return err + } + + if v != nil { + value.value = *v + value.ok = true + } + + return nil +} From 7dcc1d60f607e23e4563c02658e0edf26012e7f4 Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Sat, 6 May 2023 10:37:58 -0700 Subject: [PATCH 3/3] changelog --- docs/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 783e67d9..221f87a8 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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