## Summary: In this commit I begin the journey to add the long-awaited support for interfaces (part of #8). Well, it's not the beginning: I already had some half-written broken code around. But it's the first fully functional support, and especially, the first *tested* support; it's probably best to review the nontrivially-changed code as if it were new. Conceptually, the code so far is pretty simple: we generate an interface type, and the implementations. (That code is in fact mostly unchanged.) The complexity comes in because encoding/json doesn't know how to unmarshal that. So we have to add an UnmarshalJSON method, which actually has to be on the types with interface-type fields, that knows how. I factored it into two methods, such that that UnmarshalJSON method is just glue, and then there's a separate function, corresponding to each interface-type, that actually does all the work. (If only one could just write it as an actual method!) The method uses the same trick suggested to me by a few others in another context to deserialize all but one field, then handle that field specially, which is discussed in the code. This still has some limitations, which will be lifted in future commits: - it doesn't allow for list-of-interface fields - it requires that you manually ask for `__typename` - it doesn't support fragments, i.e. you can only query for interface fields, not concrete-type-specific ones But it works, even in integration tests, which is progress! As a part of this, I added a proper config option for the "allow broken features" flag, since I need to be able to set it from the integration tests which are in a separate package (and actually shell out via `go generate`). I also renamed what was to be the first case (InterfaceNoFragments), and replaced it with a further-simplified version (avoiding list-of-interface fields. [1] https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md Issue: https://github.com/Khan/genqlient/issues/8 ## Test plan: make tesc Author: benjaminjkraft Reviewers: dnerdy, benjaminjkraft, aberkan, csilvers, MiguelCastillo Required Reviewers: Approved by: dnerdy Checks: ⌛ Test (1.17), ✅ Test (1.16), ⌛ Test (1.15), ⌛ Test (1.14), ⌛ Test (1.13), ✅ Lint, ✅ Test (1.17), ⌛ Test (1.16), ⌛ Test (1.15), ⌛ Test (1.14), ⌛ Test (1.13), ✅ Lint Pull request URL: https://github.com/Khan/genqlient/pull/52
143 lines
4.6 KiB
Go
143 lines
4.6 KiB
Go
package generate
|
|
|
|
import (
|
|
"go/token"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
var defaultConfig = &Config{
|
|
Schema: "schema.graphql",
|
|
Operations: []string{"genqlient.graphql"},
|
|
Generated: "generated.go",
|
|
ContextType: "context.Context",
|
|
}
|
|
|
|
type Config struct {
|
|
// The filename with the GraphQL schema (in SDL format); defaults to
|
|
// schema.graphql
|
|
Schema string `yaml:"schema"`
|
|
|
|
// Filenames or globs with the operations for which to generate code;
|
|
// defaults to genqlient.graphql.
|
|
//
|
|
// These may be .graphql files, containing the queries in SDL format, or
|
|
// Go files, in which case any string-literal starting with (optional
|
|
// whitespace and) the string "# @genqlient" will be extracted as a query.
|
|
Operations []string `yaml:"operations"`
|
|
|
|
// If set, a file at this path will be generated containing the exact
|
|
// operations that genqlient will send to the server.
|
|
//
|
|
// This is useful for systems which require queries to be explicitly
|
|
// safelisted, especially for cases like queries involving fragments where
|
|
// it may not exactly match the input queries. The JSON is an object of
|
|
// the form
|
|
// {"operations": [{
|
|
// "operationName": "operationname",
|
|
// "query": "query operationName { ... }",
|
|
// "sourceLocation": "myqueriesfile.graphql",
|
|
// }]}
|
|
// Keys may be added in the future.
|
|
//
|
|
// By default, no such file is written.
|
|
ExportOperations string `yaml:"export_operations"`
|
|
|
|
// The filename to which to write the generated code; defaults to
|
|
// generated.go
|
|
Generated string `yaml:"generated"`
|
|
|
|
// The package name for the output code; defaults to the directory name of
|
|
// Generated
|
|
Package string `yaml:"package"`
|
|
|
|
// Set to the fully-qualified name of a type which generated helpers should
|
|
// accept and use as the context.Context for HTTP requests. Defaults to
|
|
// context.Context; set to the empty string to omit context entirely.
|
|
ContextType string `yaml:"context_type"`
|
|
|
|
// If set, a snippet of Go code to get a *graphql.Client from the context
|
|
// (which will be named ctx). For example, this might do
|
|
// ctx.Value(myKey).(*graphql.Client). If omitted, client must be
|
|
// passed to each method explicitly.
|
|
// TODO(#5): This is a bit broken, fix it.
|
|
ClientGetter string `yaml:"client_getter"`
|
|
|
|
// A map from GraphQL scalar type name to Go fully-qualified type name for
|
|
// the types to use for any custom or builtin scalars. By default, builtin
|
|
// scalars are mapped to the obvious Go types (String and ID to string, Int
|
|
// to int, Float to float64, and Boolean to bool), but this setting will
|
|
// extend or override those mappings. These types must define MarshalJSON
|
|
// and UnmarshalJSON methods, or otherwise be convertible to JSON.
|
|
Scalars map[string]string `yaml:"scalars"`
|
|
|
|
// Set to true to use features that aren't fully ready to use.
|
|
//
|
|
// This is primarily intended for genqlient's own tests. These features
|
|
// are likely BROKEN and come with NO EXPECTATION OF COMPATIBBILITY. Use
|
|
// them at your own risk!
|
|
AllowBrokenFeatures bool `yaml:"allow_broken_features"`
|
|
|
|
// Set automatically to the filename of the config file itself.
|
|
configFilename string
|
|
}
|
|
|
|
// baseDir returns the directory of the config-file (relative to which
|
|
// all the other paths are resolved).
|
|
func (c *Config) baseDir() string {
|
|
return filepath.Dir(c.configFilename)
|
|
}
|
|
|
|
func (c *Config) ValidateAndFillDefaults(configFilename string) error {
|
|
c.configFilename = configFilename
|
|
// Make paths relative to config dir
|
|
c.Schema = filepath.Join(c.baseDir(), c.Schema)
|
|
for i := range c.Operations {
|
|
c.Operations[i] = filepath.Join(c.baseDir(), c.Operations[i])
|
|
}
|
|
c.Generated = filepath.Join(c.baseDir(), c.Generated)
|
|
if c.ExportOperations != "" {
|
|
c.ExportOperations = filepath.Join(c.baseDir(), c.ExportOperations)
|
|
}
|
|
|
|
if c.Package == "" {
|
|
abs, err := filepath.Abs(c.Generated)
|
|
if err != nil {
|
|
return errorf(nil, "unable to guess package-name: %v", err)
|
|
}
|
|
|
|
base := filepath.Base(filepath.Dir(abs))
|
|
if !token.IsIdentifier(base) {
|
|
return errorf(nil, "unable to guess package-name: %v is not a valid identifier", base)
|
|
}
|
|
|
|
c.Package = base
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ReadAndValidateConfig(filename string) (*Config, error) {
|
|
config := *defaultConfig
|
|
if filename != "" {
|
|
text, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, errorf(nil, "unreadable config file %v: %v", filename, err)
|
|
}
|
|
|
|
err = yaml.UnmarshalStrict(text, &config)
|
|
if err != nil {
|
|
return nil, errorf(nil, "invalid config file %v: %v", filename, err)
|
|
}
|
|
}
|
|
|
|
err := config.ValidateAndFillDefaults(filename)
|
|
if err != nil {
|
|
return nil, errorf(nil, "invalid config file %v: %v", filename, err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|