quickgraph

package module
v0.7.4 Latest Latest
Warning

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

Go to latest
Published: Dec 24, 2023 License: MIT Imports: 15 Imported by: 2

README

Build status Go Report Card PkgGoDev

About

go-quickgraph presents a simple code-first library for creating a GraphQL service in Go. The intent is to have a simple way to create a GraphQL service without having to write a lot of boilerplate code. Since this is a code-first implementation, the library is also able to generate a GraphQL schema from the set-up GraphQL environment.

Design Goals

  • Minimal boilerplate code. Write Go functions and structs, and the library will take care of the rest.
  • Ability to process all valid GraphQL queries. This includes queries with fragments, aliases, variables, and directives.
  • Generation of a GraphQL schema from the set-up GraphQL environment.
  • Be as idiomatic as possible. This means using Go's native types and idioms as much as possible. If you are familiar with Go, you should be able to use this library without having to learn a lot of new concepts.
  • If we need to relax something in the processing of a GraphQL query, err on the side of preserving Go idioms, even if it means that we are not 100% compliant with the GraphQL specification.
  • Attempt to allow anything legal in GraphQL to be expressed in Go. Make the common things as easy as possible.
  • Be as fast as practical. Cache aggressively.

The last point is critical in thinking about how the library is built. It relies heavily on Go reflection to figure out what the interfaces are of the functions and structs. Certain things, like function parameter names, are not available at runtime, so we have to make some compromises. If, for instance, we have a function that takes two non-context arguments, we can process requests assuming that the order of the parameters in the request matches the order from the function. This is not 100% compliant with the GraphQL specification, but it is a reasonable compromise to make in order to preserve Go idioms and minimize boilerplate code.

Installation

go get github.com/gburgyan/go-quickgraph

Usage and Examples

The examples here, as well as many of the unit tests are based directly on the examples from the GraphQL documentation examples.

An example service that uses this can be found in the https://github.com/gburgyan/go-quickgraph-sample repo.

type Character struct {
	Id        string       `json:"id"`
	Name      string       `json:"name"`
	Friends   []*Character `json:"friends"`
	AppearsIn []Episode    `json:"appearsIn"`
}

type Episode string

// Go doesn't have a way reflecting these values, so we need to implement
// the EnumValues interface if we want to use them in our GraphQL schema.
// If not provided, the library will use the string representation of the
// enum values.
func (e Episode) EnumValues() []string {
    return []string{
        "NEWHOPE",
        "EMPIRE",
        "JEDI",
    }
}

func HeroProvider(ctx context.Context) Character {
	// Logic for resolving the Hero query
}

// How to manually run a query.
func RunExampleQuery(ctx context.Context) (string, err){
    g := Graphy{}
    g.RegisterQuery(ctx, "hero", HeroProvider)
    input := `
    {
      hero {
        name
      }
    }`
    return g.ProcessRequest(ctx, input, "") // The last parameter is the variable JSON (optional)
}

