Help us shape the future of Go! Take the 2026 Go Developer Survey

rigclient

package module
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package rigclient is the half of a rig Go SDK that is not generated.

A generated client is types and one method per endpoint. Everything under those methods — building the request, carrying the credential, reading a failure back into an error worth switching on, walking a paginated read to its end — is the same in every project, because the server it talks to was generated too. So it lives here, hand-written, and the generated code is thin enough to read.

What the generator supplies is what only the document knows: the base path, the shape of each call, and — for a project with authentication — the profile in AuthProfile, whose lifetimes are the ones the server enforces rather than numbers a client author guessed.

The three things a caller sets up are in Config: where the server is, what credential to present, and which http.Client to use. Everything else has a default that is right until it is not.

Index

Constants

View Source
const DefaultAttempts = 4

DefaultAttempts is how many times a repeatable call is sent when Config.Retry says nothing: the first try and three more.

View Source
const DefaultRefreshLeeway = 30 * time.Second

DefaultRefreshLeeway is used when the profile names none, which is a project whose configuration left the rotation leeway at zero.

View Source
const DefaultRequestIDHeader = "X-Request-Id"

DefaultRequestIDHeader is where the generated server looks for a caller's own request identifier.

View Source
const DefaultRetryBase = time.Second

DefaultRetryBase is the first backoff window, doubling per attempt after it.

With the default attempt count that is an immediate retry, then about a second, then about two — call it five seconds before a failing call gives up, which is long enough to cross a rolling deploy's window and short enough that somebody waiting on the answer has not gone to look at the logs yet.

View Source
const DefaultRetryCap = 2 * time.Second

DefaultRetryCap bounds one backoff window. See Retry.Cap.

View Source
const DefaultRevisionHeader = "API-Revision"

DefaultRevisionHeader carries the API revision, and is what rig.yaml's api.revision_header defaults to. A generated client passes its project's own, so this is the fallback for a client somebody built by hand.

View Source
const DefaultTimeout = 30 * time.Second

DefaultTimeout bounds a whole request when the caller supplied no client of their own.

View Source
const DefaultUserAgent = "rig-go-client"

DefaultUserAgent is what a client that did not name itself sends.

View Source
const MaxRetryAfter = 30 * time.Second

MaxRetryAfter is the longest a Retry-After will be slept through.

A server asking for an hour is asking for something a client library cannot agree to on somebody's behalf: whether this program has an hour is a question about the program. So a longer interval is not waited for — the call comes back with the refusal and Error.RetryAfter on it, which is the same information handed to somebody who can act on it.

View Source
const MethodQuery = "QUERY"

MethodQuery is the HTTP method a generated search uses. It is a method rather than a POST because a search is a read: it has a body, and it is safe and idempotent, and pretending otherwise is what made every API in the world invent /_search.

Variables

View Source
var ErrCannotRetry = errors.New("rigclient: the request body cannot be sent twice")

ErrCannotRetry is returned when a request had to be sent a second time and its body could not be produced again.

It is only ever an upload from a reader that cannot seek — a pipe, another response, a decompressor. The alternative is to buffer every upload so that a retry which almost never happens is always possible, and that is the wrong trade for a feature whose whole point is that a large file never lands in memory.

It arrives joined to the failure that prompted the retry, so an upload interrupted by an expired credential still answers IsUnauthorized. A caller needs both facts: the credential has been refreshed, and the call has to be made again from a reader that still has the bytes.

View Source
var ErrNoSession = errors.New("rigclient: no session; sign in again")

ErrNoSession is returned when a session has nothing left to present: it was never signed in, or its refresh token has expired and the person has to sign in again.

Functions

func CodeOf

func CodeOf(err error) rigerr.Code

CodeOf returns the code an error carries, or empty when it carries none.

func Do

func Do[T any](ctx context.Context, rt *Runtime, op Op, opts ...CallOption) (*T, error)

Do performs a call and decodes the response body.

The result is a pointer so that a 204 — which several endpoints can answer with even when they usually have a body — comes back as nil rather than as a zero value indistinguishable from an empty one.

func DoNoContent

func DoNoContent(ctx context.Context, rt *Runtime, op Op, opts ...CallOption) error

DoNoContent performs a call that answers with nothing, such as a delete.

Any body is drained and discarded rather than parsed: an endpoint that grows one later should not break a client that never wanted it.

func FieldsAs

func FieldsAs[T any](err error) (T, bool)

FieldsAs decodes the per-field failures into the generated shape for the input that failed — TodoCreateFields, say, whose members are the input's members.

It reports false when the failure carried none, so the ordinary

