structschema

package
v0.0.0-...-02cfa7b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2023 License: Apache-2.0 Imports: 10 Imported by: 2

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

type ArgProvider func(context.Context) interface{}

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) Build

func (b *Builder) Build(queryTypeName string) (*schema.Schema, error)

Build creates a schema from this builder

func (*Builder) MustBuild

func (b *Builder) MustBuild(queryTypeName string) *schema.Schema

MustBuild is the same as Build but panics on error

func (*Builder) RegisterArgProvider

func (b *Builder) RegisterArgProvider(argSig string, provider ArgProvider)

RegisterArgProvider registers an ArgProvider for the given argument type

func (*Builder) SchemaBuilder

func (b *Builder) SchemaBuilder() (*schema.Builder, error)

SchemaBuilder creates a schema builder from the supplied types

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
	}`
}

func (Enum) Nil

func (e Enum) Nil() bool

Nil whether this enum value represents nil

func (Enum) String

func (e Enum) String() string

type EnumValue

type EnumValue interface {
	Nil() bool
	String() string
	// contains filtered or unexported methods
}

EnumValue is the interface implemented by enums

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.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL