octoql

package module
v0.0.1 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 13 Imported by: 0

README

Go Reference Test Status Contributor Covenant

octoql

octoql generates type-safe Go clients and typed test handlers for GitHub-shaped GraphQL APIs. It validates queries and mutations against a pinned schema, then generates Go types and helpers backed by a small root runtime package.

octoql is a standalone project derived from Khan/genqlient. See THIRD_PARTY_NOTICES.md for exact source pins and attribution, including the bounded gqltesthandler input used by typed test-handler generation.

Contents

Requirements and installation

octoql requires Go 1.26 or newer. Add the runtime and pin octoqlgen as a Go tool dependency in the module that owns the generated client:

go get github.com/willabides/octoql
go get -tool github.com/willabides/octoql/cmd/octoqlgen

Run the pinned tool with go tool octoqlgen. To install a standalone binary from a release archive with bindown:

bindown template-source add octoql https://github.com/WillAbides/octoql/releases/latest/download/bindown.yaml
bindown dependency add octoqlgen --source octoql

To install the standalone binary from source instead, use an explicit version or commit:

go install github.com/willabides/octoql/cmd/octoqlgen@<version-or-commit>

The Go tool dependency remains the recommended installation because the runtime and generator then resolve from the same module version.

Generate a client

Initialize a project:

go tool octoqlgen init

This creates octoqlgen.yaml and .octoql/.gitignore. It does not fetch a schema. The generated config uses the gitignored .octoql/schema.graphql path, graphql/**/*.graphql for operations, and internal/githubapi/generated.go for output.

Add the JSON Schema directive to octoqlgen.yaml for editor completion and validation, then configure a local or remote schema:

# yaml-language-server: $schema=https://raw.githubusercontent.com/WillAbides/octoql/main/octoqlgen.schema.yaml

schema:
  path: .octoql/schema.graphql
operations:
  - graphql/**/*.graphql
generated: internal/githubapi/generated.go

Repository checkouts can use # yaml-language-server: $schema=./octoqlgen.schema.yaml instead. All paths and globs are relative to octoqlgen.yaml. The committed octoqlgen.schema.yaml is the canonical structural schema, and the annotated docs/octoqlgen.yaml explains every option.

Create graphql/repository.graphql:

query GetRepository($owner: String!, $name: String!, $first: Int!) {
  repository(owner: $owner, name: $name) {
    nameWithOwner
    issues(first: $first) {
      nodes {
        number
        title
      }
    }
  }
}

Materialize or verify the configured schema, then generate:

go tool octoqlgen schema materialize
go tool octoqlgen generate

Generation performs the same schema verification or materialization before it writes code. Query and mutation operation names become generated helper names, so use an uppercase name when the helper must be exported. octoql does not support GraphQL subscriptions, and octoqlgen rejects subscription operations. Generation also refuses output paths that alias octoqlgen.yaml, the schema, or any expanded operation input. Operation manifests record source paths relative to the configuration directory so checked-in manifests stay portable.

operations may also select Go files. A string literal beginning with # @octoqlgen is parsed as an operation:

const getViewerQuery = `# @octoqlgen
query GetViewer {
  viewer {
    login
  }
}
`

Use the @octoqlgen comment quasi-directive for per-operation options such as pointer, omitempty, flatten, bind, and typename. See the directive reference. It is written in a comment because server-defined GraphQL directives cannot configure the client generator.

Schema sources and updates

schema.path is always the schema used for generation. Keep it in the gitignored .octoql directory when the source is remote. A local schema needs no source:

schema:
  path: schema/github.graphql

For a local schema, sha256 is optional. When present, materialization verifies it. Remote sources require a SHA-256 digest and, for GitHub sources, a full commit SHA. Configure exactly one source variant.

GitHub Docs

Use fpt, ghec, or a GHES version such as ghes-3.17:

schema:
  path: .octoql/schema.graphql
  sha256: "<64-character-schema-sha256>"
  source:
    github_docs:
      version: fpt
      revision: "<full-github-docs-commit-sha>"
GitHub repository or GHES

Pin a schema file in a GitHub repository. Add host for GitHub Enterprise Server:

schema:
  path: .octoql/schema.graphql
  sha256: "<64-character-schema-sha256>"
  source:
    github_repository:
      repository: octo-org/graphql-schema
      revision: "<full-commit-sha>"
      path: schema/github.graphql
      host: github.example.com

Omit host for github.com.

Immutable URL
schema:
  path: .octoql/schema.graphql
  sha256: "<64-character-schema-sha256>"
  source:
    url: https://schemas.example.com/github/<revision>/schema.graphql

Prefer a URL whose content is immutable. Materialization refuses bytes that do not match the configured digest and validates the GraphQL SDL before writing schema.path.