if fields, ok := rigclient.FieldsAs[client.TodoCreateFields](err); ok {

reads as the question it is.

It answers the fields alone. The generated client declares a function per call — client.TodoCreateError(err) — that answers the envelope with them, and picks the shape for you: this one cannot tell that the shape it was handed is not the one that failed, because every member of one is optional.

func IsConflict

func IsConflict(err error) bool

IsConflict reports whether the request contradicted the current state.

func IsForbidden

func IsForbidden(err error) bool

IsForbidden reports whether the caller is known but not permitted.

func IsInvalid

func IsInvalid(err error) bool

IsInvalid reports whether a well-formed request failed validation, which is the failure FieldsAs has something to say about.

func IsNotFound

func IsNotFound(err error) bool

IsNotFound reports whether the server answered that there is no such thing. A row in another tenant is the same answer, deliberately.

func IsRateLimited

func IsRateLimited(err error) bool

IsRateLimited reports whether the caller was asked to slow down.

func IsTooLarge

func IsTooLarge(err error) bool

IsTooLarge reports whether the body was larger than the endpoint accepts.

func IsUnauthorized

func IsUnauthorized(err error) bool

IsUnauthorized reports whether no valid credential was presented.

func IsUnavailable added in v0.6.0

func IsUnavailable(err error) bool

IsUnavailable reports whether something the server depends on did not answer in time.

The one failure here worth retrying on its own. Nothing was rejected and nothing is broken — the request was not answered — so the same request sent again may well succeed, which is not true of any other code in this list.

func IsUnsupportedMediaType

func IsUnsupportedMediaType(err error) bool

IsUnsupportedMediaType reports whether the body's content type was refused.

func IsUpgradeRequired

func IsUpgradeRequired(err error) bool

IsUpgradeRequired reports whether this client is older than the server will serve, and has to be regenerated against the current schema.

It is the one failure here that nothing the caller does at runtime will fix — not a retry, not a different input, not signing in again — so it is usually the one worth turning into a message a person sees rather than an error a handler swallows.

func P

func P[T any](v T) *T

P takes the address of a value, so a query can be filled in one expression:

c.Todos.List(ctx, client.TodoListQuery{Limit: rigclient.P(10)})

func Paginate

func Paginate[T any](
	ctx context.Context, offset int, fetch func(context.Context, int) (Page[T], error),
) iter.Seq2[T, error]

Paginate walks a paginated read to its end.

fetch is handed the offset to ask for and returns one page; the limit is whatever the caller's query said, which fetch closes over. Iteration stops after the page that reaches the reported total, at the first failure, or when a page comes back empty — that last one is the guard against a server whose total disagrees with what it returns, which would otherwise be an infinite loop rather than a bug report.

A failure is the second value of the final pair. There is no partial answer: what came before the failure was yielded, and nothing after it is.

func PathValue

func PathValue(v string) string

PathValue escapes a value for a path segment.

An identifier that arrived from somewhere else can be anything at all, and a slash in one would otherwise silently address a different route.

func SetBool

func SetBool(q url.Values, key string, v *bool)

SetBool writes a boolean parameter.

func SetFloat64

func SetFloat64(q url.Values, key string, v *float64)

SetFloat64 writes a floating-point parameter.

func SetInt

func SetInt(q url.Values, key string, v *int)

SetInt writes an integer parameter.

func SetInt64

func SetInt64(q url.Values, key string, v *int64)

SetInt64 writes a 64-bit integer parameter.

func SetString

func SetString[T ~string](q url.Values, key string, v *T)

SetString writes a string parameter.

The type parameter covers every named string type as well: an enum, and the scope parameter, are strings the compiler keeps apart from each other and this does not have to.

func SetStrings

func SetStrings[T ~string](q url.Values, key string, vs []T)

SetStrings writes a repeated string parameter, one key per value.

func SetTime

func SetTime(q url.Values, key string, v *time.Time)

SetTime writes a timestamp parameter, in RFC 3339.

func SetUUID

func SetUUID(q url.Values, key string, v *uuid.UUID)

SetUUID writes an identifier parameter.

Types

type API

type API struct {
	// BasePath is the prefix every route sits under, for example "/api/v1".
	BasePath string
	// Auth is the authentication profile, or nil for a project with none.
	Auth *AuthProfile
	// Revision is the date the API surface this client was generated from last
	// changed. It is sent on every request, which is how a server's logs can
	// answer how old the oldest caller still calling is.
	Revision string
	// RevisionHeader is where Revision is sent — the same header the server it
	// was generated with reads.
	RevisionHeader string
}

API is what the generated client knows about the server and this package does not. Generated code fills it in; a caller never sees it.

type APIKey

type APIKey string

APIKey presents an API key secret — the rig_sk_ value shown once, when the key was minted.

It is a distinct type from StaticToken although the header is identical, so that a caller reading the code can see which kind of credential a program holds. The server tells them apart by prefix.

func (APIKey) Apply

func (k APIKey) Apply(_ context.Context, r *http.Request) error

Apply implements Credential.

type AuditQuery

type AuditQuery struct {
	AccountID *uuid.UUID
	Event     *string
	Outcome   *string
	Since     *time.Time
	Until     *time.Time

	// Limit is the page size, defaulting to 50 and capped at 500. Offset is
	// where the page starts.
	Limit  *int
	Offset *int
}

AuditQuery narrows the authentication trail.

Every field is optional and a nil one does not narrow. AccountID is only answerable alongside Wide: without it the trail is the caller's own and naming somebody else is refused rather than quietly ignored.

func (AuditQuery) Values

func (q AuditQuery) Values() url.Values

Values renders the query.

type Auth

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

Auth is rig's own authentication endpoints.

Nothing here is generated, because none of it varies: these are the same routes with the same bodies in every project that turns authentication on. What varies is which of them are mounted and how long the credentials last, and that arrives in the AuthProfile the generator emitted.

Reached as Client.Auth on a generated client, and nil for a project with no authentication at all.

func (*Auth) APIKeys

func (a *Auth) APIKeys(ctx context.Context, opts ...CallOption) ([]authwire.APIKeyView, error)

APIKeys lists the keys the caller may see: their own, or the tenant's when they administer keys.

func (*Auth) AcceptInvitation

func (a *Auth) AcceptInvitation(
	ctx context.Context, in authwire.AcceptRequest, opts ...CallOption,
) (*authwire.TokenPair, error)

AcceptInvitation redeems an invitation with the token from the mail, and installs the session it answers with.

The token is the credential, for one use, so this needs none of its own.

func (*Auth) AcceptMyInvitation

func (a *Auth) AcceptMyInvitation(
	ctx context.Context, identityToken string, invitationID uuid.UUID, client string,
	opts ...CallOption,
) (*authwire.SignInResponse, error)

AcceptMyInvitation joins the tenant an invitation names, and installs the session that comes back.

func (*Auth) AuditLog

func (a *Auth) AuditLog(
	ctx context.Context, q AuditQuery, opts ...CallOption,
) (*authwire.Page[authwire.AuthLogEntryView], error)

AuditLog reads one page of the authentication trail, newest first.

The caller's own events by default, which needs no permission; the whole tenant's with Wide, which needs `authlog.read.all`. What neither reaches is the attempts that resolved to no tenant — a sign-in that named none, or one against an address with no account anywhere. Those are recorded and no tenant has the standing to read them.

func (*Auth) AuditLogAll

func (a *Auth) AuditLogAll(
	ctx context.Context, q AuditQuery, opts ...CallOption,
) iter.Seq2[authwire.AuthLogEntryView, error]

AuditLogAll walks the trail a page at a time.

The query's own limit is the page size and its offset is where the walk starts. Iteration stops at the first failure, which arrives as the second value of the last pair — so a loop that ignores it is a loop that silently stops early.

func (*Auth) CreateAPIKey

func (a *Auth) CreateAPIKey(
	ctx context.Context, in authwire.CreateKeyRequest, opts ...CallOption,
) (*authwire.CreateKeyResponse, error)

CreateAPIKey mints one. The secret in the answer is shown exactly once: nothing stored can produce it again.

func (*Auth) CreateTenant

func (a *Auth) CreateTenant(
	ctx context.Context, identityToken string, in authwire.CreateTenantRequest, opts ...CallOption,
) (*authwire.SignInResponse, error)

CreateTenant makes a tenant and signs the holder of an identity token into it.

func (*Auth) EndIdentitySession

func (a *Auth) EndIdentitySession(
	ctx context.Context, identityToken string, opts ...CallOption,
) error

EndIdentitySession signs somebody out of the picker.

func (*Auth) EndImpersonation

func (a *Auth) EndImpersonation(ctx context.Context, opts ...CallOption) error

EndImpersonation ends it. The administrator signs in again afterwards: the session that was impersonating is gone, and it was the credential in hand.

func (*Auth) Impersonate

func (a *Auth) Impersonate(
	ctx context.Context, accountID uuid.UUID, opts ...CallOption,
) (*authwire.TokenPair, error)

Impersonate issues a session acting as another account, and installs it. What the caller was holding is replaced, so Auth.EndImpersonation is how to get back.

func (*Auth) Invitations

func (a *Auth) Invitations(
	ctx context.Context, opts ...CallOption,
) ([]authwire.InvitationView, error)

Invitations lists the live invitations into the caller's tenant.

func (*Auth) Invite added in v0.6.0

func (a *Auth) Invite(
	ctx context.Context, in authwire.InviteRequest, opts ...CallOption,
) (*authwire.InvitationView, error)

Invite asks an address to join the caller's tenant, and creates nothing in it.

Whoever holds the credential this call is made with is recorded as the inviter, which is what a landing page shows somebody who has not signed in yet. There is no way to say it was somebody else.

func (*Auth) Logout

func (a *Auth) Logout(ctx context.Context, opts ...CallOption) error

Logout ends the session the client is holding, and forgets it.

func (*Auth) MyInvitations

func (a *Auth) MyInvitations(
	ctx context.Context, identityToken string, opts ...CallOption,
) ([]authwire.InvitationToMeView, error)

MyInvitations lists the invitations waiting for the holder of an identity token, in every tenant.

func (*Auth) MyTenants

func (a *Auth) MyTenants(
	ctx context.Context, identityToken string, opts ...CallOption,
) ([]authwire.TenantView, error)

MyTenants lists the tenants the holder of an identity token belongs to. It is the picker somebody sees between signing in and choosing where to be.

func (*Auth) PreviewInvitation added in v0.6.0

func (a *Auth) PreviewInvitation(
	ctx context.Context, token string, opts ...CallOption,
) (*authwire.InvitationPreview, error)

PreviewInvitation says what an invitation link is for, without spending it.

Anonymous, and the point of it: it is what a landing page calls with the token out of its own URL, before anybody has signed in, so that it can say who invited you and where instead of showing a bare sign-in box.

The address that comes back is masked. Every way the token can be wrong — not found, already used, withdrawn, expired — is the same 404.

func (*Auth) Profile

func (a *Auth) Profile() AuthProfile

Profile is what the document says about this API's authentication.

func (*Auth) Provision

func (a *Auth) Provision(
	ctx context.Context, in authwire.ProvisionRequest, opts ...CallOption,
) (*authwire.AccountView, error)

Provision creates an account for somebody else, in the caller's tenant.

func (*Auth) Refresh

func (a *Auth) Refresh(
	ctx context.Context, refreshToken string, opts ...CallOption,
) (*authwire.TokenPair, error)

Refresh exchanges a refresh token for a new pair.

Session calls it; a caller rarely needs to. It is anonymous because the refresh token in the body is the credential, and the access token it is being swapped for is by then expired.

func (*Auth) RequestEmailCode added in v0.6.0

func (a *Auth) RequestEmailCode(
	ctx context.Context, emailAddress string, opts ...CallOption,
) error

RequestEmailCode asks for a sign-in code to be mailed.

It always succeeds, whether or not the address is registered: any difference in the answer is the account enumeration this endpoint would otherwise be used for. What it does report is a rate-limit refusal, which is about how often you have asked rather than about the address.

func (*Auth) ResendVerification

func (a *Auth) ResendVerification(ctx context.Context, opts ...CallOption) error

ResendVerification sends the verification mail again.

func (*Auth) RevokeAPIKey

func (a *Auth) RevokeAPIKey(ctx context.Context, id uuid.UUID, opts ...CallOption) error

RevokeAPIKey kills one.

func (*Auth) RevokeInvitation

func (a *Auth) RevokeInvitation(
	ctx context.Context, invitationID uuid.UUID, opts ...CallOption,
) error

RevokeInvitation withdraws one.

func (*Auth) RevokeSession

func (a *Auth) RevokeSession(ctx context.Context, id uuid.UUID, opts ...CallOption) error

RevokeSession ends one of them.

Pass Wide to end somebody else's, which needs `session.revoke.all`. Without it — and with it, for a session in another tenant — a session that is not the caller's answers NotFound rather than Forbidden, so an identifier cannot be probed for existence.

func (*Auth) Sessions

func (a *Auth) Sessions(ctx context.Context, opts ...CallOption) ([]authwire.SessionView, error)

Sessions lists the caller's own sessions, with the one making this request marked.

Pass Wide for every session open in the tenant, which needs `session.read.all`. There is one method rather than two, for the reason the parameter exists at all: two endpoints would let a client written against the narrow one keep working, silently, when its credential was widened.

func (*Auth) SignIn

SignIn types a mailed code back and installs the resulting session on the client.

It is Auth.VerifyEmailCode plus the bookkeeping every caller would otherwise write: from here on every request carries the access token, and the session refreshes itself before that token expires.

A person who belongs to no tenant gets a response with no session in it and an identity token instead — that is not a failure, it is the tenant picker. Check SignInResponse.AccessToken before assuming there is one.

func (*Auth) SwitchTenant

func (a *Auth) SwitchTenant(
	ctx context.Context, tenantID uuid.UUID, opts ...CallOption,
) (*authwire.TokenPair, error)

SwitchTenant issues a session for another tenant the caller belongs to, and installs it.

func (*Auth) Tenants

func (a *Auth) Tenants(ctx context.Context, opts ...CallOption) ([]authwire.TenantView, error)

Tenants lists every tenant the caller belongs to, reached with a session.

func (*Auth) VerifyEmail

func (a *Auth) VerifyEmail(ctx context.Context, token string, opts ...CallOption) error

VerifyEmail confirms an address with the token from the mail.

func (*Auth) VerifyEmailCode added in v0.6.0

func (a *Auth) VerifyEmailCode(
	ctx context.Context, in authwire.VerifyEmailCodeRequest, opts ...CallOption,
) (*authwire.SignInResponse, error)

VerifyEmailCode signs in with a mailed code and returns the answer without installing anything.

func (*Auth) WithTenant

func (a *Auth) WithTenant(tenantID uuid.UUID) CallOption

WithTenant names the tenant a sign-in is for.

A sign-in is the one call that cannot read the tenant from a credential — there is not one yet — so it is named here. Which header or parameter carries it is the project's decision, and it is in the profile.

type AuthProfile

type AuthProfile struct {
	// BasePath is where the endpoints sit, for example "/auth".
	BasePath string

	// AccessTTL is the lifetime of the token that travels on every request.
	AccessTTL time.Duration
	// RefreshTTL is how long an ordinary session lasts.
	RefreshTTL time.Duration
	// RememberTTL is how long one lasts when somebody asked to stay signed in.
	RememberTTL time.Duration
	// RotationLeeway is how long a refresh token stays usable after it has been
	// exchanged. It is also what this package refreshes ahead by: the server
	// having decided how much slack a swap deserves, a client has no business
	// picking a different number.
	RotationLeeway time.Duration
	// IdentityTTL is the lifetime of the tenant-less credential somebody holds
	// between signing in and picking a tenant.
	IdentityTTL time.Duration
	// CacheTTL is how long the server may go on answering out of memory after an
	// invalidation it never received. Zero means it caches nothing and every
	// request reads the row.
	//
	// It is not how long a revocation takes to take effect, and reading it that
	// way is the mistake worth naming. The server publishes an invalidation on
	// the transaction that revokes something, so the ordinary answer is "at
	// once"; this is the worst case for a replica that had lost the channel at
	// that moment.
	//
	// A client cannot act on either number. It is here so that somebody reading
	// the generated document can say how stale an answer could possibly be.
	CacheTTL time.Duration

	// TenantHeader is the header a sign-in names its tenant with, when the
	// project resolves tenants that way. Empty when it does not.
	TenantHeader string
	// TenantQuery is the query parameter that does the same, for the projects
	// configured to read one.
	TenantQuery string

	// HasEmailCode is POST <base>/email-code and POST <base>/email-code/verify:
	// whether this deployment signs anybody in with a mailed code.
	HasEmailCode bool
	// HasTenantCreation is POST <base>/tenants.
	HasTenantCreation bool
	// HasIdentitySessions is the tenant picker: /me/tenants and its siblings,
	// which exist only where identity sessions are configured.
	HasIdentitySessions bool
	// HasAPIKeys is the /api-keys routes.
	HasAPIKeys bool

	// OAuthProviders are the provider names with a sign-in route, in the order
	// the project listed them. Empty when there is no provider sign-in.
	OAuthProviders []string
}

AuthProfile is what the document says about this API's authentication.

Generated code declares one; nothing in it is a client's guess. The lifetimes are the ones the server enforces, which is what makes refreshing ahead of expiry possible at all: a client that had to discover the access lifetime by being refused would refresh only after a failed request, and every session would cost one wasted 401.

The Has flags are the routes rig mounts conditionally. A call to one that is not mounted is refused here, with a sentence saying which project setting would mount it — a bare 404 from the server says only that the URL is wrong.

type CallOption

type CallOption func(*call)

CallOption adjusts one call.

Every generated method takes these, which is what keeps a new header from being a regeneration: the document says what an endpoint takes, and a deployment's own headers are not part of that.

func Anonymous

func Anonymous() CallOption

Anonymous sends the call with no credential.

It is what a sign-in does — presenting an expired token to the endpoint that would have replaced it is how a client gets stuck — and what a public endpoint wants when the caller would rather not be identified by a stale session.

func Wide

func Wide() CallOption

Wide asks an endpoint for everything in the tenant rather than the caller's own rows.

It has to be asked for and it has to be held: the permission is per endpoint — `session.read.all`, `authlog.read.all` — and a caller who asks for more than they hold is refused rather than quietly given less. That refusal is the point. A narrower answer would leave a client unable to tell "you may not see that" from "there is nothing else", and drawing the wrong conclusion from a correct-looking response is worse than an error.

An option rather than a parameter, because it applies to a handful of the hand-written authentication endpoints and to every generated read, so a method signature per endpoint would be the same word spelled thirty times. A generated read also carries it as a typed field on its query, which is the same request written the other way.

func WithHeader

func WithHeader(key, value string) CallOption

WithHeader sets a header on this call, replacing any the client sends.

func WithIdempotencyKey

func WithIdempotencyKey(key string) CallOption

WithIdempotencyKey marks a retry of a write as the same write.

You rarely need it. The SDK names every write it might have to send twice, so a create that comes back 503 is already retried and already collapses to one row. This is for a key you would rather choose yourself — one derived from the data, the way an import job names a row by the line it came from, which deduplicates a re-run of the whole job in a way a fresh name cannot.

The same key with a different body is refused rather than answered, so a key has to name one request and not a slot to put requests in. See Retry.

A form body is the exception to "every write": the SDK does not name one, and does not repeat one — see [Op.writes]. A create that carries a file still records the key it is given, so choosing one here still collapses a re-run of your own; what it will not do is make the SDK send the upload again.

func WithIfNoneMatch

func WithIfNoneMatch(etag string) CallOption

WithIfNoneMatch asks for a file only if it has changed, quoting the tag from a previous Content.

The unchanged answer is 304 with no body, and it arrives as a Content whose Status says so rather than as an error — a caller who asked the question is owed the answer.

func WithQuery

func WithQuery(key, value string) CallOption

WithQuery sets a query parameter on this call, replacing one the method built.

func WithRange

func WithRange(start, end int64) CallOption

WithRange asks for part of a file, from start to end inclusive. A negative end means the rest of it.

It is only meaningful on a download, and it is an option rather than a generated parameter because the document says nothing about ranges: what an endpoint takes is what the document describes, and this is a question about one call. A server that honours it answers 206, which arrives as Content.Status.

func WithRequestID

func WithRequestID(id string) CallOption

WithRequestID names this call in the server's logs, overriding Config.RequestID.

func WithRetry

func WithRetry(attempts int) CallOption

WithRetry sets how many times this call may be sent, the first try included, overriding Config.Retry.

One sends it once, which is how a caller turns retries off for a call they would rather have fail than have repeated. Anything higher is for a call worth waiting on — a nightly job reading from a server that is mid-deploy — and it does not lengthen the call: the client's timeout, or WithTimeout, bounds the whole of it, retries and backoff included. So raising this without raising that buys more tries inside the same wall clock rather than more wall clock.

It does not make an unrepeatable call repeatable. A create is retried because it goes out under an WithIdempotencyKey the SDK generates, not because of this number; an upload is not retried at any number, because its body is a stream and the server does not record it against a key. See Retry.

func WithTimeout

func WithTimeout(d time.Duration) CallOption

WithTimeout bounds this call, replacing the client's own.

DefaultTimeout caps a whole exchange at thirty seconds, which is right for a JSON call and wrong for a hundred-megabyte upload. A context deadline cannot help, because http.Client.Timeout is a ceiling and not a default — so this takes a shallow copy of the client with a different one, sharing the transport. Raising the default instead would weaken every other route to suit the one that transfers files.

It bounds the call and not each attempt: a call the SDK had to send three times is sent three times inside this, which is what keeps ten minutes meaning ten minutes. See Retry.

type Config

type Config struct {
	// BaseURL is the origin the API is served from, for example
	// "https://api.example.com". A path on it is kept and the API's own base
	// path is appended, so a server behind "/gateway" is a base URL and not a
	// special case.
	BaseURL string

	// HTTPClient is the transport. The default has a thirty-second timeout,
	// which is a limit on the whole exchange rather than on a stalled read —
	// use a context for anything finer.
	//
	// Thirty seconds is right for a JSON call and wrong for a file, and a
	// context cannot raise it. A client that uploads or downloads at size wants
	// [WithTimeout] on those calls rather than a longer ceiling on every one.
	HTTPClient *http.Client

	// Credential is what authorizes each request: [StaticToken], [APIKey], or a
	// [Session] that keeps itself fresh. Nil sends no Authorization header,
	// which is what a public API or a header-authenticated demo wants.
	Credential Credential

	// Header is sent on every request. It is where a tenant header, a trace
	// header, or anything else a deployment adds belongs.
	Header http.Header

	// UserAgent identifies the caller in the server's logs. An SDK that does not
	// say who it is turns "which integration is hammering us" into a guess.
	UserAgent string

	// RequestID supplies the value of the request-ID header, so a client-side
	// log line and a server-side one can be joined. Nil sends none, and the
	// server generates its own.
	RequestID func() string

	// RequestIDHeader defaults to X-Request-Id, which is what the generated
	// server reads.
	RequestIDHeader string

	// OnRateLimit is called with what each response said about the caller's
	// budget, when the server said anything at all.
	//
	// It is the other half of rate limiting, and the half a client can act on.
	// A 429 is already handled without it — the SDK reads Retry-After and backs
	// off — but by then the call has been refused. These numbers arrive on every
	// response, so a caller that watches them can slow down, shed work, or raise
	// an alarm while its calls are still succeeding.
	//
	// Deliberately a callback and not automatic pacing. A client library that
	// silently slept because Remaining was low would turn a batch job's
	// throughput into a mystery, and it cannot know whether this caller would
	// rather go slower or fail sooner. The numbers are handed over; the policy
	// is the application's.
	//
	// Called on the request's own goroutine, once per attempt, before the call
	// returns. Keep it quick and do not call back into the client from it.
	OnRateLimit func(RateLimitStatus)

	// Revision overrides the API revision this client says it was built against.
	//
	// Almost nobody should set it. The generated client carries the revision
	// from the document it was generated from, which is the honest answer and
	// the one the server's logs are for; this is here for a client somebody
	// assembled by hand.
	Revision string

	// RevisionHeader overrides where the revision is sent. Empty takes the
	// generated client's, which is what the server it was generated with reads.
	RevisionHeader string

	// Now is the clock, for a test that has to cross a token expiry without
	// waiting ten minutes for one. Nil is [time.Now].
	Now func() time.Time

	// Trace runs one piece of work inside a span, and is how a caller that
	// traces gets this client's calls into the same trace as everything else it
	// does:
	//
	//	Trace: observe.Call,
	//
	// A function rather than a tracer, because this module is imported by every
	// generated client and most of them do not want an OpenTelemetry dependency
	// — the same argument that keeps otel out of rig/runtime. Anything with
	// this shape works; rig/observe supplies one.
	//
	// Two spans per call, and the nesting is the point. The outer one is the
	// operation, named by [Op.Name]; the inner ones are the attempts, and one
	// call can be several — the QUERY a proxy refused, the POST to the alias,
	// the send after a credential refreshed, and every [Retry]. Tracing the
	// attempts alone would show one call as a handful of unrelated requests.
	//
	// Nil traces nothing, which is what a client that was never told about
	// tracing does.
	Trace func(ctx context.Context, name string, f func(context.Context) error) error

	// Retry is how a call the server could not answer is sent again. The zero
	// value sends a call up to four times with a jittered backoff — a read and a
	// delete because they are repeatable by nature, a write because the SDK names
	// it with an Idempotency-Key first. A form body is the exception and is never
	// repeated. See [Retry].
	Retry Retry
}

Config is what a caller supplies. Every field but BaseURL has a default.

type Content

type Content struct {
	// Body is the bytes. Close it.
	Body io.ReadCloser

	// ContentType is what the server says the bytes are: the type it sniffed
	// when the file was uploaded, never the one the client claimed when it sent
	// it.
	ContentType string

	// Length is the number of bytes in Body, or -1 when the server did not say.
	// On the 206 that [WithRange] asks for it is the length of the range and
	// not of the file.
	Length int64

	// Filename is the name from Content-Disposition, or empty. It is a
	// suggestion and not a path: a program writing it to disk decides where, and
	// sanitizes what.
	Filename string

	// ETag identifies this version of the file, for a later call to quote back
	// through [WithIfNoneMatch].
	ETag string

	// Status is 200 for the whole file, 206 for the range [WithRange] asked
	// for, and 304 for a body the caller already has — in which case Body is
	// empty and closing it is all there is to do with it.
	Status int
}

Content is a response whose body is bytes rather than JSON.

Nothing here reads ahead. A download is streamed, so a file larger than memory can go to disk without passing through it — which is why this is not a []byte, and why Body is the caller's to close. Until it is closed the connection is not returned to the pool.

func DoContent

func DoContent(ctx context.Context, rt *Runtime, op Op, opts ...CallOption) (*Content, error)

DoContent performs a call whose response is bytes.

The returned Content owns an open connection, so a caller that does not close its Body leaks one. It is the one thing a generated download method cannot do for you: the whole point of streaming is that the bytes outlive the call.

A 304 comes back as a Content rather than as an error, because a caller who asked with WithIfNoneMatch is owed the answer they asked for. A 204 comes back as nil, nil, the same as Do.

type Credential

type Credential interface {
	// Apply adds whatever the request needs — an Authorization header, in every
	// implementation here. It may block, and it may fail, and a failure means
	// the request is not sent.
	Apply(ctx context.Context, r *http.Request) error
}

Credential is what authorizes a request.

One method, called once per request, which is what lets a credential that expires do its work without the call sites knowing: Session refreshes here, and everything above it goes on calling methods.

type Error

type Error struct {
	// Status is the HTTP status, for the cases where only it is meaningful — a
	// 502 from something in front of the server, say, which carries no code.
	Status int
	// Code is the machine-readable reason. Empty when the failure came from
	// something that is not a rig server.
	Code rigerr.Code
	// Message is prose, for a person. It is not meant to be parsed.
	Message string
	// RequestID correlates this failure with the server's logs. Quoting it in a
	// bug report is the difference between a search and a guess.
	RequestID string
	// Fields is present when the failure was validation, and is shaped like the
	// request body that failed — one member per field, holding what was wrong
	// with it.
	//
	// These are the bytes. A call made through a generated method comes back as
	// that method's own error — client.TodoCreateError, which is a [Failure] —
	// and has them decoded already. [FieldsAs] is for a call made by hand.
	Fields json.RawMessage
	// RetryAfter is how long the server asked the caller to wait, from the
	// header of the same name, in either form it takes: the seconds rig's own
	// server sends, and the date something in front of it might. Zero when it
	// said nothing, or asked for a moment already past.
	//
	// The SDK honours it for a call it may repeat — see [Retry] — so this is
	// mostly for the refusal that came back anyway, where the interval was
	// longer than the call had left to spend.
	RetryAfter time.Duration
	// Body is the start of the raw response, kept for a failure that decoded
	// into nothing useful — a proxy's HTML error page, say. It is bounded even
	// where the read was not, so a large validation failure is complete in
	// Fields and cut short here.
	Body string
}

Error is a request the server refused.

The code, not the status, is what to switch on. Three unrelated failures share a 400 and none of them share a code, which is the whole reason the generated server sends one.

func (*Error) Error

func (e *Error) Error() string

Error is the code and the message, prefixed with "rig: " so that a failure from the server is distinguishable at a glance from one the transport had.

type Failure

type Failure[F any] struct {
	*Refusal

	// Fields is what was wrong with each member of the body, and is nil when the
	// refusal was not about the body — which is every refusal but a 422. A 404
	// carries a code and a message and nothing to attach to a control, and a
	// zero-valued shape there would look like a body nobody complained about.
	//
	// It shadows [Error.Fields], which is the point: the typed value is what a
	// caller wants, and the bytes are still under Refusal for anything that
	// wants those.
	Fields *F
}

Failure is everything a refused call said, in one value: the envelope, and the per-field detail decoded into the shape of the body that went out.

F is that shape, and a caller does not name it — the generated client declares a function per call that does, so client.TodoCreateError(err) reads back what Todos.Create refused. Naming it by hand is what FieldsAs asks for, and it is why the per-call function exists: the wrong shape decodes perfectly and answers with an empty struct, because every member of a field-error shape is optional.

The envelope is embedded, so Code, Message, RequestID, Status and RetryAfter read exactly as they do on Error.

func As

func As[F any](err error) (*Failure[F], bool)

As reads a refusal back as the shape of the body that caused it.

It reports false for anything that is not a refusal — a request that never reached the server carries no envelope, so there is nothing for a code or a field to have come from — which is what makes

if refused, ok := client.TodoCreateError(err); ok {

the whole of the question. The generated per-call functions are one line each on top of this; reach for it directly only for a call made by hand, where nothing chose a shape for you.

func (*Failure[F]) Unwrap

func (f *Failure[F]) Unwrap() error

Unwrap returns the refusal, so errors.As reaches Error and CodeOf, FieldsAs and the Is predicates all answer about a Failure unchanged.

type Multipart

type Multipart struct {
	// JSON is encoded into the part named "json". Nil leaves the part out,
	// which is what a bare upload does: there is no row to send, only bytes for
	// a row that already exists.
	JSON any

	// Files are the file parts, in the order they are written.
	Files []Upload
}

Multipart is a multipart/form-data body: the row, and the files beside it.

It is the shape rig's own endpoints take rather than a general form encoder. The JSON part is written first and is named "json", because the server reads the parts in order and needs the row before it has anywhere to put the bytes — which is also why Files is a slice and not a map.

A generated method builds this; a caller supplies the Upload that goes in it. See Op.Multipart.

type Op

type Op struct {
	// Name is the operation this call is, as the document named it —
	// "listTodos", "createTodo". The generated method fills it in.
	//
	// It is what a call's span is called, which is why it is a field rather
	// than something derived from Method and Path: the path here already has
	// its identifiers substituted, so a name built from it would be a new name
	// for every row anybody ever fetched.
	Name string

	Method string
	Path   string
	Query  url.Values
	// Body is encoded as JSON when it is not nil. A pointer to a struct and the
	// struct itself both work; nil means no body at all, which is not the same
	// as an empty object.
	Body any

	// Multipart is a form body, sent instead of Body.
	//
	// The two are exclusive rather than ordered: an Op carrying both is a
	// generated method with a bug in it, and it is reported rather than resolved
	// by a precedence somebody would have to look up.
	//
	// A form is streamed, so it is sent chunked and carries no Content-Length.
	// Computing the envelope's length up front is possible and would mean
	// buffering nothing useful for a header almost nothing needs; an
	// intermediary that insists on one is a deployment problem, and buffering
	// every upload to satisfy it is not a trade this makes.
	Multipart *Multipart

	// Accept is the media type this call will take back. Empty means
	// application/json, which is every endpoint but a download: a download
	// answers with whatever the file turned out to be, and a client that
	// insisted on JSON would be asking for the one thing it is not.
	Accept string

	// Fallback is the path to POST to when Method is QUERY and something between
	// here and the server refuses it — the `_search` alias the router mounts
	// beside the QUERY route. Empty means there is none, and a refusal is
	// reported rather than worked around.
	Fallback string

	// Root says the path is relative to the server rather than to the API's base
	// path. The authentication endpoints are: /auth/login is mounted beside
	// /api/v1 and not inside it, because a sign-in is not a version of the
	// application's API.
	Root bool
}

Op is one call, as the generated method describes it.

The path is relative to the API's base path and already has its parameters substituted: escaping an identifier into a route is the generated method's job, because only it knows which argument goes where.

type Page

type Page[T any] struct {
	Items []T
	// Total is every row matching the query, ignoring pagination. It is what
	// says whether there is another page.
	Total int64
	// Offset is where this page started, as the server reported it.
	Offset int
}

Page is one page of a paginated read, as far as the iteration cares.

type RateLimitStatus

type RateLimitStatus struct {
	// Op is the operation that was called, as the document names it —
	// "listTodos". A gauge keyed on it says which call is spending the budget.
	Op string

	// Limit is how many calls the window allows, and Remaining how many are
	// left. Remaining is zero on the response that was refused.
	Limit     int
	Remaining int

	// ResetAfter is how long until the window frees, and is only stated on a
	// refusal — an allowed response says how much is left but not when it comes
	// back. Zero means the server did not say.
	ResetAfter time.Duration

	// Refused is true when this response was the 429 rather than a call that
	// went through.
	Refused bool
}

RateLimitStatus is what a response said about the caller's budget.

It is the tightest limit the caller is under rather than a list of every one that applied: the server evaluates each and reports the one closest to refusing, because that is the only number a client can act on.

func (RateLimitStatus) Fraction

func (s RateLimitStatus) Fraction() float64

Fraction is how much of the budget is spent, from 0 to 1.

It is the number worth alerting on, and it is here rather than left to the caller because the obvious arithmetic has a divide-by-zero in it: a response from a server with no limit configured carries no headers and leaves Limit at zero.

func (RateLimitStatus) Used

func (s RateLimitStatus) Used() int

Used is how much of the budget has been spent.

type Reauthorizer

type Reauthorizer interface {
	// Reauthorize reports whether it did something worth retrying for.
	Reauthorize(ctx context.Context) (bool, error)
}

Reauthorizer is a credential that might be able to recover from a refusal.

The transport calls it once, and only once, when a request comes back 401. A static token reports nothing to try; a session exchanges its refresh token.

type Refusal

type Refusal = Error

Refusal is Error under a second name, for embedding.

A struct that embedded *Error would get a field called Error, and a field at depth zero shadows the promoted method of the same name — so the struct would carry a refusal without being one. The alias is the whole of the fix: the field is called Refusal, Error stays a method, and there is still one error type rather than two that have to be kept in step.

type Retry

type Retry struct {
	// Attempts is how many times a call is sent, the first try included. Zero
	// takes [DefaultAttempts]; one sends it once and reports whatever came back.
	//
	// Four is three retries, and the first of them is immediate — see
	// [Retry.Base]. Beyond that a caller is waiting seconds on a call that was
	// probably going to fail anyway, and the calls worth waiting longer on are
	// the ones a caller knows about, which is what [WithRetry] is for.
	Attempts int

	// Base is the first backoff window, doubling per attempt after it. Zero
	// takes [DefaultRetryBase].
	//
	// The first retry does not use it: it goes out immediately, because the
	// commonest retryable failure there is — a pooled connection the server had
	// already closed — is fixed by opening another one, and waiting a second
	// first would only be slower. Everything after that waits.
	//
	// A wait is half the window plus a random share of the other half, rather
	// than the window. The failures worth retrying arrive in crowds — one
	// instance drained, one limiter window closed — and a hundred callers who
	// all waited exactly one second come back together and refuse the server
	// together, which is the thing the interval was for.
	Base time.Duration

	// Cap bounds one backoff window, however many attempts have already failed.
	// Zero takes [DefaultRetryCap].
	//
	// It does not bind at the default attempt count, which is why it is a field
	// rather than a number anybody has to choose: a caller who raises Attempts
	// to eight is asking for eight tries, not for a two-minute sleep in the
	// middle of them.
	Cap time.Duration
}

Retry is how a call the server could not answer is sent again, and how long the client waits in between.

It applies to the calls where sending the same request twice cannot mean two different things. A read and a delete are that by nature. A create and an update are made so: each one goes out named by an idempotency key, and a rig server that sees the same key twice answers the second one with what it answered the first rather than doing the work again.

An upload is not, and is the one write this never repeats. See [Op.writes].

type Runtime

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

Runtime carries one client's configuration and the state it accumulates.

One per client. The remembered QUERY decision below is the reason it is state rather than configuration: a client that learned an intermediary refuses QUERY should not have to learn it again on the next search.

func New

func New(cfg Config, api API) (*Runtime, error)

New builds a runtime. Generated code calls it; a caller reaches it through the generated New, which supplies the api argument from the document.

func (*Runtime) Auth

func (rt *Runtime) Auth() *Auth

Auth returns the authentication endpoints, or nil for a project that mounts none. Generated code exposes it as Client.Auth.

func (*Runtime) BaseURL

func (rt *Runtime) BaseURL() string

BaseURL is the origin requests go to.

func (*Runtime) Credential

func (rt *Runtime) Credential() Credential

Credential returns the credential in force, or nil.

func (*Runtime) Now

func (rt *Runtime) Now() time.Time

Now is the clock this client reads. A test can move it with Config.Now.

func (*Runtime) Session

func (rt *Runtime) Session() (*Session, bool)

Session returns the installed session, and whether the credential is one.

func (*Runtime) Use

func (rt *Runtime) Use(c Credential)

Use installs a credential, replacing whatever was there.

It is what signing in does, and what a caller does by hand when they already hold a token from somewhere else.

type Session

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

Session is a credential that keeps itself fresh.

It holds the pair a sign-in returned and exchanges the refresh token before the access token expires, using the lifetimes in AuthProfile — so a program that runs for a day makes a handful of refresh calls at moments of its own choosing, rather than discovering the expiry through a failed request in the middle of something.

It is safe for concurrent use, and a refresh is not: several goroutines arriving at once take turns, and the ones behind find the work already done instead of each spending a rotation. That matters more than it sounds — the server bounds how often a session may rotate, and a client that raced with itself would spend that budget on nothing.

func NewSession

func NewSession(tokens authwire.TokenPair) *Session

NewSession builds a session around a pair a sign-in returned.

The ordinary way to get one is Auth.SignIn, which installs it. This is for a program that stored the pair somewhere between runs.

func (*Session) Apply

func (s *Session) Apply(ctx context.Context, r *http.Request) error

Apply implements Credential. It refreshes first if the token is close enough to expiry that this request might outlive it.

func (*Session) Expired

func (s *Session) Expired(now time.Time) bool

Expired reports whether the session has nothing usable left: the refresh token's own lifetime has run out, and only a fresh sign-in will do.

func (*Session) Reauthorize

func (s *Session) Reauthorize(ctx context.Context) (bool, error)

Reauthorize implements Reauthorizer: a 401 despite a token that looked current is a revoked or invalidated one, and the refresh token is the only thing left to try.

It exchanges whatever the expiry says, because the expiry is exactly what has just been proved wrong.

func (*Session) SessionID

func (s *Session) SessionID() string

SessionID identifies the session, for showing it in a list of the caller's own and revoking it.

func (*Session) Tokens

func (s *Session) Tokens() authwire.TokenPair

Tokens returns the pair currently held, for a program that stores it between runs.

type StaticToken

type StaticToken string

StaticToken presents a token that does not change: an access token from somewhere else, or one issued for a script that runs for less time than the token lasts.

func (StaticToken) Apply

func (t StaticToken) Apply(_ context.Context, r *http.Request) error

Apply implements Credential.

type Upload

type Upload struct {
	// Field is the part's name, which is the field the server binds the bytes
	// to. A generated method fills it in from the document, through [Part];
	// setting it by hand is for a client somebody assembled themselves.
	Field string

	// Name is what the file is called. The server records it on the row and puts
	// it in the download path, and it never becomes the storage key — so a name
	// with a slash in it is a strange name and not a way out of the bucket.
	Name string

	// ContentType is what the caller claims the bytes are. Empty sends none, and
	// it makes little difference either way: the server sniffs the content and
	// the sniffed type is the one it stores and the one it serves back.
	ContentType string

	// Body is the bytes. A reader that can also seek can be sent twice, which is
	// what a credential refresh mid-upload needs — see [ErrCannotRetry].
	Body io.Reader
}

Upload is one file a caller is sending.

func Part

func Part(field string, u Upload) Upload

Part names an upload, for a generated method putting a caller's file into the field the document says it belongs in.

The caller supplies the name, the type and the bytes; only the generated method knows what the part is called on the wire, the same way only it knows which argument goes in which path segment.

func UploadBytes

func UploadBytes(name, contentType string, b []byte) Upload

UploadBytes is a file already in memory.

It is also the form that can always be sent twice, because a reader over a byte slice can seek. See ErrCannotRetry.

Jump to

Keyboard shortcuts

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