restkit

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

README

restkit

A dependency-light HTTP-JSON transport core for Go. It owns the repetitive parts of talking to a JSON API — request building, sending, status checking, JSON decoding, OpenTelemetry instrumentation, and typed errors — so hand-written API clients can focus on their own endpoints and types. restkit knows nothing about any specific API; everything API-specific (auth, header pins, query defaults) is injected through a RequestHook.

Features

  • Generic Do[T] that marshals the body, builds the request, runs hooks, sends, checks for a 2xx, and decodes the response into T.
  • Immutable, concurrency-safe Client with a 30s default timeout.
  • Transport wrapped in otelhttp, so every call is traced and metered via the global OpenTelemetry providers.
  • A single RequestHook extension point for auth, headers, and query — run client-wide first, then per-call.
  • Built-in query helpers: WithQuery (merges, not clobbers) and a nil-safe Values builder.
  • Three typed errors (ConfigError, RequestError, ResponseError), all matchable with errors.As.

Install

go get github.com/acidsailor/restkit

Requires Go 1.26+.

Quick start

package main

import (
	"context"
	"fmt"

	"github.com/acidsailor/restkit"
)

type Quote struct {
	Symbol string  `json:"symbol"`
	Price  float64 `json:"price"`
}

func main() {
	client, err := restkit.New("https://api.example.com")
	if err != nil {
		panic(err)
	}

	q, err := restkit.Do[Quote](
		context.Background(),
		client,
		"GET", "/quotes/ACME",
		nil, // request body; marshaled to JSON when non-nil
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s: %.2f\n", q.Symbol, q.Price)
}

Client construction

New(endpoint, ...Option) builds an immutable, concurrency-safe client. It trims a trailing / from the endpoint and wraps the HTTP transport with otelhttp.

client, err := restkit.New(
	"https://api.example.com",
	restkit.WithName("example"),          // attaches to every error's Name field
	restkit.WithHTTPClient(myHTTPClient), // override the default (30s timeout)
	restkit.WithHook(authHook),           // client-wide hook, runs on every call
)
Option Purpose
WithName Names this client; carried into every typed error ("restkit" by default).
WithHTTPClient Replaces the default *http.Client. A nil client is ignored.
WithHook Registers a client-wide RequestHook. Repeatable; runs in order.

Hooks

A RequestHook is func(*http.Request) error. It can mutate headers and req.URL (query), which makes it the single extension point for auth, header pins, and query parameters. Returning an error aborts the call as a RequestError with Op == OpHook.

Hooks run client-wide first, then per-call, so a per-call hook can override a client-wide default.

func authHook(r *http.Request) error {
	r.Header.Set("Authorization", "Bearer "+token)
	return nil
}

// Per-call hook passed as the trailing varargs of Do:
restkit.Do[Result](ctx, client, "GET", "/search", nil,
	restkit.WithQuery(url.Values{"q": {"acme"}}),
)
Query helpers

WithQuery is a built-in hook that merges values into the request's existing query rather than clobbering them, so a client-wide pin and a per-call filter compose.

Values is a generic, nil-safe url.Values builder. Each setter is a no-op for a nil pointer and returns the receiver, so calls chain — handy for building queries from a struct of optional fields:

q := restkit.NewValues().
	Str("symbol", filter.Symbol). // *string
	Int("limit", filter.Limit).   // *int
	Bool("active", filter.Active).// *bool
	Values                        // embedded url.Values

restkit.Do[Result](ctx, client, "GET", "/items", nil, restkit.WithQuery(q))

Setters: Str, Int, Int32, Int64, Float, Bool. For required or bespoke values, use the embedded url.Values.Set directly.

Errors

restkit returns three typed errors, all matchable with errors.As. Each carries the client Name.

  • ConfigError — invalid input to New (e.g. an empty endpoint).
  • RequestError — a per-call failure before a usable result. Its Op field names the failing stage via the Op* constants (OpMarshal, OpBuild, OpHook, OpSend, OpRead, OpUnmarshal). The underlying cause is reachable with errors.Is / errors.Unwrap.
  • ResponseError — a non-2xx response, carrying StatusCode and the raw response Body verbatim (the body is not decoded). It also exposes GetStatusCode() int.
result, err := restkit.Do[Result](ctx, client, "GET", "/items", nil)
if err != nil {
	var respErr *restkit.ResponseError
	if errors.As(err, &respErr) {
		log.Printf("API returned %d: %s", respErr.StatusCode, respErr.Body)
	}

	var reqErr *restkit.RequestError
	if errors.As(err, &reqErr) && reqErr.Op == restkit.OpSend {
		// network/transport failure — safe to retry
	}
	return err
}

License

Licensed under the GNU Affero General Public License v3. See LICENSE.

Documentation

Overview

Package restkit is a dependency-light HTTP-JSON transport core shared by hand-written API clients (moexoptcalc, alor, tinvest/rest). It owns the request lifecycle, client construction with otelhttp instrumentation, the Values/WithQuery query helpers, and three typed errors (ConfigError, RequestError, ResponseError), all matched with errors.As. Per-client behaviour (auth, header/query pins) is injected through RequestHook.

Index

Constants

View Source
const (
	OpMarshal   = "marshal"   // encoding the request body to JSON
	OpBuild     = "build"     // constructing the *http.Request
	OpHook      = "hook"      // a RequestHook returned an error
	OpSend      = "send"      // the HTTP round-trip
	OpRead      = "read"      // reading the response body
	OpUnmarshal = "unmarshal" // decoding the 2xx body into T
)

Op values name the failed RequestError stage, following the net.OpError / fs.PathError convention (a short verb). Match on these consts, not literals.

Variables

This section is empty.

Functions

func Do

func Do[T any](
	ctx context.Context,
	c *Client,
	method, path string,
	body any,
	hooks ...RequestHook,
) (T, error)

Do issues the request and decodes the 2xx JSON body into T. A non-2xx is a *ResponseError; any other stage failure is a *RequestError naming it.

func Pathf added in v0.2.0

func Pathf(format string, args ...string) string

Pathf builds a request path, percent-escaping each interpolated argument as a single path segment. It is the path-side counterpart to Values (which escapes query parameters): pass a format like "/v1/accounts/%s/orders/%s" with raw parameter values, and a value containing '/', '?', '#', or a space is escaped rather than corrupting the URL (injecting a segment, starting a query string, etc.).

Every arg is interpolated with %s; use fmt.Sprintf directly if you need other verbs. An empty arg yields an empty segment (the server decides how to handle it) — Pathf validates escaping, not presence.

Types

type Client

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

Client is an immutable, concurrency-safe HTTP-JSON transport core. Build with New; issue calls via the package-level Do.

func New

func New(endpoint string, opts ...Option) (*Client, error)

New trims the endpoint, resolves the *http.Client, wraps its transport with otelhttp (global tracer/meter providers), and builds the immutable Client. Returns a *ConfigError for an empty endpoint.

type ConfigError

type ConfigError struct {
	Name   string // client name from WithName; "restkit" by default
	Reason string // e.g. "empty endpoint"
}

ConfigError reports invalid input to New (an empty endpoint). Match with errors.As.

func (*ConfigError) Error

func (e *ConfigError) Error() string

type Option

type Option func(*config)

Option configures a Client at construction time.

func WithHTTPClient

func WithHTTPClient(h *http.Client) Option

WithHTTPClient overrides the default *http.Client (30s timeout, stdlib transport). A nil client is ignored, leaving the default in place.

func WithHook

func WithHook(h RequestHook) Option

WithHook registers a client-wide RequestHook, run on every call before any per-call hooks. Repeatable; runs in registration order. Use it for auth, header pins, or a query pin.

func WithName

func WithName(name string) Option

WithName sets the name carried into every typed error's Name field, so a program using several clients can attribute an error. Defaults to "restkit".

type RequestError

type RequestError struct {
	Name string
	Op   string // one of the Op* consts
	Err  error
}

RequestError reports a per-call failure before a usable result. Like *url.Error: Op names the stage, Err wraps the cause. Match with errors.As; the cause stays reachable via errors.Is (Unwrap).

func (*RequestError) Error

func (e *RequestError) Error() string

func (*RequestError) Unwrap

func (e *RequestError) Unwrap() error

type RequestHook

type RequestHook func(*http.Request) error

RequestHook mutates a built request before it is sent. It reaches headers and req.URL (query), covering auth, header pins, and query. Returning an error aborts the call as a *RequestError with Op == OpHook.

func WithQuery

func WithQuery(q url.Values) RequestHook

WithQuery returns a hook that merges q into the request's existing query, so a client-wide pin and a per-call filter compose rather than clobber.

type ResponseError

type ResponseError struct {
	Name       string
	StatusCode int
	Body       string
}

ResponseError reports a non-2xx HTTP response, carrying the status and raw body verbatim (the error envelope is not decoded). Match with errors.As.

func (*ResponseError) Error

func (e *ResponseError) Error() string

func (*ResponseError) GetStatusCode

func (e *ResponseError) GetStatusCode() int

GetStatusCode lets callers read the status via an interface{ GetStatusCode() int } probe without importing this type.

type Values

type Values struct{ url.Values }

Values is a generic, nil-safe url.Values builder. Each setter skips a nil pointer and returns the receiver so calls chain. The embedded url.Values provides Set for required/bespoke values (e.g. a formatted Date).

func NewValues

func NewValues() Values

NewValues returns an empty builder.

func (Values) Bool

func (v Values) Bool(key string, p *bool) Values

func (Values) Float

func (v Values) Float(key string, p *float64) Values

func (Values) Int

func (v Values) Int(key string, p *int) Values

func (Values) Int32

func (v Values) Int32(key string, p *int32) Values

func (Values) Int64

func (v Values) Int64(key string, p *int64) Values

func (Values) Str

func (v Values) Str(key string, p *string) Values

Jump to

Keyboard shortcuts

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