Authentication, verification, and updates

For GitHub Docs and repository sources, authentication is discovered in this order:

  1. GH_TOKEN
  2. GITHUB_TOKEN
  3. gh auth token --hostname <host>

GitHub GraphQL requires authentication, including for public repositories. Materialization and updates fail with guidance when none of these token sources is available.

schema materialize verifies an existing file or fetches a missing remote file. It never changes octoqlgen.yaml:

go tool octoqlgen schema materialize

schema update fetches and validates the current remote source. It atomically publishes the materialized schema, then atomically updates the configuration pin: sha256 for every remote source and the GitHub revision for GitHub-backed sources:

The schema/config pair is not published atomically. schema update takes no lock and maintains no journal, rollback, or automatic recovery. Concurrent schema commands are unsupported, and a failure after schema publication, such as a failed config write, can leave an incoherent schema/config pair. Simultaneous invocations targeting the same schema or config can be last-writer-wins, leave an incoherent pair even when all commands succeed, or result in a verification/materialization failure. After concurrent activity or a failure after schema publication, fix the underlying write failure or other error, then run one schema update serially to establish a coherent schema/config pair. Then rerun any command that failed.

go tool octoqlgen schema update
git diff -- octoqlgen.yaml
go tool octoqlgen generate

The .octoql schema normally remains ignored while the reviewed pin in octoqlgen.yaml is committed. Use --config PATH with materialize, update, or generate when the config has another name or location.

Call the generated client

Configure GitHub bearer authentication directly on the client:

client := octoql.NewClient("https://api.github.com/graphql", nil)
err := client.SetBearerToken(os.Getenv("GITHUB_TOKEN"))
if err != nil {
	return err
}

response, err := githubapi.GetRepository(
	ctx,
	client,
	githubapi.GetRepositoryVariables{
		Owner: "octo-org",
		Name:  "octo-repo",
		First: 10,
	},
)
if err != nil {
	return err
}
fmt.Println(response.Repository.NameWithOwner)

Pass a different endpoint to octoql.NewClient for GHES, a proxy, or an httptest.Server. Pass nil as the HTTP client to use http.DefaultClient.

SetBearerToken accepts tokens containing letters, digits, -, ., _, ~, +, or /, with optional trailing = padding. For basic authentication or another authentication scheme, configure the http.Client or http.RoundTripper passed to NewClient.

Runtime responses and errors

Generated helpers return a pointer to the concrete operation response and an error. GraphQL data is available directly:

response, err := githubapi.GetRepository(ctx, client, githubapi.GetRepositoryVariables{
	Owner: owner,
	Name:  name,
	First: 10,
})
if err == nil {
	fmt.Println(response.Repository.NameWithOwner)
}

[!IMPORTANT] A generated helper returns a nil response whenever its error is non-nil, even when GitHub returned decodable, non-null partial GraphQL data. Do not expect the regular response result to contain partial data. Instead, use errors.AsType to extract the operation-specific partial-data error:

partialErr, ok := errors.AsType[*githubapi.GetRepositoryPartialDataError](err)
if ok {
	fmt.Printf("partial repository: %+v\n", partialErr.PartialData().Repository)
}

Each *octoql.Error retains Type, Message, Path, Locations, and its own Extensions. Error.Type is an open string type so new GitHub values remain available without a runtime update.

Generated helpers follow the usual Go convention: the response is nil whenever the error is non-nil. When GitHub returns decodable, non-null partial data alongside GraphQL errors, octoql stores that data in the error for explicit extraction. A data: null response has no partial-data facet:

response, err := githubapi.GetRepository(ctx, client, githubapi.GetRepositoryVariables{
	Owner: owner,
	Name:  name,
	First: 10,
})
if err != nil {
	// response is always nil here.
	partialErr, ok := errors.AsType[*githubapi.GetRepositoryPartialDataError](err)
	if ok {
		fmt.Printf("partial repository: %+v\n", partialErr.PartialData().Repository)
	}
}

graphqlErrors, ok := errors.AsType[octoql.Errors](err)
if ok {
	for _, graphqlError := range graphqlErrors {
		fmt.Printf("%s: %s at %s\n",
			graphqlError.Type,
			graphqlError.Message,
			graphqlError.Path,
		)
	}
}

responseError, ok := errors.AsType[*octoql.ResponseError](err)
if ok {
	fmt.Printf("status=%d request_id=%s\n",
		responseError.StatusCode,
		responseError.RequestID,
	)
}

Every failure after receiving an HTTP response includes *octoql.ResponseError, including HTTP-200 GraphQL, read, close, protocol, and decode failures. It carries the status and X-GitHub-Request-ID. Client buffers and decodes at most octoql.DefaultResponseSizeLimit (10 MiB) from each HTTP response. Configure a different positive limit before executing operations:

