Documentation ¶
Overview ¶
Package structschema is used to create a GraphQL schema via reflection on go types.
A schema is built in structschema by types that follow a particular set of naming conventions:
Object types ¶
A Go struct optionally includes a field of type Meta. The Meta type is a zero length struct that is intended to serve as a place to attach a struct tag containing additional GraphQL schema definition. Meta fields will be ignored for schema processing. If the Meta field exists, its struct tag is expected to contain a partial GraphQL object definition. This can be used to declare the signatures for fields accessed via resolver methods, change the name of the type, or add directives to the type. Note that struct tags on Meta fields do NOT conform to the standard Go convention of `namespace:"..."`. Instead, the entire struct tag is interpreted as GraphQL schema definition.
GraphQL fields for the object type are constructed by merging any fields declared on a Meta with the public fields of the struct. Each struct field may optionally have a "gq" field in its struct tag. The gq part of the struct tag consists of two parts separated by a ;. If the second part is unused, the ; may be omitted. The first part of the field consists of a GraphQL field definition, or a subset thereof. Any missing portions of the GraphQL field signature will be filled in with what information can be obtained via reflection. The second part of the field consists of a doc string that is set as the field's description. As a special case, to omit a field from GraphQL use the string "-" (example: `gq:"-"`)
GraphQL fields declared solely via Meta declaration are expected to have a resolver method. Resolver methods are named Resolve<FieldName> (case insensitively), an should conform to the following conventions:
A resolver method optionally takes a ResolverContext as its first argument. If a ResolverContext is the first argument, parameters to accept GraphQL declared arguments become optional.
After the context argument, a resolver method may accept any number of arguments that can be provided by ArgProviders registered with the builder.
After the context and injected arguments, the method should declare, in order, parameters matching the GraphQL arguments for the field.
The method's return value should conform to one of the following signatures: (TypeFromGraphQL) - Synchronously returns a value (TypeFromGraphQL, error) - Synchronously returns either a value or an error (chan TypeFromGraphQL) - Asynchronously returns a value. One value will be read from the chan, further values will be ignored (chan TypeFromGraphQL, chan error) - Asynchronously returns either a value or an error. The first value to appear on either the value chan or error chan will be the value used. (func () <supported return signature>) - Asynchronously returns something specified by the return value of the returned function
Interface types ¶
A Go struct containing a single field named "Interface" of type interface{...}. This field may optionally have a struct tag which is processed according to the same rule as Meta on an object type.
Union types ¶
A Go struct containing a single field named "Union" of type interface{...}. This field may optionally have a struct tag which is processed according to the same rule as Meta on an object type.
Enum types ¶
A Go struct with a single anonymous embedding of type ss.Enum. This field may optionally have a struct tag which is processed according to the same rule as Meta on an object type. ss.Enum is an alias for string, so at runtime the value of the enum will be stored in this field.
Scalar types ¶
Any Go type which implements ScalarMarshaler and ScalarUnmarshaler.
Example ¶
package main import ( "bytes" "context" "encoding/json" "fmt" "io" "os" "github.com/housecanary/gq/query" "github.com/housecanary/gq/schema/structschema" "github.com/housecanary/gq/types" ) // Root is the root query type of the schema type Root struct { structschema.Meta `"Root Query Object" { "Says hello to the named person" hello(name: String): String "Says hello politely" politeHello(title: TitleInput!): String "Says hello politely to multiple people" politeHellos(titles: [TitleInput!]): [String!] "Returns a canned greeting" cannedHello(type: CannedHelloType = BRIEF): String "Returns an object that can say hello (interface)" greeter: Greeter "Returns an object that can say hello (union)" greeterByName(name: String!): HumanOrRobot }` RandomNumber types.Int `gq:";A random number. Selected by a fair roll of a die."` } func (r *Root) ResolveHello(name types.String) types.String { // if the input is nil, nothing to do if name.Nil() { return types.NilString() } return types.NewString(fmt.Sprintf("Hello %v", name.String())) } func (r *Root) ResolvePoliteHello(title *TitleInput) types.String { greeting := fmt.Sprintf("Hello %s", title.Title.String()) for _, e1 := range title.AdditionalTitles { for _, e2 := range e1 { greeting += " " + e2.String() } } if title.LastName.Nil() { greeting += " " + title.FirstName.String() } else { greeting += " " + title.LastName.String() if !title.FirstName.Nil() { greeting += ". May I call you " + title.FirstName.String() } } return types.NewString(greeting) } func (r *Root) ResolvePoliteHellos(titles []*TitleInput) []types.String { var result []types.String for _, ti := range titles { result = append(result, r.ResolvePoliteHello(ti)) } return result } func (r *Root) ResolveCannedHello(typ CannedHelloType) (types.String, error) { switch typ.String() { case "BRIEF": return types.NewString("Hi!"), nil case "LONG": return types.NewString("Good day"), nil case "ELABORATE": return types.NilString(), fmt.Errorf("Elaborate hellos are not yet implemented") } panic("Unreachable") } // Example of a resolver with injected arguments, returning an interface func (r *Root) ResolveGreeter(randomizer *randomizer) Greeter { switch randomizer.random() { case 0: return Greeter{ &Human{ Greeting: types.NewString("Salutations"), Name: types.NewString("Bob Smith"), }, } case 1: return Greeter{ &Robot{ Greeting: types.NewString("Beep Boop Bop"), ModelNumber: types.NewString("RX-123"), }, } default: return Greeter{ &Tree{ Greeting: types.NewString("..."), Height: types.NewInt(6), }, } } } func (r *Root) ResolveGreeterByName(name types.String) (HumanOrRobot, error) { if name.String() == "Bob Smith" { return HumanOrRobot{ &Human{ Greeting: types.NewString("Salutations"), Name: types.NewString("Bob Smith"), }, }, nil } else if name.String() == "RX-123" { return HumanOrRobot{ &Robot{ Greeting: types.NewString("Beep Boop Bop"), ModelNumber: types.NewString("RX-123"), }, }, nil } return HumanOrRobot{}, fmt.Errorf("Thing with name %s not found", name.String()) } // TitleInput is an example of an input object type TitleInput struct { structschema.InputObject `"A name and an optional title"` FirstName types.String LastName types.String Title types.String `gq:":String!;Title of the person (i.e. Dr/Mr/Mrs/Ms etc)"` AdditionalTitles [][]types.String } // If an input object has a Validate method matching this signature, it will // be invoked on input. func (t *TitleInput) Validate() error { if t.FirstName.Nil() && t.LastName.Nil() { return fmt.Errorf("Either first name or last name (or both) must be provided") } return nil } // CannedHelloType is an example of an enum type CannedHelloType struct { structschema.Enum `"The type of canned greeting to return" { "A brief greeting" BRIEF "A longer greeting" LONG "An elaborate greeting" ELABORATE }` } // Greeter is an example of an interface type Greeter struct { Interface interface { isGreeter() } `"A greeter is any object that knows how to provide a greeting" { greeting: String! }` } // HumanOrRobot is an example of a union type HumanOrRobot struct { Union interface { isHumanOrRobot() } } // A Human is an object type type Human struct { structschema.Meta `"A human represents a person" { mood: String "Returns what this person is currently working on" currentActivity: String }` Greeting types.String `gq:":String!"` Name types.String } func (Human) isGreeter() {} func (Human) isHumanOrRobot() {} // Resolves the mood of a human using a function style async return func (h *Human) ResolveMood() func() (types.String, error) { type moodResponse struct { err error mood types.String } c := make(chan moodResponse) go func() { // Here we would talk to the human an ask what mood they're in c <- moodResponse{ mood: types.NewString("Good"), } }() return func() (types.String, error) { result := <-c return result.mood, result.err } } // Resolves the current activity of a human using a channel style async return func (h *Human) ResolveCurrentActivity() (<-chan types.String, <-chan error) { c := make(chan types.String) e := make(chan error) go func() { // Here we would talk to the human an ask what they are working on e <- fmt.Errorf("Could not contact human") }() return c, e } // A Robot is an object type type Robot struct { Greeting types.String `gq:":String!"` ModelNumber types.String } func (Robot) isGreeter() {} func (Robot) isHumanOrRobot() {} // A Tree is an object type type Tree struct { Greeting types.String `gq:":String!"` Height types.Int } func (Tree) isGreeter() {} type randomizer struct { randomValue int } func (r *randomizer) random() int { return r.randomValue } func main() { builder := structschema.Builder{ Types: []interface{}{ &Root{}, &Human{}, &Robot{}, &Tree{}, }, } // Register a value to be injected to resolvers. In this case we use a deterministic random value // so our output is consistent. rando := &randomizer{randomValue: 2} builder.RegisterArgProvider("*structschema_test.randomizer", func(ctx context.Context) interface{} { return rando }) schema := builder.MustBuild("Root") io.WriteString(os.Stdout, "---- Generated schema ----\n") schema.WriteDefinition(os.Stdout) io.WriteString(os.Stdout, "\n---- End generated schema ----\n") q, err := query.PrepareQuery(`{ randomNumber hello(name: "Bob") politeHello(title: { title: "Mr" lastName: "Random" }) politeHellos(titles: [{ title: "Mr" lastName: "Random" }, { title: "Mrs" lastName: "Random" }]) politeHelloLong: politeHello(title: { title: "Professor" additionalTitles: [["Doctor"]] lastName: "Random" }) politeHelloError: politeHello(title: { title: "Mr" }) cannedHello cannedHelloLong: cannedHello(type: LONG) cannedHelloElaborate: cannedHello(type: ELABORATE) greeter { greeting } greeterByNameHuman: greeterByName(name: "Bob Smith") { ... on Human { name mood currentActivity } } greeterByNameDroid: greeterByName(name: "RX-123") { ... on Robot { modelNumber } } }`, "", schema) if err != nil { panic(err) } io.WriteString(os.Stdout, "---- Query ----\n") data := q.Execute(context.Background(), &Root{RandomNumber: types.NewInt(7)}, nil, nil) buf := &bytes.Buffer{} _ = json.Indent(buf, data, "", " ") os.Stdout.Write(buf.Bytes()) io.WriteString(os.Stdout, "\n---- End query ----\n") }
Output: ---- Generated schema ---- schema { query: Root "The type of canned greeting to return" enum CannedHelloType { "A brief greeting" BRIEF "An elaborate greeting" ELABORATE "A longer greeting" LONG } "A greeter is any object that knows how to provide a greeting" interface Greeter { greeting: String! } "A human represents a person" object Human implements & Greeter { "Returns what this person is currently working on" currentActivity: String greeting: String! mood: String name: String } union HumanOrRobot = | Human | Robot object Robot implements & Greeter { greeting: String! modelNumber: String } "Root Query Object" object Root { "Returns a canned greeting" cannedHello ( type: CannedHelloType = BRIEF ): String "Returns an object that can say hello (interface)" greeter: Greeter "Returns an object that can say hello (union)" greeterByName ( name: String! ): HumanOrRobot "Says hello to the named person" hello ( name: String ): String "Says hello politely" politeHello ( title: TitleInput! ): String "Says hello politely to multiple people" politeHellos ( titles: [TitleInput!] ): [String!] "A random number. Selected by a fair roll of a die." randomNumber: Int } "A name and an optional title" input TitleInput { additionalTitles: [[String]] firstName: String lastName: String "Title of the person (i.e. Dr/Mr/Mrs/Ms etc)" title: String! } object Tree implements & Greeter { greeting: String! height: Int } } ---- End generated schema ---- ---- Query ---- { "data": { "randomNumber": 7, "hello": "Hello Bob", "politeHello": "Hello Mr Random", "politeHellos": [ "Hello Mr Random", "Hello Mrs Random" ], "politeHelloLong": "Hello Professor Doctor Random", "politeHelloError": null, "cannedHello": "Hi!", "cannedHelloLong": "Good day", "cannedHelloElaborate": null, "greeter": { "greeting": "..." }, "greeterByNameHuman": { "name": "Bob Smith", "mood": "Good", "currentActivity": null }, "greeterByNameDroid": { "modelNumber": "RX-123" } }, "errors": [ { "message": "Error resolving argument title: Error in argument title: Either first name or last name (or both) must be provided", "path": [ "politeHelloError" ], "locations": [ { "line": 21, "column": 3 } ] }, { "message": "Elaborate hellos are not yet implemented", "path": [ "cannedHelloElaborate" ], "locations": [ { "line": 26, "column": 3 } ] }, { "message": "Could not contact human", "path": [ "greeterByNameHuman", "currentActivity" ], "locations": [ { "line": 35, "column": 5 } ] } ] } ---- End query ----
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ArgProvider ¶
An ArgProvider can provide a value to be passed to a resolver argument.
type Builder ¶
type Builder struct { // Types should be a slice of instances or pointer to instances of types // to build into the schema. Only roots need to be specified here, all types // directly reachable through fields of any type here will be added to the // schema. (e.g. if the query type refers to a union, you may need to // supply the union members here) Types []interface{} // contains filtered or unexported fields }
A Builder creates a schema.Schema from a set of annotated struct types. See readme for additional details and examples.
func (*Builder) RegisterArgProvider ¶
func (b *Builder) RegisterArgProvider(argSig string, provider ArgProvider)
RegisterArgProvider registers an ArgProvider for the given argument type
type Enum ¶
type Enum string
Enum is a marker field for enum types. It also serves as the container for an enum value.
Example usage:
type Episode struct { Enum `{ # Released in 1977. NEWHOPE # Released in 1980. EMPIRE # Released in 1983. JEDI }` }
type InputObject ¶
type InputObject struct{}
InputObject is a marker field for InputObject types. It serves much the same purpose as Meta for object types.
type Meta ¶
type Meta struct{}
Meta is a marker field for object types. It can be used to attach a GraphQL object type definition to an object. Note that such a definition is entirely optional, and many portions of the definition are relaxed where they can be derived from metadata.
type StructFieldMetadata ¶
type StructFieldMetadata struct { Name string Description string Directives ast.Directives ReflectField reflect.StructField }
func GetStructFieldMetadata ¶
func GetStructFieldMetadata(typ reflect.Type) ([]*StructFieldMetadata, error)
GetStructFieldMetadata loads GQL information about the fields of a struct from the annotations on the struct.
Information such as the resolved GQL type of the field is not available as that would require a proper builder to resolve the types.