func main() {
	ctx := context.Background()

	graph := quickgraph.Graphy{}
	graph.RegisterQuery(ctx, "hero", HeroProvider)
	graph.EnableIntrospection(ctx)

	http.Handle("/graphql", graph.HttpHandler())
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

What's going on here?

When we define the Character type, it's simply a Go struct. We have decorated the struct's fields with the standard json tags that will be used to serialize the result to JSON. If those tags are omitted, the library will use the field names as the JSON keys.

There is also an enumeration for the Episode. Go doesn't natively support enums, so we have made a type that implements the StringEnumValues interface. This is used to generate the GraphQL schema as well as for soe validation during the processing of the request. This, as with the json tags, is also optional. The fallback is to just treat things as strings and leave the validation to the functions that process the request.

The HeroProvider function is the function that will be called when the hero query is processed. The function takes a context as a parameter, and returns a Character struct. This is the primary example of the "code-first" approach. We write the Go functions and structs, and the library takes care of the rest. There are ways to tweak the behavior of things, but the defaults should be sufficient for many use cases.

Finally, we set up a Graphy object and tell it about the function that will process the hero query. We then call the ProcessRequest function with the GraphQL query and the variable JSON (if any). The result is a JSON string that can be returned to the client.

In normal usage, we would initialize the Graphy object once, and then use it to process multiple requests. The Graphy object is thread-safe, so it can be used concurrently. It also caches all the reflection information that instructs it how to process the requests, so it is more efficient to reuse the same object. Additionally, it caches the parsed queries as well so if the same query is processed multiple times, it will be faster. This allows for the same query to be resused with different variable values.

Finally, Graphy provides a default HTTP handler that works with the native Go HTTP server. It allows for both schema output if it's called with a GET request, and a POST will execute the query.

To enable schema generation with the default HTTP server, you must also enable introspection:

graph.EnableIntrospection(ctx)

This is to ensure that if you don't want the be able to serve up the introspection schema, you won't get schema generation either. These are enabled together as they have the potential to "leak" internal information.

Theory of Operation

Initialization of the Graphy Object

As the above example illustrates, the first step is to create a Graphy object. The intent is that an instance of this object is set up at service initialization time, and then used to process requests. The Graphy object is thread-safe, so it can be used concurrently.

The process is to add all the functions that can be used to service queries and mutations. The library will use reflection to figure out what the function signatures are, and will use that information to process the requests. The library will also use reflection to figure out what the types are of the parameters and return values, and will use that information to process the requests and generate the GraphQL schema.

Functions are simple Go funcs that handle the processing. There are two broad "flavors" of functions: struct-based and anonymous. Since Go doesn't permit reflection of the parameters of functions we a few options. If the function takes one non-Context struct parameter, it will be treated as struct-based. The fields of the struct will be used to figure out the function signature. Otherwise, the function is an anonymous function. During the addition of a function, the ordered listing of parameter names can be passed. This will be covered in more detail further in this README.

Once all the functions are added, the Graphy object is ready to be used.

Processing of a Request

Internally, a request is processed in four primary phases:

Query Processing:

The inbound query is parsed into a RequestStub object. Since a request can have variables, the determination of the variable types can be done once, then cached. This is important because in complex cases, this can be an expensive operation as both the input and potential outputs must be considered.

Variable Processing

If there are variables to be processed, they are unmarshalled using JSON. The result objects are then mapped into the variables that have been defined by the preceding step. Variables can represent both simple scalar types and more complex object graphs.

Function Execution

Once the variables have been defined, the query or mutator functions can be called. This will call a function that was initially registered with the Graphy instance.

Output Generation

The most complex part of the overall request processing is the generation of the result object graph. All aspects of the standard GraphQL query language are supported. See the section on the type systems for more information about how this operates.

Another aspect that GraphQL supports is operations that function on the returned data. This library models that as receiver functions on results variables:


func (c *Character) FriendsConnection(first int) *FriendsConnection {
	// Processing goes here...
    return result
}

type FriendsConnection struct {
    TotalCount int               `json:"totalCount"`
    Edges      []*ConnectionEdge `json:"edges"`
}

This, in addition to the earlier example, will expose a function on the result object that is represented by a schema such as:

type Character {
	appearsIn: [episode!]!
	friends: [Character]!
	FriendsConnection(arg1: Int!): FriendsConnection
	id: String!
	name: String!
}

There are several important things here:

  • All public fields of the Character struct are reflected in the schema.
  • The json tags are respected for the field naming
  • The FriendsConnection receiver function is an anonymous function.

There is a further discussion on schemata later on.

Error handling

There are two general places where errors can occur: setup and runtime. During setup, the library will generally panic as this is something that should fail fast and indicates a structural problem with the program itself. At runtime, there should be no way that the system can panic.

When calling result, err := g.ProcessRequest(ctx, input, "{variables...}) the result should contain something that can be returned to a GraphQL client in all cases. The errors, if any, are formatted the way that a GraphQL client would expect. So, for instance:

{
  "data": {},
  "errors": [
    {
      "message": "error getting call parameters for function hero: invalid enum value INVALID",
      "locations": [
        {
          "line": 3,
          "column": 8
        }
      ],
      "path": [
        "hero"
      ]
    }
  ]
}

The error is also returned by the function itself and should be able to be handled normally.

Functions

Functions are used in two ways in the processing of a Graphy request:

  • Handling the primary query processing
  • Providing processing while rendering the results from the output from the primary function

Any time a request is processed, it gets mapped to the function that is registered for that name. This is done with one of the Register* functions on the Graphy object.

In an effort to make it simple for developers to create these functions, there are several ways to add functions and Graphy that each have some advantages and disadvantages.

In all cases, if there is a context.Context parameter, that will be passed into the function.

Anonymous Functions

Anonymous functions look like typical Golang functions and simply take a normal parameter list.

Given this simple function:

func GetCourses(ctx context.Context, categories []*string) []*Course

You can register the processor with:

g := Graphy{}
g.RegisterQuery(ctx, "courses", GetCourses)

The downside of this approach is based on a limitation in reflection in Golang: there is no way to get the names of the parameters. In this case Graphy will run assuming the parameters are positional. This is a variance from orthodox GraphQL behavior as it expects named parameters. If a schema is generated from the Graphy, these will be rendered as arg1, arg2, etc. Those are purely placeholders for the schema and the processing of the request will still be treated positionally; the names of the passed parameters are ignored.

If the named parameters are needed, you can do that via:

g := Graphy{}
g.RegisterQuery(ctx, "courses", GetCourses, "categories")

Once you tell Graphy the names of the paramters to expect, it can function with named parameters as is typical for GraphQL.

Finally, you can also register the processor with the most flexible function:

g := Graphy{}
g.RegisterFunction(ctx, graphy.FunctionDefinition{
	Name: "courses",
	Function: GetCourses,
	ParameterNames: {"categories"},
	Mode: graphy.ModeQuery,
})

This is also how you can register a mutation instead of the default query.

A function may also take zero non-Context parameters, in which case it simply gets run.

Struct Functions

The alternate way that a function can be defined is by passing in a struct as the parameter. The structure must be passed as value. In addition to the struct parameter, it may also, optionally, have a context.Context parameter.

For example:

type CourseInput struct {
	Categories []string
}

func GetCourses(ctx context.Context, in CourseInput) []*Course
{
	// Implementation
}

g := Graphy{}
g.RegisterQuery(ctx, "courses", GetCourses)

Since reflection can be used to get the names of the members of the structure, that information will be used to get the names of the parameters that will be exposed for the function.

The same RegisterFunction function can also be used as described above to define additional aspects of the function, such as if it's a mutator.

Return Values

Regardless of how the function is defined, it is required to return a struct, a pointer to a struct, or a slice of either. It may optionally return an error as well. The returned value will be used to populate the response to the GraphQL calls. The shape of the response object will be used to construct the schema of the Graphy in case that is used.

There is a special case where a function can return an any type. This is valid from a runtime perspective as the type of the object can be determined at runtime, but it precludes schema generation for the result as the type of the result cannot be determined by the signature of the function.

Output Functions

When calling a function to service a request, that function returns the value that is processed into the response -- that part is obvious. Another feature is that those objects can have functions on them as well. This plays into the overall Graph functionality that is exposed by Graphy. These receiver functions follow the same pattern as above.

Additionally, they get transformed into schemas exactly as expected. If a receiver function takes nothing (or only a context.Context object), then it gets exposed as a field. If the field is referenced, then the function is invoked and the output generation continues. If the function takes parameters, then it's exposed as a function with parameters both for the request as well as the schema:

type Human struct {
	Character
	HeightMeters float64 `json:"HeightMeters"`
}

func (h *Human) Height(units *string) float64 {
	if units == nil {
		return h.HeightMeters
	}
	if *units == "FOOT" {
		return roundToPrecision(h.HeightMeters*3.28084, 7)
	}
	return h.HeightMeters
}

Since the Height function takes a pointer to a string as a parameter, it's treated as optional.

In this case both of the following queries will work:

{
  Human(id: "1000") {
    name
    height
  }
}
{
    Human(id: "1000") {
        name
        height(unit: FOOT)
    }
}

Function Parameters

Regardless of how the function is invoked, the parameters for the function come from either the base query itself or variables that are passed in along with the query. Graphy supports both scalar types, as well as more complex types including complex, and even nested, structures, as well as slices of those objects.

Type System

The way Graphy works with types is intended to be as transparent to the user as possible. The normal types, scalars, structs, and slices all work as expected. This applies to both input in the form of parameters being sent in to functions and the results of those functions.

Maps, presently, are not supported.

Enums

Go doesn't have a native way of representing enumerations in a way that is open to be used for reflection. To get around this, Graphy provides a few different ways of exposing enumerations.

Simple strings
EnumUnmarshaler interface

If you need to have function inputs that map to specific non-string inputs, you can implement the EnumUnmarshaler interface:

// EnumUnmarshaler provides an interface for types that can unmarshal
// a string representation into their enumerated type. This is useful
// for types that need to convert a string, typically from external sources
// like JSON or XML, into a specific enumerated type in Go.
//
// UnmarshalString should return the appropriate enumerated value for the
// given input string, or an error if the input is not valid for the enumeration.
type EnumUnmarshaler interface {
	UnmarshalString(input string) (interface{}, error)
}

UnmarshalString is called with the supplied identifier, and is responsible for converting that into whatever type is needed. If the identifier cannot be converted, simply return an error.

The downside of this is that there is no way to communicate to the schema what are the valid values for the enumeration.

Example:

type MyEnum string

const (
	EnumVal1 MyEnum = "EnumVal1"
	EnumVal2 MyEnum = "EnumVal2"
	EnumVal3 MyEnum = "EnumVal3"
)

func (e *MyEnum) UnmarshalString(input string) (interface{}, error) {
	switch input {
	case "EnumVal1":
		return EnumVal1, nil
	case "EnumVal2":
		return EnumVal2, nil
	case "EnumVal3":
		return EnumVal3, nil
	default:
		return nil, fmt.Errorf("invalid enum value %s", input)
	}
}

In this case, the enum type is a string, but that's not a requirement.

StringEnumValues

Another way of dealing with enumerations is to treat them as strings, but with a layer of validation applied. You can implement the StringEnumValues interface to say what are the valid values for a given type.

// StringEnumValues provides an interface for types that can return
// a list of valid string representations for their enumeration.
// This can be useful in scenarios like validation or auto-generation
// of documentation where a list of valid enum values is required.
//
// EnumValues should return a slice of strings representing the valid values
// for the enumeration.
type StringEnumValues interface {
	EnumValues() []string
}

These strings are used both for input validation and schema generation. The limitation is that the inputs and outputs that use this type need to be of a string type.

An example from the tests:

type episode string

func (e episode) EnumValues() []string {
	return []string{
		"NEWHOPE",
		"EMPIRE",
		"JEDI",
	}
}

Interfaces

Interfaces, in this case, are referring to how GraphQL uses the term "interface." The way that a type can implement an interface, as well as select the output filtering based on the type of object that is being returned.

The way this is modeled in this library is by using anonymous fields on a struct type to show an "is-a" relationship.

So, for instance:

type Character struct {
	Id        string       `json:"id"`
	Name      string       `json:"name"`
	Friends   []*Character `json:"friends"`
	AppearsIn []episode    `json:"appearsIn"`
}

type Human struct {
	Character
	HeightMeters float64 `json:"HeightMeters"`
}

In this case, a Human is a subtype of Character. The schema generated from this is:

type Human implements Character {
	FriendsConnection(arg1: Int!): FriendsConnection
	HeightMeters: Float!
}

type Character {
	appearsIn: [episode!]!
	friends: [Character]!
	id: String!
	name: String!
}

Unions

Another aspect of GraphQL that doesn't cleanly map to Go is the concept of unions -- where a value can be one of several distinct types.

This is handled by one of two ways: implicit and explicit unions.

Implicit Unions

Implicit unions are created by functions that return multiple pointers to results. Of course only one of those result pointers can be non-nil. For example:

type resultA struct {
	OutStringA string
}
type resultB struct {
	OutStringB string
}
func Implicit(ctx context.Context, selector string) (*resultA, *resultB, error) {
	// implementation
}

This will generate a schema that looks like:

type Query {
	Implicit(arg1: String!): ImplicitResultUnion!
}

union ImplicitResultUnion = resultA | resultB

type resultA {
	OutStringA: String!
}

type resultB {
	OutStringB: String!
}

If you need a custom-named enum, you can register the function like:

g.RegisterFunction(ctx, FunctionDefinition{
	Name:            "CustomResultFunc",
	Function:        function,
	ReturnUnionName: "MyUnion",
})

In which case the name of the union is MyUnion.

Explicit Unions

You can also name a type ending with the string Union and that type will be treated as a union. The members of that type must all be pointers. The result of the evaluation of the union must have a single non-nil value, and that is the implied type of the result.

Schema Generation

Once a graphy is set up with all the query and mutation handlers, you can call:

schema, err := g.SchemaDefinition(ctx)

This will create a GraphQL schema that represents the state of the graphy object. Explore the schema_type_test.go test file for more examples of generated schemata.

Introspection

By calling graph.EnableIntrospection(ctx) you also enable the introspection queries. Internally this is handled by the schema generation subsystem. This also turns on implicit schema generation by the built-in HTTP handler.

Limitations

  • If there are multiple types with the same name, but from different packages, the results will not be valid.

Caching

Caching is an optional feature of the graph processing. To enable it, simply set the RequestCache on the Graphy object. The cache is an implementation of the GraphRequestCache interface. If this is not set, the graphy functionality will not cache anything.

The cache is used to cache the result of parsing the request. This is a RequestStub as well as any errors that were present in parsing the errors. The request stub contains everything that was prepared to run the request except the variables that were passed in. This process involves a lot of reflection, so this is a comparatively expensive operation. By caching this processing, we gain a roughly 10x speedup.

We cache errors as well because a request that can't be fulfilled by the Graphy library will continue to be an error even if it submitted again -- there is no reason to reprocess the request to simply get back to the answer of error.

The internals of the RequestStub is only in-memory and not externally serializable.

Example implementation

Using a simple cache library github.com/patrickmn/go-cache, here's a simple implementation:

type SimpleGraphRequestCache struct {
	cache *cache.Cache
}

type simpleGraphRequestCacheEntry struct {
	request string
	stub    *RequestStub
	err     error
}

func (d *SimpleGraphRequestCache) SetRequestStub(ctx context.Context, request string, stub *RequestStub, err error) {
	setErr := d.cache.Add(request, &simpleGraphRequestCacheEntry{
		request: request,
		stub:    stub,
		err:     err,
	}, time.Hour)
	if setErr != nil {
		// Log this error, but don't return it.
		// Potentially disable the cache if this recurs continuously.
	}
}

func (d *SimpleGraphRequestCache) GetRequestStub(ctx context.Context, request string) (*RequestStub, error) {
	value, found := d.cache.Get(request)
	if !found {
		return nil, nil
	}
	entry, ok := value.(*simpleGraphRequestCacheEntry)
	if !ok {
		return nil, nil
	}
	return entry.stub, entry.err
}

Since each unique request, independent of the variables, can be cached, it's important to have a working eviction policy to prevent a denial of service attack from exhausting memory.

Internal caching

Internally Graphy will cache much of the results of reflection operations. These relate to the types that are used for input and output. Since these have a one-to-one relationship to the internal types of the running system, they are cached by Graphy for the lifetime of the object; it can't grow out of bounds and cannot be subject to a denial of service attack.

Dealing with unknown commands

A frequent requirement is to implement a strangler pattern to start taking requests for things that can be processed, but to forward requests that can't be processed to another service. This is enabled by the processing pipeline by returning a UnknownCommandError. Since the processing of the request can be cached, this can be a fail-fast scenario so that the request could be forwarded to another service for processing.

Benchmarks

Given this relatively complex query:

mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
  createReview(episode: $ep, review: $review) {
    stars
    commentary
  }
}

with this variable JSON:

{
  "ep": "JEDI",
  "review": {
    "stars": 5,
    "commentary": "This is a great movie!"
  }
}

with caching enabled, the framework overhead is less than 4.8µs on an Apple M1 Pro processor. This includes parsing the variable JSON, calling the function for CreateReviewForEpisode, and processing the output. The vast majority of the overhead, roughly 75% of the time, isn't the library itself, but rather the unmarshalling of the variable JSON as well as marshaling the result to be returned.

See the benchmark_test.go benchmarks for more tests and to evaluate this on your hardware.

While potentially caching the variable JSON would be possible, the decision was made that it's likely not worthwhile as the variables are what are most likely to change between requests negating any benefits of caching.

General Limitations

Interfaces

While interfaces are handled appropriately when processing responses, due to limitations in the Go reflection system there is no way to find all types that implement that interface. Because of this, when looking at the generated schemata or using the introspection system, there may be implementing types that are not known.

Validation of Input

Queries ignore the type specifiers on the input. The types are always inferred from the actual function inputs.

Documentation

Index

Constants

View Source
const (
	IntrospectionKindScalar      __TypeKind = "SCALAR"
	IntrospectionKindObject      __TypeKind = "OBJECT"
	IntrospectionKindInterface   __TypeKind = "INTERFACE"
	IntrospectionKindUnion       __TypeKind = "UNION"
	IntrospectionKindEnum        __TypeKind = "ENUM"
	IntrospectionKindInputObject __TypeKind = "INPUT_OBJECT"
	IntrospectionKindList        __TypeKind = "LIST"
	IntrospectionKindNonNull     __TypeKind = "NON_NULL"
)
View Source
const (
	FieldTypeField fieldType = iota
	FieldTypeGraphFunction
)

Variables

This section is empty.

Functions

func AugmentGraphError

func AugmentGraphError(err error, message string, pos lexer.Position, paths ...string) error

AugmentGraphError wraps the provided error with additional context, creating or augmenting a GraphError structure. This function serves to enrich errors with Graph-specific details such as file position, path, and a custom message.

The function primarily handles two cases:

  1. If the passed error is already a GraphError, it augments the existing GraphError with the provided context without losing any existing data.
  2. If the passed error is not a GraphError, it wraps it within a new GraphError, setting the provided context.

Parameters:

  • err: The error to be wrapped or augmented. It can be a regular error or a GraphError.
  • message: A custom message to be set on the GraphError. If a GraphError is passed and it already contains a message, this parameter is ignored.
  • pos: A lexer.Position structure indicating where in the source the error occurred. If a GraphError is passed and it already contains location data, this parameter is ignored.
  • paths: A variadic slice of strings indicating the path in the graph where the error occurred. These are prepended to any existing paths in a GraphError.

Returns: - A GraphError containing the augmented or wrapped error details.

Types

type EnumUnmarshaler

type EnumUnmarshaler interface {
	UnmarshalString(input string) (interface{}, error)
}

EnumUnmarshaler provides an interface for types that can unmarshal a string representation into their enumerated type. This is useful for types that need to convert a string, typically from external sources like JSON or XML, into a specific enumerated type in Go.

UnmarshalString should return the appropriate enumerated value for the given input string, or an error if the input is not valid for the enumeration.

type EnumValue added in v0.7.1

type EnumValue struct {
	Name              string
	Description       string
	IsDeprecated      bool
	DeprecationReason string
}

type ErrorLocation

type ErrorLocation struct {
	Line   int `json:"line"`
	Column int `json:"column"`
}

ErrorLocation provides details about where in the source a particular error occurred.

Fields: - Line: The line number of the error. - Column: The column number of the error.

func (ErrorLocation) String

func (e ErrorLocation) String() string

type FunctionDefinition

type FunctionDefinition struct {
	// Name is the name of the function.
	// This is used to map the function to the GraphQL query.
	Name string

	// Function is the function to call.
	Function any

	// ParameterNames is a list of parameter names to use for the function. Since reflection
	// doesn't provide parameter names, this is needed to map the parameters to the function.
	// This is used when the function can have anonymous parameters otherwise.
	ParameterNames []string

	// ReturnAnyOverride is a list of types that may be returned as `any` when returned from
	// the function. This is a function-specific override to the global `any` types that are
	// on the base Graphy instance.
	ReturnAnyOverride []any

	// Mode controls how the function is to be run. Mutators are functions that change the
	// state of the system. They will be called sequentially and in the order they are referred
	// to in the query. Regular queries are functions that do not change the state of the
	// system. They will be called in parallel.
	Mode GraphFunctionMode

	// ReturnEnumName is used to provide a custom name for implicit return unions. If this is
	// not set the default name is the name of the function followed by "ResultUnion".
	ReturnUnionName string

	// Description is used to provide a description for the function. This will be used in the
	// schema.
	Description *string

	// DeprecatedReason is used to mark a function as deprecated. This will cause the function to
	// be marked as deprecated in the schema.
	DeprecatedReason *string
}

type GraphError

type GraphError struct {
	Message    string            `json:"message"`
	Locations  []ErrorLocation   `json:"locations,omitempty"`
	Path       []string          `json:"path,omitempty"`
	Extensions map[string]string `json:"extensions,omitempty"`
	InnerError error             `json:"-"`
}

GraphError represents an error that occurs within a graph structure. It provides a structured way to express errors with added context, such as their location in the source and any associated path.

Fields: - Message: The main error message. - Locations: A slice of ErrorLocation structs that detail where in the source the error occurred. - Path: Represents the path in the graph where the error occurred. - Extensions: A map containing additional error information not part of the standard fields. - InnerError: An underlying error that might have caused this GraphError. It is not serialized to JSON.

func NewGraphError

func NewGraphError(message string, pos lexer.Position, paths ...string) GraphError

NewGraphError creates a new GraphError with the provided message, position, and path. It uses the position to create an ErrorLocation structure and adds it to the Locations field.

func (*GraphError) AddExtension

func (e *GraphError) AddExtension(key string, value string)

AddExtension adds a key-value pair to the Extensions field of a GraphError. Extensions in a GraphError provide a way to include additional error information that is not part of the standard error fields.

If the Extensions map is nil, it will be initialized before adding the key-value pair.

Parameters: - key: The key for the extension. It represents the name or identifier for the additional data. - value: The value associated with the key. It provides extra context or data for the error.

func (GraphError) Error

func (e GraphError) Error() string

Implement the error interface

func (GraphError) MarshalJSON

func (e GraphError) MarshalJSON() ([]byte, error)

MarshalJSON implements the json.Marshaler interface for GraphError. This allows us to format the error in the way that GraphQL expects.

func (GraphError) Unwrap

func (e GraphError) Unwrap() error

type GraphFunctionMode

type GraphFunctionMode int
const (
	ModeQuery GraphFunctionMode = iota
	ModeMutation
)

type GraphFunctionParamType

type GraphFunctionParamType int
const (
	NamedParamsStruct GraphFunctionParamType = iota
	NamedParamsInline
	AnonymousParamsInline
)

type GraphHttpHandler added in v0.5.0

type GraphHttpHandler struct {
	// contains filtered or unexported fields
}

func (GraphHttpHandler) ServeHTTP added in v0.5.0

func (g GraphHttpHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request)

type GraphRequestCache

type GraphRequestCache interface {
	// GetRequestStub returns the request stub for a request. It should return nil if the request
	// is not cached. The error can either be the cached error or an error indicating a cache error.
	// In case the request is not cached, the returned *RequestStub should be nil.
	GetRequestStub(ctx context.Context, request string) (*RequestStub, error)

	// SetRequestStub sets the request stub for a request.
	SetRequestStub(ctx context.Context, request string, stub *RequestStub, err error)
}

GraphRequestCache represents an interface for caching request stubs associated with graph requests. Implementations of this interface provide mechanisms to store and retrieve `RequestStub`s, allowing for optimizations and reduced processing times in graph operations. Note that the `RequestStub` is an internal representation of a graph request, and is not intended to be used directly by consumers. It is not serializable to JSON and needs to be kept in memory.

type GraphTypeExtension added in v0.7.1

type GraphTypeExtension interface {
	GraphTypeExtension() GraphTypeInfo
}

type GraphTypeInfo added in v0.7.1

type GraphTypeInfo struct {
	// Name is the name of the type.
	Name string

	// Description is the description of the type.
	Description string

	// Deprecated is the deprecation status of the type.
	Deprecated string

	// Function overrides for the type.
	FunctionDefinitions []FunctionDefinition
}

type Graphy

type Graphy struct {
	RequestCache GraphRequestCache

	EnableTiming bool
	// contains filtered or unexported fields
}

Graphy is the main entry point for the go-quickgraph library. This holds all the registered functions and types and provides methods for executing requests. It is safe to use concurrently once it has been initialized -- there is no guarantee that the initialization is thread-safe.

The zero value for Graphy is safe to use.

The RequestCache is optional, but it can be used to cache the results of parsing requests. This can be useful if you are using the library in a server environment and want to cache the results of parsing requests to speed things up. Refer to GraphRequestCache for more information.

func (*Graphy) EnableIntrospection added in v0.7.0

func (g *Graphy) EnableIntrospection(ctx context.Context)

func (*Graphy) HttpHandler added in v0.5.0

func (g *Graphy) HttpHandler() http.Handler

func (*Graphy) ProcessRequest

func (g *Graphy) ProcessRequest(ctx context.Context, request string, variableJson string) (string, error)

func (*Graphy) RegisterAnyType

func (g *Graphy) RegisterAnyType(ctx context.Context, types ...any)

RegisterAnyType registers a type that is potentially used as a return type for a function that returns `any`. This isn't critical to use all cases, but it can be needed for results that contain functions that can be called. Without this, those functions would not be found -- this it needed to infer the types of parameters in cases those are fulfilled with variables.

func (*Graphy) RegisterFunction

func (g *Graphy) RegisterFunction(ctx context.Context, def FunctionDefinition)

RegisterFunction is similar to both RegisterQuery and RegisterMutation, but it allows the caller to specify additional parameters that are less commonly used. See the FunctionDefinition documentation for more information.

func (*Graphy) RegisterMutation added in v0.4.0

func (g *Graphy) RegisterMutation(ctx context.Context, name string, f any, names ...string)

RegisterMutation registers a function as a mutator.

The function must return a valid result value and may return an error. If the function returns multiple values, they must be pointers and the result will be an implicit union type.

If the names are specified, they must match the non-context parameter count of the function. If the names are not specified, then the parameters are dealt with as either anonymous parameters or as a single parameter that is a struct. If the function has a single parameter that is a struct, then the names of the struct fields are used as the parameter names.

func (*Graphy) RegisterQuery added in v0.4.0

func (g *Graphy) RegisterQuery(ctx context.Context, name string, f any, names ...string)

RegisterQuery registers a function as a query.

The function must return a valid result value and may return an error. If the function returns multiple values, they must be pointers and the result will be an implicit union type.

If the names are specified, they must match the non-context parameter count of the function. If the names are not specified, then the parameters are dealt with as either anonymous parameters or as a single parameter that is a struct. If the function has a single parameter that is a struct, then the names of the struct fields are used as the parameter names.

func (*Graphy) RegisterTypes added in v0.7.1

func (g *Graphy) RegisterTypes(ctx context.Context, types ...any)

RegisterTypes is a method on the Graphy struct that registers types that implement interfaces. This is useful for discovering types that implement certain interfaces. The method takes in a context and a variadic parameter of types (of any kind). It iterates over the provided types and performs a type lookup for each type.

Parameters: - ctx: The context within which the method operates. - types: A variadic parameter that represents instances ot types to be registered.

Usage: g := &Graphy{} g.RegisterTypes(context.Background(), Type1{}, Type2{}, Type3{})

func (*Graphy) SchemaDefinition

func (g *Graphy) SchemaDefinition(ctx context.Context) string

type RequestStub

type RequestStub struct {
	// contains filtered or unexported fields
}

RequestStub represents a stub of a GraphQL-like request. It contains the Graphy instance, the mode of the request (Query or Mutation), the commands to execute, and the variables used in the request.

func (*RequestStub) Name added in v0.7.2

func (r *RequestStub) Name() string

type RequestType

type RequestType int

RequestType is an enumeration of the types of requests. It can be a Query or a Mutation.

const (
	RequestQuery RequestType = iota
	RequestMutation
)

type StringEnumValues

type StringEnumValues interface {
	EnumValues() []EnumValue
}

StringEnumValues provides an interface for types that can return a list of valid string representations for their enumeration. This can be useful in scenarios like validation or auto-generation of documentation where a list of valid enum values is required.

EnumValues should return a slice of strings representing the valid values for the enumeration.

type TypeKind added in v0.3.0

type TypeKind int
const (
	TypeInput TypeKind = iota
	TypeOutput
)

type UnknownCommandError

type UnknownCommandError struct {
	GraphError
	Commands []string
}

UnknownCommandError is a specific type of GraphError that occurs when an unknown command is encountered. It embeds the GraphError and adds a Commands field which contains a list of commands that were unrecognized.

func (UnknownCommandError) Unwrap

func (e UnknownCommandError) Unwrap() error

Jump to

Keyboard shortcuts

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