err := client.SetResponseSizeLimit(20 * 1024 * 1024)
if err != nil {
	return err
}

An oversized response fails before JSON decoding with *octoql.ResponseSizeLimitError; it still includes *octoql.ResponseError and any applicable *octoql.RateLimitError in the same error chain. RawBody contains at most 64 KiB for non-2xx, over-limit, or undecodable responses; RawBodyTruncated reports truncation. Raw response bodies may contain sensitive data and should not be logged indiscriminately.

Error types are independent facets of one chain, not mutually exclusive categories. A rate-limited response can match *octoql.RateLimitError, *octoql.ResponseError, and octoql.Errors. Use separate errors.As or errors.AsType checks when more than one facet matters.

Outcome Generated response Error facets
Success Non-nil concrete data nil
GraphQL errors with decodable non-null data nil; inspect the generated operation partial-data error operation partial-data error, ResponseError, Errors
Any error without decodable data nil ResponseError and available causes
Primary or secondary rate limit with decodable data nil; inspect the generated operation partial-data error operation partial-data error, RateLimitError, ResponseError, and possibly Errors
Client getter, encoding, or transport failure before a response nil Wrapped underlying error; no ResponseError

Primary and secondary GitHub limits wrap *octoql.ResponseError in *octoql.RateLimitError. Primary limits require X-RateLimit-Remaining: 0 plus HTTP 403/429 or a GraphQL RATE_LIMITED error. Secondary limits use Retry-After on HTTP 200/403/429 responses:

rateLimitError, ok := errors.AsType[*octoql.RateLimitError](err)
if ok {
	fmt.Printf("kind=%s remaining=%d retry_at=%s\n",
		rateLimitError.Kind,
		rateLimitError.RateLimit.Remaining,
		rateLimitError.RateLimit.RetryAt,
	)
}

The client also keeps the latest valid primary rate-limit headers observed from any response:

rateLimit, known := client.RateLimit()
if known {
	fmt.Printf("remaining=%d reset=%s\n", rateLimit.Remaining, rateLimit.Reset)
}

The snapshot is concurrency-safe and advisory, not a reservation: other clients or processes can consume the same GitHub budget after it is observed. Missing or malformed rate-limit headers do not erase the last valid snapshot. Successful response status, arbitrary headers, and request ID are intentionally not attached to generated data. Use the supplied http.RoundTripper when an application needs arbitrary successful-response headers.

octoql never retries or sleeps automatically. Apply retry policy in the calling application after considering operation safety, Retry-After, and the parsed reset time.

Generated types and GitHub defaults

GraphQL's built-in scalars map to ordinary Go values:

GraphQL Go
Int int
Float float64
String, ID string
Boolean bool

Nullable named values generate as pointers by default. Generated abstract interface values are the exception: their nil interface value represents GraphQL null, so they are never wrapped in pointers. Nullable list values remain slices so GraphQL null and [] map to nil and empty slices, respectively; nullable non-interface list elements generate as pointers. Use @octoqlgen(pointer: false) on a specific operation argument or selected field when its zero value should represent GraphQL null. The omitempty directive remains independent and explicit.

octoqlgen also supplies GitHub defaults:

GitHub scalar Go
DateTime, PreciseDateTime, GitTimestamp time.Time
CustomPropertyValue encoding/json.RawMessage
Base64String, BigInt, Date string
GitObjectID, GitRefname, GitSSHRemote string
HTML, URI, X509Certificate string

Explicit bindings in octoqlgen.yaml override defaults:

bindings:
  DateTime:
    type: example.com/project/githubapi.Timestamp
    marshaler: example.com/project/githubapi.MarshalTimestamp
    unmarshaler: example.com/project/githubapi.UnmarshalTimestamp

Unknown custom scalars require a binding. Bound types must implement the necessary JSON behavior, or provide marshal and unmarshal functions.

For GraphQL interfaces and unions, octoqlgen defaults to generating concrete types only for implementations selected by fragments. Every abstract selection also receives an OctoqlOther catch-all with shared fields and __typename, so new server implementations remain decodable. To restore inherited generation of every schema implementation:

omit_unreferenced_implementations: false

The opt-out removes the catch-all and may make a new server typename an unmarshal error. Generated types that need JSON first-pass protection embed octoql.NoMarshalJSON or octoql.NoUnmarshalJSON to prevent method promotion from changing encoding/json behavior. These exported names are generated-code contracts; application code should not embed or call them directly.

Typed test handlers

Generate a typed http.Handler from the same immutable operation plan as the client:

generated: internal/githubapi/generated.go
test_handler:
  generated: internal/githubapitest/generated.go
  types: client

types: client is the default. It imports the generated client package and aliases operation types, so handler response values are directly assignable to client types.

