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
- Variables
- func CodeOf(err error) rigerr.Code
- func Do[T any](ctx context.Context, rt *Runtime, op Op, opts ...CallOption) (*T, error)
- func DoNoContent(ctx context.Context, rt *Runtime, op Op, opts ...CallOption) error
- func FieldsAs[T any](err error) (T, bool)
- func IsConflict(err error) bool
- func IsForbidden(err error) bool
- func IsInvalid(err error) bool
- func IsNotFound(err error) bool
- func IsRateLimited(err error) bool
- func IsTooLarge(err error) bool
- func IsUnauthorized(err error) bool
- func IsUnavailable(err error) bool
- func IsUnsupportedMediaType(err error) bool
- func IsUpgradeRequired(err error) bool
- func P[T any](v T) *T
- func Paginate[T any](ctx context.Context, offset int, ...) iter.Seq2[T, error]
- func PathValue(v string) string
- func SetBool(q url.Values, key string, v *bool)
- func SetFloat64(q url.Values, key string, v *float64)
- func SetInt(q url.Values, key string, v *int)
- func SetInt64(q url.Values, key string, v *int64)
- func SetString[T ~string](q url.Values, key string, v *T)
- func SetStrings[T ~string](q url.Values, key string, vs []T)
- func SetTime(q url.Values, key string, v *time.Time)
- func SetUUID(q url.Values, key string, v *uuid.UUID)
- type API
- type APIKey
- type AuditQuery
- type Auth
- func (a *Auth) APIKeys(ctx context.Context, opts ...CallOption) ([]authwire.APIKeyView, error)
- func (a *Auth) AcceptInvitation(ctx context.Context, in authwire.AcceptRequest, opts ...CallOption) (*authwire.TokenPair, error)
- func (a *Auth) AcceptMyInvitation(ctx context.Context, identityToken string, invitationID uuid.UUID, ...) (*authwire.SignInResponse, error)
- func (a *Auth) AuditLog(ctx context.Context, q AuditQuery, opts ...CallOption) (*authwire.Page[authwire.AuthLogEntryView], error)
- func (a *Auth) AuditLogAll(ctx context.Context, q AuditQuery, opts ...CallOption) iter.Seq2[authwire.AuthLogEntryView, error]
- func (a *Auth) CreateAPIKey(ctx context.Context, in authwire.CreateKeyRequest, opts ...CallOption) (*authwire.CreateKeyResponse, error)
- func (a *Auth) CreateTenant(ctx context.Context, identityToken string, in authwire.CreateTenantRequest, ...) (*authwire.SignInResponse, error)
- func (a *Auth) EndIdentitySession(ctx context.Context, identityToken string, opts ...CallOption) error
- func (a *Auth) EndImpersonation(ctx context.Context, opts ...CallOption) error
- func (a *Auth) Impersonate(ctx context.Context, accountID uuid.UUID, opts ...CallOption) (*authwire.TokenPair, error)
- func (a *Auth) Invitations(ctx context.Context, opts ...CallOption) ([]authwire.InvitationView, error)
- func (a *Auth) Invite(ctx context.Context, in authwire.InviteRequest, opts ...CallOption) (*authwire.InvitationView, error)
- func (a *Auth) Logout(ctx context.Context, opts ...CallOption) error
- func (a *Auth) MyInvitations(ctx context.Context, identityToken string, opts ...CallOption) ([]authwire.InvitationToMeView, error)
- func (a *Auth) MyTenants(ctx context.Context, identityToken string, opts ...CallOption) ([]authwire.TenantView, error)
- func (a *Auth) PreviewInvitation(ctx context.Context, token string, opts ...CallOption) (*authwire.InvitationPreview, error)
- func (a *Auth) Profile() AuthProfile
- func (a *Auth) Provision(ctx context.Context, in authwire.ProvisionRequest, opts ...CallOption) (*authwire.AccountView, error)
- func (a *Auth) Refresh(ctx context.Context, refreshToken string, opts ...CallOption) (*authwire.TokenPair, error)
- func (a *Auth) RequestEmailCode(ctx context.Context, emailAddress string, opts ...CallOption) error
- func (a *Auth) ResendVerification(ctx context.Context, opts ...CallOption) error
- func (a *Auth) RevokeAPIKey(ctx context.Context, id uuid.UUID, opts ...CallOption) error
- func (a *Auth) RevokeInvitation(ctx context.Context, invitationID uuid.UUID, opts ...CallOption) error
- func (a *Auth) RevokeSession(ctx context.Context, id uuid.UUID, opts ...CallOption) error
- func (a *Auth) Sessions(ctx context.Context, opts ...CallOption) ([]authwire.SessionView, error)
- func (a *Auth) SignIn(ctx context.Context, in authwire.VerifyEmailCodeRequest, opts ...CallOption) (*authwire.SignInResponse, error)
- func (a *Auth) SwitchTenant(ctx context.Context, tenantID uuid.UUID, opts ...CallOption) (*authwire.TokenPair, error)
- func (a *Auth) Tenants(ctx context.Context, opts ...CallOption) ([]authwire.TenantView, error)
- func (a *Auth) VerifyEmail(ctx context.Context, token string, opts ...CallOption) error
- func (a *Auth) VerifyEmailCode(ctx context.Context, in authwire.VerifyEmailCodeRequest, opts ...CallOption) (*authwire.SignInResponse, error)
- func (a *Auth) WithTenant(tenantID uuid.UUID) CallOption
- type AuthProfile
- type CallOption
- func Anonymous() CallOption
- func Wide() CallOption
- func WithHeader(key, value string) CallOption
- func WithIdempotencyKey(key string) CallOption
- func WithIfNoneMatch(etag string) CallOption
- func WithQuery(key, value string) CallOption
- func WithRange(start, end int64) CallOption
- func WithRequestID(id string) CallOption
- func WithRetry(attempts int) CallOption
- func WithTimeout(d time.Duration) CallOption
- type Config
- type Content
- type Credential
- type Error
- type Failure
- type Multipart
- type Op
- type Page
- type RateLimitStatus
- type Reauthorizer
- type Refusal
- type Retry
- type Runtime
- type Session
- type StaticToken
- type Upload
Constants ¶
const DefaultAttempts = 4
DefaultAttempts is how many times a repeatable call is sent when Config.Retry says nothing: the first try and three more.
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.
const DefaultRequestIDHeader = "X-Request-Id"
DefaultRequestIDHeader is where the generated server looks for a caller's own request identifier.
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.
const DefaultRetryCap = 2 * time.Second
DefaultRetryCap bounds one backoff window. See Retry.Cap.
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.
const DefaultTimeout = 30 * time.Second
DefaultTimeout bounds a whole request when the caller supplied no client of their own.
const DefaultUserAgent = "rig-go-client"
DefaultUserAgent is what a client that did not name itself sends.
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.
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 ¶
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.
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 Do ¶
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 ¶
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 ¶
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 ¶
IsConflict reports whether the request contradicted the current state.
func IsForbidden ¶
IsForbidden reports whether the caller is known but not permitted.
func IsInvalid ¶
IsInvalid reports whether a well-formed request failed validation, which is the failure FieldsAs has something to say about.
func IsNotFound ¶
IsNotFound reports whether the server answered that there is no such thing. A row in another tenant is the same answer, deliberately.
func IsRateLimited ¶
IsRateLimited reports whether the caller was asked to slow down.
func IsTooLarge ¶
IsTooLarge reports whether the body was larger than the endpoint accepts.
func IsUnauthorized ¶
IsUnauthorized reports whether no valid credential was presented.
func IsUnavailable ¶ added in v0.6.0
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 ¶
IsUnsupportedMediaType reports whether the body's content type was refused.
func IsUpgradeRequired ¶
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 ¶
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 SetFloat64 ¶
SetFloat64 writes a floating-point parameter.
func SetString ¶
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 ¶
SetStrings writes a repeated string parameter, one key per value.
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.
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.
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 ¶
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 ¶
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 ¶
func (a *Auth) SignIn( ctx context.Context, in authwire.VerifyEmailCodeRequest, opts ...CallOption, ) (*authwire.SignInResponse, error)
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 ¶
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 ¶
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.
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 ¶
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.
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 ¶
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 ¶
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 ¶
Auth returns the authentication endpoints, or nil for a project that mounts none. Generated code exposes it as Client.Auth.
func (*Runtime) Credential ¶
func (rt *Runtime) Credential() Credential
Credential returns the credential in force, or nil.
func (*Runtime) Now ¶
Now is the clock this client reads. A test can move it with Config.Now.
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 ¶
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 ¶
Apply implements Credential. It refreshes first if the token is close enough to expiry that this request might outlive it.
func (*Session) Expired ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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.