diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 922ea665..f0d2f5d7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -31,6 +31,7 @@ When releasing a new version: - You can now enable `use_extensions` in the configuration file, to receive extensions returned by the GraphQL API server. Generated functions will return extensions as `map[string]interface{}`, if enabled. - You can now use `graphql.NewClientUsingGet` to create a client that uses query parameters to pass the query to the GraphQL API server. - In config files, `schema`, `operations`, and `generated` can now be absolute paths. +- You can now configure how nullable types are mapped to Go types in the configuration file. Specifically, you can set `optional: pointer` to have all nullable GraphQL arguments, input fields, and output fields map to pointers. ### Bug fixes: diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml index d30235b6..37bb8e94 100644 --- a/docs/genqlient.yaml +++ b/docs/genqlient.yaml @@ -98,6 +98,23 @@ use_struct_references: boolean use_extensions: boolean +# Customize how optional fields are handled. +optional: + # Customize how models are generated for optional fields. This can currently + # be set to one of the following values: + # - value (default): optional fields are generated as values, the same as + # non-optional fields. E.g. fields with GraphQL types `String` or `String!` + # will both map to the Go type `string`. When values are absent in + # responses the zero value will be used. + # - pointer: optional fields are generated as pointers. E.g. fields with + # GraphQL type `String` will map to the Go type `*string`. When values are + # absent in responses `nil` will be used. Optional list fields do not use + # 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. + output: value + + # 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 0654a336..d6f057d9 100644 --- a/generate/config.go +++ b/generate/config.go @@ -26,6 +26,7 @@ type Config struct { ContextType string `yaml:"context_type"` ClientGetter string `yaml:"client_getter"` Bindings map[string]*TypeBinding `yaml:"bindings"` + Optional string `yaml:"optional"` StructReferences bool `yaml:"use_struct_references"` Extensions bool `yaml:"use_extensions"` diff --git a/generate/convert.go b/generate/convert.go index 38035f52..adfbf0c1 100644 --- a/generate/convert.go +++ b/generate/convert.go @@ -256,7 +256,7 @@ func (g *generator) convertType( oe := true options.Omitempty = &oe } - } else if options.GetPointer() { + } else if options.GetPointer() || (!typ.NonNull && g.Config.Optional == "pointer") { // Whatever we get, wrap it in a pointer. (Because of the way the // options work, recursing here isn't as connvenient.) // Note this does []*T or [][]*T, not e.g. *[][]T. See #16. diff --git a/generate/generate_test.go b/generate/generate_test.go index 64690a27..21320fe5 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -147,55 +147,64 @@ func getDefaultConfig(t *testing.T) *Config { // configurations. It uses snapshots, just like TestGenerate. func TestGenerateWithConfig(t *testing.T) { tests := []struct { - name string - baseDir string // relative to dataDir - config *Config // omits Schema and Operations, set below. + name string + baseDir string // relative to dataDir + operations []string // overrides the default set below + config *Config // omits Schema and Operations, set below. }{ - {"DefaultConfig", "", getDefaultConfig(t)}, - {"Subpackage", "", &Config{ + {"DefaultConfig", "", nil, getDefaultConfig(t)}, + {"Subpackage", "", nil, &Config{ Generated: "mypkg/myfile.go", }}, - {"SubpackageConfig", "mypkg", &Config{ + {"SubpackageConfig", "mypkg", nil, &Config{ Generated: "myfile.go", // (relative to genqlient.yaml) }}, - {"PackageName", "", &Config{ + {"PackageName", "", nil, &Config{ Generated: "myfile.go", Package: "mypkg", }}, - {"ExportOperations", "", &Config{ + {"ExportOperations", "", nil, &Config{ Generated: "generated.go", ExportOperations: "operations.json", }}, - {"CustomContext", "", &Config{ + {"CustomContext", "", nil, &Config{ Generated: "generated.go", ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext", }}, - {"StructReferences", "", &Config{ + {"StructReferences", "", nil, &Config{ StructReferences: true, Generated: "generated-structrefs.go", }}, - {"NoContext", "", &Config{ + {"NoContext", "", nil, &Config{ Generated: "generated.go", ContextType: "-", }}, - {"ClientGetter", "", &Config{ + {"ClientGetter", "", nil, &Config{ Generated: "generated.go", ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromContext", }}, - {"ClientGetterCustomContext", "", &Config{ + {"ClientGetterCustomContext", "", nil, &Config{ Generated: "generated.go", ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromMyContext", ContextType: "github.com/Khan/genqlient/internal/testutil.MyContext", }}, - {"ClientGetterNoContext", "", &Config{ + {"ClientGetterNoContext", "", nil, &Config{ Generated: "generated.go", ClientGetter: "github.com/Khan/genqlient/internal/testutil.GetClientFromNowhere", ContextType: "-", }}, - {"Extensions", "", &Config{ + {"Extensions", "", nil, &Config{ Generated: "generated.go", Extensions: true, }}, + {"OptionalValue", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ + Generated: "generated.go", + Optional: "value", + }}, + {"OptionalPointer", "", []string{"ListInput.graphql", "QueryWithSlices.graphql"}, &Config{ + Generated: "generated.go", + Optional: "pointer", + }}, } sourceFilename := "SimpleQuery.graphql" @@ -206,7 +215,14 @@ func TestGenerateWithConfig(t *testing.T) { t.Run(test.name, func(t *testing.T) { err := config.ValidateAndFillDefaults(baseDir) config.Schema = []string{filepath.Join(dataDir, "schema.graphql")} - config.Operations = []string{filepath.Join(dataDir, sourceFilename)} + if test.operations == nil { + config.Operations = []string{filepath.Join(dataDir, sourceFilename)} + } else { + config.Operations = make([]string, len(test.operations)) + for i := range test.operations { + config.Operations[i] = filepath.Join(dataDir, test.operations[i]) + } + } if err != nil { t.Fatal(err) } diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-OptionalPointer-testdata-queries-generated.go b/generate/testdata/snapshots/TestGenerateWithConfig-OptionalPointer-testdata-queries-generated.go new file mode 100644 index 00000000..72a5a552 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-OptionalPointer-testdata-queries-generated.go @@ -0,0 +1,142 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package queries + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// 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 *ListInputQueryUser `json:"user"` +} + +// GetUser returns ListInputQueryResponse.User, and is useful for accessing the field via an interface. +func (v *ListInputQueryResponse) GetUser() *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 *QueryWithSlicesUser `json:"user"` +} + +// GetUser returns QueryWithSlicesResponse.User, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesResponse) GetUser() *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 []*string `json:"emailsWithNulls"` + EmailsWithNullsOrNull []*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() []*string { return v.EmailsWithNulls } + +// GetEmailsWithNullsOrNull returns QueryWithSlicesUser.EmailsWithNullsOrNull, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesUser) GetEmailsWithNullsOrNull() []*string { return v.EmailsWithNullsOrNull } + +// __ListInputQueryInput is used internally by genqlient +type __ListInputQueryInput struct { + Names []*string `json:"names"` +} + +// GetNames returns __ListInputQueryInput.Names, and is useful for accessing the field via an interface. +func (v *__ListInputQueryInput) GetNames() []*string { return v.Names } + +func ListInputQuery( + ctx context.Context, + client graphql.Client, + names []*string, +) (*ListInputQueryResponse, error) { + req := &graphql.Request{ + OpName: "ListInputQuery", + Query: ` +query ListInputQuery ($names: [String]) { + user(query: {names:$names}) { + id + } +} +`, + Variables: &__ListInputQueryInput{ + Names: names, + }, + } + var err error + + var data ListInputQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + +func QueryWithSlices( + ctx context.Context, + client graphql.Client, +) (*QueryWithSlicesResponse, error) { + req := &graphql.Request{ + OpName: "QueryWithSlices", + Query: ` +query QueryWithSlices { + user { + emails + emailsOrNull + emailsWithNulls + emailsWithNullsOrNull + } +} +`, + } + var err error + + var data QueryWithSlicesResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + diff --git a/generate/testdata/snapshots/TestGenerateWithConfig-OptionalValue-testdata-queries-generated.go b/generate/testdata/snapshots/TestGenerateWithConfig-OptionalValue-testdata-queries-generated.go new file mode 100644 index 00000000..7d6fafe3 --- /dev/null +++ b/generate/testdata/snapshots/TestGenerateWithConfig-OptionalValue-testdata-queries-generated.go @@ -0,0 +1,142 @@ +// Code generated by github.com/Khan/genqlient, DO NOT EDIT. + +package queries + +import ( + "context" + + "github.com/Khan/genqlient/graphql" +) + +// 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 ListInputQueryUser `json:"user"` +} + +// GetUser returns ListInputQueryResponse.User, and is useful for accessing the field via an interface. +func (v *ListInputQueryResponse) GetUser() 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 QueryWithSlicesUser `json:"user"` +} + +// GetUser returns QueryWithSlicesResponse.User, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesResponse) GetUser() 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 []string `json:"emailsWithNulls"` + EmailsWithNullsOrNull []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() []string { return v.EmailsWithNulls } + +// GetEmailsWithNullsOrNull returns QueryWithSlicesUser.EmailsWithNullsOrNull, and is useful for accessing the field via an interface. +func (v *QueryWithSlicesUser) GetEmailsWithNullsOrNull() []string { return v.EmailsWithNullsOrNull } + +// __ListInputQueryInput is used internally by genqlient +type __ListInputQueryInput struct { + Names []string `json:"names"` +} + +// GetNames returns __ListInputQueryInput.Names, and is useful for accessing the field via an interface. +func (v *__ListInputQueryInput) GetNames() []string { return v.Names } + +func ListInputQuery( + ctx context.Context, + client graphql.Client, + names []string, +) (*ListInputQueryResponse, error) { + req := &graphql.Request{ + OpName: "ListInputQuery", + Query: ` +query ListInputQuery ($names: [String]) { + user(query: {names:$names}) { + id + } +} +`, + Variables: &__ListInputQueryInput{ + Names: names, + }, + } + var err error + + var data ListInputQueryResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} + +func QueryWithSlices( + ctx context.Context, + client graphql.Client, +) (*QueryWithSlicesResponse, error) { + req := &graphql.Request{ + OpName: "QueryWithSlices", + Query: ` +query QueryWithSlices { + user { + emails + emailsOrNull + emailsWithNulls + emailsWithNullsOrNull + } +} +`, + } + var err error + + var data QueryWithSlicesResponse + resp := &graphql.Response{Data: &data} + + err = client.MakeRequest( + ctx, + req, + resp, + ) + + return &data, err +} +