Use types: local to generate distinct wire-equivalent types in the handler package without importing the client:

test_handler:
  generated: internal/githubapitest/generated.go
  types: local

Local handler values are intentionally not assignable to client types. Local mode also rejects reachable bindings or marshal helpers owned by the generated client package because those references would recreate the dependency.

When test_handler is configured, every query or mutation name must begin with an uppercase letter. This applies to both types: client and types: local; the strategy changes type ownership, not the generated handler API's exported naming rule. Client types also need the restriction because the separate handler package aliases generated client types.

After go tool octoqlgen generate, each handler operation has matching Expect<Operation>, Default<Operation>, and Reset<Operation> methods:

handler := githubapitest.NewTestHandler(t)
server := httptest.NewServer(handler)
t.Cleanup(server.Close)

variables := githubapitest.GetRepositoryVariables{
	Owner: "octo-org",
	Name:  "octo-repo",
	First: 1,
}
handler.ExpectGetRepository(variables, githubapitest.Times(2)).
	Respond(githubapitest.GetRepositoryResponse{
		Repository: githubapitest.GetRepositoryRepository{
			NameWithOwner: "octo-org/octo-repo",
		},
	})

client := octoql.NewClient(server.URL, server.Client())
response, err := githubapi.GetRepository(
	t.Context(),
	client,
	variables,
)
require.NoError(t, err)
require.Equal(t, "octo-org/octo-repo", response.Repository.NameWithOwner)

An expectation defaults to one call. Pass Times(n) to require exactly n, MinTimes(n) to set a minimum, or MinTimes(0) to create an unlimited stub. Default<Operation> is an unlimited fallback. Cleanup verifies unmet expectations, and expectation state is safe for concurrent requests.

Partial data and errors, per-error extensions, headers, status, and rate limits are configurable:

handler.ExpectGetRepository(variables).
	WithOptions(
		githubapitest.WithStatus(http.StatusForbidden),
		githubapitest.WithHeader("X-GitHub-Request-ID", "request-123"),
		githubapitest.WithPrimaryRateLimit(octoql.RateLimit{
			Limit:     5000,
			Remaining: 0,
			Used:      5000,
			Resource:  "graphql",
		}),
	).
	RespondDataAndErrors(
		partialData,
		octoql.Error{
			Type:       "FORBIDDEN",
			Message:    "one field is unavailable",
			Extensions: map[string]any{"code": "missing"},
		},
	)

WithHeaders replaces multiple headers. WithSecondaryRateLimit writes Retry-After. RespondError returns errors without data, and Handle gives a test complete http.ResponseWriter control. Test-handler helpers do not retry or sleep.

Migration from genqlient or earlier octoql config

  • octoqlgen.yaml is the only user-facing generator config. Rename and flatten inherited genqlient.yaml settings into it. Legacy config is not discovered, parsed, merged, or translated.
  • Replace a scalar or list schema setting with schema.path and, for remote materialization, one pinned schema.source plus schema.sha256.
  • All configured paths are relative to octoqlgen.yaml.
  • Import the root runtime as github.com/willabides/octoql. There is no graphql runtime package or public generate package. Invoke github.com/willabides/octoql/cmd/octoqlgen.
  • Generated helpers now return concrete operation data. Replace response.Data.Field with response.Field. Replace handwritten Do calls with generated operations.
  • Replace HTTPError checks with ResponseError. The latter covers every failure after an HTTP response, including HTTP-200 GraphQL and decode errors.
  • Read successful primary rate-limit state from Client.RateLimit().
  • Remove use_extensions. It was a no-op. Top-level response extensions are ignored; per-error Error.Extensions remains available.
  • octoql does not support subscriptions. Convert supported operations to queries or mutations before migration.
  • Explicit scalar bindings override octoql's GitHub defaults.
  • Abstract selections now use OctoqlOther by default. Set omit_unreferenced_implementations: false temporarily when migrating an exhaustive type switch.
  • Upgrade the runtime and generator together, then regenerate checked-in code. The single module intentionally keeps their versions synchronized.

Contributing

Snapshot maintenance is a contributor workflow, not part of installing or using octoql. Update go-snaps output only when the behavior or generated artifacts covered by an affected test intentionally change, review every update, and run the same focused test normally afterward. See CONTRIBUTING.md for commands and repository conventions.

Reference and project policies

The root README is the primary user guide. The docs/ directory is limited to specialized references and project policies. Project history remains available in Git.

Documentation

Overview

Package octoql provides reusable runtime APIs for generated GraphQL clients.

Index

Examples

Constants

View Source
const DefaultResponseSizeLimit int64 = 10 * 1024 * 1024

DefaultResponseSizeLimit is the maximum GraphQL HTTP response body size a Client accepts for decoding unless configured otherwise. It is 10 MiB.

Variables

This section is empty.

Functions

This section is empty.

Types

type Client

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

Client executes GraphQL operations against an HTTP endpoint.

Configure bearer authentication with Client.SetBearerToken. Other request behavior, including other authentication schemes, belongs on the supplied http.Client or its http.RoundTripper. A Client may be used concurrently after construction.

func NewClient

func NewClient(endpoint string, httpClient *http.Client) *Client

NewClient returns a client for endpoint. A nil httpClient uses http.DefaultClient.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/willabides/octoql"
)

type exampleViewerResponse struct {
	Viewer struct {
		Login string `json:"login"`
	} `json:"viewer"`
}

type exampleViewerPartialDataError struct {
	data *exampleViewerResponse
	err  error
}

func (e *exampleViewerPartialDataError) Error() string { return e.err.Error() }
func (e *exampleViewerPartialDataError) Unwrap() error { return e.err }
func (e *exampleViewerPartialDataError) PartialData() *exampleViewerResponse {
	return e.data
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		_, err := io.WriteString(writer, `{"data":{"viewer":{"login":"octocat"}}}`)
		if err != nil {
			panic(err)
		}
	}))
	defer server.Close()

	client := octoql.NewClient(server.URL, server.Client())
	response, err := getViewer(context.Background(), client)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(response.Viewer.Login)
}

func getViewer(
	ctx context.Context,
	client *octoql.Client,
) (*exampleViewerResponse, error) {
	return executeExample[exampleViewerResponse](
		ctx,
		client,
		"Viewer",
		"query Viewer { viewer { login } }",
		func(data *exampleViewerResponse, err error) error {
			return &exampleViewerPartialDataError{data: data, err: err}
		},
	)
}

func executeExample[T any](
	ctx context.Context,
	client *octoql.Client,
	operationName string,
	query string,
	newPartialDataError func(*T, error) error,
) (*T, error) {
	response := new(T)
	hasData, err := client.Execute(ctx, octoql.Payload{
		OperationName: operationName,
		Query:         query,
	}, response)
	if !hasData {
		return nil, err
	}
	if err != nil {
		return nil, newPartialDataError(response, err)
	}
	return response, nil
}
Output:
octocat

func (*Client) Execute

func (c *Client) Execute(ctx context.Context, payload Payload, response any) (bool, error)

Execute runs operation and decodes its response data into response.

Execute is a generated-code contract. Response must be a non-nil pointer. The returned boolean reports whether the GraphQL data field decoded successfully and response is usable, including when GraphQL errors are also returned. Every failure after the server returns an HTTP response includes ResponseError. GraphQL errors and rate limits remain discoverable in that error chain as Errors and RateLimitError.

func (*Client) RateLimit

func (c *Client) RateLimit() (RateLimit, bool)

RateLimit returns the latest valid primary rate-limit snapshot observed by client. The boolean is false until a response includes a valid X-RateLimit-Remaining header.

The snapshot is advisory: other clients and processes can consume the same GitHub rate-limit budget after it is observed.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/willabides/octoql"
)

type exampleViewerResponse struct {
	Viewer struct {
		Login string `json:"login"`
	} `json:"viewer"`
}

type exampleViewerPartialDataError struct {
	data *exampleViewerResponse
	err  error
}

func (e *exampleViewerPartialDataError) Error() string { return e.err.Error() }
func (e *exampleViewerPartialDataError) Unwrap() error { return e.err }
func (e *exampleViewerPartialDataError) PartialData() *exampleViewerResponse {
	return e.data
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("X-RateLimit-Limit", "5000")
		writer.Header().Set("X-RateLimit-Remaining", "4999")
		writer.Header().Set("X-RateLimit-Used", "1")
		_, err := io.WriteString(writer, `{"data":{}}`)
		if err != nil {
			panic(err)
		}
	}))
	defer server.Close()

	client := octoql.NewClient(server.URL, server.Client())
	_, err := getViewer(context.Background(), client)
	if err != nil {
		fmt.Println(err)
		return
	}

	rateLimit, known := client.RateLimit()
	fmt.Println(known)
	fmt.Println(rateLimit.Limit)
	fmt.Println(rateLimit.Remaining)
}

func getViewer(
	ctx context.Context,
	client *octoql.Client,
) (*exampleViewerResponse, error) {
	return executeExample[exampleViewerResponse](
		ctx,
		client,
		"Viewer",
		"query Viewer { viewer { login } }",
		func(data *exampleViewerResponse, err error) error {
			return &exampleViewerPartialDataError{data: data, err: err}
		},
	)
}

func executeExample[T any](
	ctx context.Context,
	client *octoql.Client,
	operationName string,
	query string,
	newPartialDataError func(*T, error) error,
) (*T, error) {
	response := new(T)
	hasData, err := client.Execute(ctx, octoql.Payload{
		OperationName: operationName,
		Query:         query,
	}, response)
	if !hasData {
		return nil, err
	}
	if err != nil {
		return nil, newPartialDataError(response, err)
	}
	return response, nil
}
Output:
true
5000
4999

func (*Client) ResponseSizeLimit

func (c *Client) ResponseSizeLimit() int64

ResponseSizeLimit returns the maximum HTTP response body size Client accepts for decoding. A newly constructed Client uses DefaultResponseSizeLimit.

func (*Client) SetBearerToken

func (c *Client) SetBearerToken(token string) error

SetBearerToken configures the OAuth 2.0 bearer token sent with each request. token must use the RFC 6750 b64token syntax. It may be called concurrently with Client.Execute to rotate credentials.

func (*Client) SetResponseSizeLimit

func (c *Client) SetResponseSizeLimit(limit int64) error

SetResponseSizeLimit configures the maximum HTTP response body size Client accepts for decoding. limit must be greater than zero. It may be called concurrently with Client.Execute.

type Error

type Error struct {
	Type       ErrorType      `json:"type,omitempty"`
	Message    string         `json:"message"`
	Path       Path           `json:"path,omitempty"`
	Locations  []Location     `json:"locations,omitempty"`
	Extensions map[string]any `json:"extensions,omitempty"`
}

Error describes an error returned in a GraphQL response.

func (*Error) Error

func (e *Error) Error() string

Error returns the GraphQL error message and response path.

type ErrorType

type ErrorType string

ErrorType identifies a GitHub GraphQL error category. It is an open string type so values introduced by GitHub remain available to callers.

type Errors

type Errors []*Error

Errors is the list of errors returned in a GraphQL response.

Example (PartialData)
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/willabides/octoql"
)

type exampleRepositoryResponse struct {
	Repository struct {
		Name string `json:"name"`
	} `json:"repository"`
}

type exampleRepositoryPartialDataError struct {
	data *exampleRepositoryResponse
	err  error
}

func (e *exampleRepositoryPartialDataError) Error() string { return e.err.Error() }
func (e *exampleRepositoryPartialDataError) Unwrap() error { return e.err }
func (e *exampleRepositoryPartialDataError) PartialData() *exampleRepositoryResponse {
	return e.data
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("Content-Type", "application/json")
		_, err := io.WriteString(writer, `{
			"data":{"repository":{"name":"octoql"}},
			"errors":[{"type":"FORBIDDEN","message":"owner unavailable","path":["repository","owner"]}]
		}`)
		if err != nil {
			panic(err)
		}
	}))
	defer server.Close()

	client := octoql.NewClient(server.URL, server.Client())
	response, err := getRepository(context.Background(), client)

	var graphqlErrors octoql.Errors
	if errors.As(err, &graphqlErrors) {
		fmt.Println(response == nil)
		partialErr, ok := errors.AsType[*exampleRepositoryPartialDataError](err)
		if ok {
			fmt.Println(partialErr.PartialData().Repository.Name)
		}
		fmt.Println(graphqlErrors[0].Type)
	}
}

func getRepository(
	ctx context.Context,
	client *octoql.Client,
) (*exampleRepositoryResponse, error) {
	return executeExample[exampleRepositoryResponse](
		ctx,
		client,
		"Repository",
		"query Repository { repository { name owner { login } } }",
		func(data *exampleRepositoryResponse, err error) error {
			return &exampleRepositoryPartialDataError{data: data, err: err}
		},
	)
}

func executeExample[T any](
	ctx context.Context,
	client *octoql.Client,
	operationName string,
	query string,
	newPartialDataError func(*T, error) error,
) (*T, error) {
	response := new(T)
	hasData, err := client.Execute(ctx, octoql.Payload{
		OperationName: operationName,
		Query:         query,
	}, response)
	if !hasData {
		return nil, err
	}
	if err != nil {
		return nil, newPartialDataError(response, err)
	}
	return response, nil
}
Output:
true
octoql
FORBIDDEN

func (Errors) Error

func (e Errors) Error() string

Error returns a stable summary of all GraphQL errors.

func (Errors) Unwrap

func (e Errors) Unwrap() []error

Unwrap exposes individual GraphQL errors to errors.Is, errors.As, and errors.AsType.

type Location

type Location struct {
	Line   int `json:"line,omitempty"`
	Column int `json:"column,omitempty"`
}

Location identifies a line and column in a GraphQL document.

type NoMarshalJSON

type NoMarshalJSON struct{}

NoMarshalJSON is for generated code only.

Embedding NoMarshalJSON alongside a type with a MarshalJSON method prevents that sibling method from being promoted.

func (NoMarshalJSON) MarshalJSON

func (NoMarshalJSON) MarshalJSON() ([]byte, error)

MarshalJSON should never be called. It exists only to prevent a sibling MarshalJSON method from being promoted.

type NoUnmarshalJSON

type NoUnmarshalJSON struct{}

NoUnmarshalJSON is for generated code only.

Embedding NoUnmarshalJSON alongside a type with an UnmarshalJSON method prevents that sibling method from being promoted.

func (NoUnmarshalJSON) UnmarshalJSON

func (NoUnmarshalJSON) UnmarshalJSON([]byte) error

UnmarshalJSON should never be called. It exists only to prevent a sibling UnmarshalJSON method from being promoted.

type Path

type Path []any

Path is a GraphQL response path. Each segment is either a string field name or an integer list index.

func (Path) MarshalJSON

func (p Path) MarshalJSON() ([]byte, error)

MarshalJSON encodes string and integer path segments in GraphQL wire format.

func (Path) String

func (p Path) String() string

String formats a path using dotted fields and bracketed list indexes.

func (*Path) UnmarshalJSON

func (p *Path) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes GraphQL string and integer path segments.

type Payload

type Payload struct {
	Query         string `json:"query"`
	OperationName string `json:"operationName"`
	Variables     any    `json:"variables,omitempty"`
}

Payload is the GraphQL request body used by generated clients.

type RateLimit

type RateLimit struct {
	Limit      int
	Remaining  int
	Used       int
	Reset      time.Time
	Resource   string
	RetryAfter time.Duration
	RetryAt    time.Time
}

RateLimit describes the rate-limit headers returned by GitHub.

Missing or malformed response headers leave their corresponding fields at their zero values.

type RateLimitError

type RateLimitError struct {
	Kind      RateLimitKind
	RateLimit RateLimit
	Err       error
}

RateLimitError describes a response rejected because of a GitHub rate limit.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/willabides/octoql"
)

type exampleViewerResponse struct {
	Viewer struct {
		Login string `json:"login"`
	} `json:"viewer"`
}

type exampleViewerPartialDataError struct {
	data *exampleViewerResponse
	err  error
}

func (e *exampleViewerPartialDataError) Error() string { return e.err.Error() }
func (e *exampleViewerPartialDataError) Unwrap() error { return e.err }
func (e *exampleViewerPartialDataError) PartialData() *exampleViewerResponse {
	return e.data
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("Retry-After", "30")
		_, err := io.WriteString(writer, `{"errors":[{"message":"slow down"}]}`)
		if err != nil {
			panic(err)
		}
	}))
	defer server.Close()

	client := octoql.NewClient(server.URL, server.Client())
	_, err := getViewer(context.Background(), client)

	rateLimitError, ok := errors.AsType[*octoql.RateLimitError](err)
	fmt.Println(ok)
	fmt.Println(rateLimitError.Kind)
}

func getViewer(
	ctx context.Context,
	client *octoql.Client,
) (*exampleViewerResponse, error) {
	return executeExample[exampleViewerResponse](
		ctx,
		client,
		"Viewer",
		"query Viewer { viewer { login } }",
		func(data *exampleViewerResponse, err error) error {
			return &exampleViewerPartialDataError{data: data, err: err}
		},
	)
}

func executeExample[T any](
	ctx context.Context,
	client *octoql.Client,
	operationName string,
	query string,
	newPartialDataError func(*T, error) error,
) (*T, error) {
	response := new(T)
	hasData, err := client.Execute(ctx, octoql.Payload{
		OperationName: operationName,
		Query:         query,
	}, response)
	if !hasData {
		return nil, err
	}
	if err != nil {
		return nil, newPartialDataError(response, err)
	}
	return response, nil
}
Output:
true
secondary

func (*RateLimitError) Error

func (e *RateLimitError) Error() string

Error returns a summary of the rate-limit failure.

func (*RateLimitError) Unwrap

func (e *RateLimitError) Unwrap() error

Unwrap exposes the response failure and its GraphQL or processing causes.

type RateLimitKind

type RateLimitKind string

RateLimitKind identifies the GitHub rate limit that rejected a request.

const (
	// RateLimitPrimary identifies exhaustion of GitHub's primary rate limit.
	RateLimitPrimary RateLimitKind = "primary"
	// RateLimitSecondary identifies GitHub's secondary rate limit.
	RateLimitSecondary RateLimitKind = "secondary"
)

type ResponseError

type ResponseError struct {
	// StatusCode is the HTTP response status.
	StatusCode int
	// RequestID is GitHub's X-GitHub-Request-ID value, when present.
	RequestID string
	// RawBody contains at most the first 64 KiB of a non-successful, over-limit,
	// or undecodable response. It is omitted for ordinary GraphQL errors.
	RawBody []byte
	// RawBodyTruncated reports whether RawBody omits trailing response bytes.
	RawBodyTruncated bool
	// contains filtered or unexported fields
}

ResponseError describes a failed GraphQL HTTP response.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"

	"github.com/willabides/octoql"
)

type exampleViewerResponse struct {
	Viewer struct {
		Login string `json:"login"`
	} `json:"viewer"`
}

type exampleViewerPartialDataError struct {
	data *exampleViewerResponse
	err  error
}

func (e *exampleViewerPartialDataError) Error() string { return e.err.Error() }
func (e *exampleViewerPartialDataError) Unwrap() error { return e.err }
func (e *exampleViewerPartialDataError) PartialData() *exampleViewerResponse {
	return e.data
}

func main() {
	server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) {
		writer.Header().Set("X-GitHub-Request-ID", "request-example")
		writer.WriteHeader(http.StatusForbidden)
		_, err := io.WriteString(
			writer,
			`{"errors":[{"type":"FORBIDDEN","message":"request rejected"}]}`,
		)
		if err != nil {
			panic(err)
		}
	}))
	defer server.Close()

	client := octoql.NewClient(server.URL, server.Client())
	_, err := getViewer(context.Background(), client)

	responseError, ok := errors.AsType[*octoql.ResponseError](err)
	if !ok {
		fmt.Println("response error not found")
		return
	}
	graphqlErrors, ok := errors.AsType[octoql.Errors](err)
	if !ok {
		fmt.Println("graphql errors not found")
		return
	}

	fmt.Println(responseError != nil)
	fmt.Println(responseError.StatusCode)
	fmt.Println(responseError.RequestID)
	fmt.Println(graphqlErrors[0].Type)
}

func getViewer(
	ctx context.Context,
	client *octoql.Client,
) (*exampleViewerResponse, error) {
	return executeExample[exampleViewerResponse](
		ctx,
		client,
		"Viewer",
		"query Viewer { viewer { login } }",
		func(data *exampleViewerResponse, err error) error {
			return &exampleViewerPartialDataError{data: data, err: err}
		},
	)
}

func executeExample[T any](
	ctx context.Context,
	client *octoql.Client,
	operationName string,
	query string,
	newPartialDataError func(*T, error) error,
) (*T, error) {
	response := new(T)
	hasData, err := client.Execute(ctx, octoql.Payload{
		OperationName: operationName,
		Query:         query,
	}, response)
	if !hasData {
		return nil, err
	}
	if err != nil {
		return nil, newPartialDataError(response, err)
	}
	return response, nil
}
Output:
true
403
request-example
FORBIDDEN

func (*ResponseError) Error

func (e *ResponseError) Error() string

Error returns a stable summary of the failed response.

func (*ResponseError) Unwrap

func (e *ResponseError) Unwrap() error

Unwrap exposes decoded GraphQL errors and response processing failures to errors.Is, errors.As, and errors.AsType.

type ResponseSizeLimitError

type ResponseSizeLimitError struct {
	// Limit is the configured maximum response size in bytes.
	Limit int64
}

ResponseSizeLimitError reports that a GraphQL HTTP response exceeded Client's configured response-size limit.

func (*ResponseSizeLimitError) Error

func (e *ResponseSizeLimitError) Error() string

Error reports that the response exceeded its configured limit.

Directories

Path Synopsis
cmd
octoqlgen command
octoqlgen generates type-safe Go GraphQL clients.
octoqlgen generates type-safe Go GraphQL clients.
octoqlgen/internal/cli
Package cli defines the octoqlgen command-line interface.
Package cli defines the octoqlgen command-line interface.
octoqlgen/internal/config
Package config loads octoqlgen configuration files.
Package config loads octoqlgen configuration files.
octoqlgen/internal/schema
Package schema verifies and materializes pinned GraphQL schemas.
Package schema verifies and materializes pinned GraphQL schemas.
internal
generatefeatures/clientgetter
Package clientgetter exercises generated client getters and custom contexts.
Package clientgetter exercises generated client getters and custom contexts.
generatefeatures/githubdefaults
Package githubdefaults exercises generated helpers that use GitHub's default scalar bindings.
Package githubdefaults exercises generated helpers that use GitHub's default scalar bindings.
generatefeatures/nocontext
Package nocontext exercises generated helpers configured without a context parameter.
Package nocontext exercises generated helpers configured without a context parameter.
handlertest
Package handlertest exercises generated clients and typed test handlers.
Package handlertest exercises generated clients and typed test handlers.
handlertest/localfixture
Package localfixture generates a client and handler-local types for parity tests.
Package localfixture generates a client and handler-local types for parity tests.

Jump to

Keyboard shortcuts

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