httpbind

package module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Aug 5, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

tinybind-go

日本語

Reflection-free, code-generation-first binding for TinyGo and standard Go. Runtime dependencies are isolated into HTTP, JSON, SQL, and DynamoDB packages.

User guides: httpbind · jsonbind · configbind · htmlbind · sqlbind · dynamobind · firestorebind · reloadable components

Building a framework on top of this? Start with framework facilities, the index of what is available to you and what is not, then htmlbind for framework owners.

Define request/response structs once. The generator emits type-specific binders and writers, so the same model covers JSON, form, multipart, and query (plus path / header / cookie via tags). Responses adapt to the client Accept (and streaming negotiation where used). From the same analysis it also generates OpenAPI 3.1 (JSON), kept in sync with binders and writers, with godoc comments carried into summary / description. Route registration is discovered by static analysis of real net/http styles (HandleFunc, Handle, method values, wrappers, and so on)—not by a separate DSL.

type CreateUserRequest struct {
	// input = query + payload (JSON / form / multipart). Tag may be omitted.
	Name  string `input:"name"`  // same as untagged: Name string
	Email string `input:"email"` // same as untagged: Email string
	OrgID string `path:"org_id"`
	Token string `header:"Authorization"`
}

type CreateUserResponse struct {
	ID    string `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
	OrgID string `json:"org_id"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
	input, err := httpbind.Bind[CreateUserRequest](r)
	if err != nil {
		httpbind.WriteError(w, r, err)
		return
	}
	// Name/Email: query and/or JSON/form/multipart body (input).
	// OrgID from path, Token from Authorization header.
	out := CreateUserResponse{
		ID:    "u_1",
		Name:  input.Name,
		Email: input.Email,
		OrgID: input.OrgID,
	}
	_ = httpbind.Write[CreateUserResponse](w, r, out)
}

Run the generator on the package (binders + OpenAPI embed):

go run ./cmd/tinybind-gen generate -dir . -openapi

The same generation pass also supports CLI-only application subcommands through configbind.SubCommand[T], including required, optional, and rest positional arguments. See the configbind subcommand guide.

Struct tag reference

Wire name defaults to the lower-camel field name when a tag value is omitted (e.g. untagged Name → "name").

Tag Source Notes
(none) or input:"name" query + payload Default. Payload covers JSON, application/x-www-form-urlencoded, and multipart/form-data. Tag is optional when the field is plain user input.
query:"page" query only Not read from the body.
payload:"name" body only JSON / form / multipart by Content-Type. Not read from the query string.
payload:"image" on httpbind.File multipart file part Binds filename, content type, size, and bytes from the named part. Payload-only (not query). Multipart bodies are capped at 1 MiB by default; override with httpbind.SetMaxMultipartBodyBytes.
path:"org_id" path parameter Matches {org_id} (or equivalent) in the route pattern.
header:"Authorization" request header Header name is the tag value.
cookie:"session" cookie Cookie name is the tag value.

input vs payload vs query

  • Prefer input (or no tag) for normal fields that may arrive as query or body.
  • Use query / payload only when you must restrict the origin (e.g. search filters in the query string, body-only JSON fields).
  • payload is not the same as input: it does not accept query parameters.

Example that mixes restrictions:

type SearchRequest struct {
	Keyword string `query:"keyword"`   // query only
	Page    int    `query:"page"`
	Filter  string `payload:"filter"`  // body only (JSON/form/multipart)
}

Response structs commonly use standard json:"..." names for encoding; request binding still uses the source tags above.

Streaming (ideal API)
stream, err := httpbind.NewStream[ChatEvent](w, r)
if err != nil {
    httpbind.WriteError(w, r, err)
    return
}
defer stream.Close()

_ = stream.Write(ChatEvent{Type: "delta", Delta: "hi"})
_ = stream.Write(ChatEvent{Type: "done"})
  • Write can be called many times (incremental events).
  • Format is chosen once in NewStream from ?stream=, Accept, User-Agent, then default NDJSON.
  • Formats:
    • SSE — text/event-stream
    • NDJSON / JSONL — application/x-ndjson (one object per line; not a JSON array)
    • JSON array — application/json as [obj1,obj2,...] (Close writes the trailing ])
  • Do not use removed helpers WriteNDJSON / WriteSSE.

Packages

Path Role
. (package httpbind) Runtime: Bind / Write / WriteError / NewStream / OpenAPI serve / SwaggerUI
jsonbind/ Standalone DecodeJSON / EncodeJSON runtime; does not import net/http or database/sql
sqlbind/ ScanRows runtime and row helpers; does not import net/http
dynamobind/ DynamoDB item runtime over tinygodriver/nosql/dynamodb; does not import net/http or database/sql
firestorebind/ Firestore Datastore-mode entity runtime over tinygodriver/nosql/datastore; does not import net/http or database/sql
generator/ Field-plan binders/writers + OpenAPI 3.1 + template generation
parser/ Route/handler discovery (Bind, Write, NewStream, errors)
templates/htmlbind/ Typed, context-safe HTML template compiler
templates/sqlbind/ Typed, parameterized SQL template compiler
templates/firestorebind/ Typed Firestore access-pattern declarations (.tb.firestore)
cmd/tinybind-gen CLI: binders + OpenAPI + templates from a package dir
examples/demo End-to-end sample app
internal/* Test fixtures
testdata/cmd/* Dev-only helpers (not for distribution; under testdata so go get / ./... skip them)
go run ./cmd/tinybind-gen generate -dir ./path/to/package

Every generated file records a // tinybind:generated comment holding the SHA-256 of the inputs that produced it, so a run whose package sources, templates, go.mod, options, and generator binary all hash to the recorded value exits without regenerating. -force regenerates regardless. See docs/httpbind.md.

The CLI automatically discovers .tb.html and .tb.sql files in the target package and writes tinybind_templates_gen.go. A package containing SQL templates must name its database with -sql-dialect postgresql, mysql, or sqlite; there is no default. SQL value expressions become driver arguments, and placeholders are generated in encounter order in the style that dialect requires — $1, $2, … for PostgreSQL and ? for MySQL and SQLite:

package store

type User { id: int, name: string }

export statement FindUser(id: int): sql.optional<User> {
SELECT id, name FROM users WHERE id = {id}
}

This generates both BuildFindUser(id) (Statement, error) and the FindUser(ctx, db, id) (*User, error) convenience API. The SQL compiler also supports sql.exec, sql.one<T>, sql.many<T>, private sql.predicate composition, private sql.relation<T> subqueries, conditional clauses, and array value-list expansion. Hand-authored placeholders and unguarded UPDATE/DELETE statements are rejected.

sql.many<T> streams rows as iter.Seq2[T, error] without first allocating a result slice. Query, scan, and iteration errors are yielded as the error value, and stopping the range early closes the underlying sql.Rows:

for user, err := range FindUsers(ctx, db, filter) {
    if err != nil {
        return err
    }
    // consume user
}

Web frameworks can opt in to executor-from-Context wrappers while the explicit db APIs remain available:

go run ./cmd/tinybind-gen generate -dir ./path/to/package -sql-context-api

The generated FindUsersContext(ctx, filter) resolves the *sql.DB, *sql.Conn, or *sql.Tx installed with sqlbind.WithSQLExecutor. This allows transaction middleware to keep the executor inside its callback Context:

web.Transaction(func(ctx context.Context) error {
    for user, err := range FindUsersContext(ctx, filter) {
        if err != nil {
            return err
        }
        // consume user within the transaction
    }
    return nil
})

Custom generator commands may use a framework-owned Context key by setting a resolver with the signature func(context.Context) (SQLExecutor, error); setting it also enables the Context wrappers:

options.SQLExecutorResolver = &generator.SymbolPattern{
    PackagePath: "example.com/web/dbctx",
    Name:        "Executor",
}

Frameworks may wrap the runtime functions and still make those calls visible to the generator. Register the wrapper's package identity, semantic operation, and the zero-based positions of only the type/value roles that the generator needs:

package main

import "github.com/shibukawa/tinybind-go/generator"

func main() {
    calls := generator.NewCallRegistry()
    if err := calls.Register(
        // func RegisterConfig[T any](ctx context.Context, name string) *T
        generator.ConfigBindCall(
            generator.Function("example.com/framework", "RegisterConfig"),
            generator.GenericType("config", 0),
            generator.Argument("prefix", 1),
        ),
        // func Created(ctx context.Context, w http.ResponseWriter, value any) error
        generator.ResponseWriteStatusCall(
            generator.Function("example.com/framework", "Created"),
            generator.ArgumentType("response", 2),
            generator.Constant("status", 201),
        ),
    ); err != nil {
        panic(err)
    }
    options, err := calls.Options(generator.DefaultOptions())
    if err != nil {
        panic(err)
    }
    generator.Main(generator.MustCommandSet(generator.GenerateCommand(options)))
}

Extra wrapper arguments do not need to be described. Use GenericType when the model comes from a generic type argument, ArgumentType when it comes from a value argument's static type, Argument for a runtime value such as a config prefix or route pattern, and Constant when the wrapper hides a fixed value such as status 201. The available operations have matching constructors: RequestBindCall, ResponseWriteCall, ResponseWriteStatusCall, StreamCreateCall, JSONDecodeCall, JSONEncodeCall, RowsScanCall, ConfigBindCall, ConfigSubCommandCall, RouteRegisterCall, and ErrorResponseCall. Function targets package functions; Method targets named-receiver methods.

The required role names, in the same order, are request; response; response + status; stream; decode; encode; row; config + prefix; config + name + help; pattern + handler; and status.

RuntimePackages remains a shorthand for functions with the standard tinybind names and signatures. Use explicit call patterns for renamed wrappers, reordered arguments, extra arguments, or hidden constants. generator.Options{} deliberately has no discovery identities. Add a feature to DisableFeatures to prevent discovery even under -generate-all.

A framework can combine the built-in generate command with its own lifecycle commands:

commands := generator.MustCommandSet(
    generator.GenerateCommand(options),
    generator.Command{Name: "init", Summary: "initialize a project", Run: runInit},
    generator.Command{Name: "build", Summary: "generate and build", Run: runBuild},
    generator.Command{Name: "watch", Summary: "watch, generate, and build", Run: runWatch},
)
generator.Main(commands)

Each command receives a context.Context, arguments, and injected CommandIO containing stdin/stdout/stderr, working directory, and environment. A build or watch implementation can generate in-process without invoking a CLI:

result, err := generator.New(options).GeneratePackage(ctx, generator.GenerateRequest{
    Dir: dir, OpenAPI: true,
})

GeneratePackage runs template, mapping, configbind, and optional OpenAPI generation and returns the written paths. generator.Main is only the outer process boundary; tests and composed commands should call CommandSet.Run or GeneratePackage directly.

Generation is usage-aware: a package that only calls DecodeJSON[T] gets only its JSON decoder, imports jsonbind, and does not import the root HTTP runtime or net/http. Set Options.GenerateAll for the legacy all-enabled-mappings mode. Compatible multipart file aliases can be listed in Options.FileTypes.Set.

Standalone JSON uses the dependency-isolated package:

value, err := jsonbind.DecodeJSON[Document](reader)
err = jsonbind.EncodeJSON(writer, value)

JSON reads are capped at 1 MiB by default. Use jsonbind.SetMaxJSONBodyBytes globally or jsonbind.DecodeJSONLimit per call. jsonbind returns transport-neutral errors; httpbind.Bind maps an oversized HTTP request to status 413.

Joined SQL rows can be grouped into an object tree with generated, reflection-free ScanRows[T] code:

type Organization struct {
    ID    int    `db:"organization_id" groupkey:""`
    Name  string `db:"organization_name"`
    Users []User
}
type User struct {
    ID   int    `db:"user_id" groupkey:""`
    Name string `db:"user_name"`
}

organizations, err := sqlbind.ScanRows[Organization](rows)

Every grouped struct level has one groupkey field. Repeated keys merge into the same object; a NULL child key represents an absent outer-join child.

Formatting templates

.tb.html, .tb.sql, and .tb.dynamo are file formats this module invented, so no editor knows how to format them. The generator ships the formatter:

go run ./cmd/tinybind-gen fmt -w -dir ./store

-l lists the files that would change and exits non-zero, which is the CI form. -as sql (or html, dynamo) filters one source from stdin to stdout, which is what an editor "format on save" hook needs.

What it does per format:

  • SQL — one clause per line, CTE bodies and subqueries indented under their own SELECT, JOIN and its ON split, AND/OR aligned when a condition list is long. Keyword case, literals, and comments are left exactly as written.
  • HTML — one tag per line inside head, table, and the other positions where the HTML parser discards whitespace anyway. Elsewhere a line break only replaces whitespace that was already there, so <b>a</b><i>b</i> stays glued and rendering never changes. pre, textarea, script, style, and any preserve-whitespace subtree are copied byte for byte.
  • DynamoDB — table then key, one clause per line.

A source that does not parse is reported and left untouched. Everything the command does is available as a library:

import "github.com/shibukawa/tinybind-go/templates/templatefmt"

formatted, err := templatefmt.Source("users.tb.sql", source, templatefmt.Options{})
results, err := templatefmt.Dir("./store", templatefmt.Options{Width: 120})

templatefmt.Dir reads but never writes; each Result reports whether the file would change and carries Write() for when you want it applied.

Demo

go generate ./examples/demo
go run ./examples/demo
# http://localhost:8080/       index + browser stream demo
# http://localhost:8080/docs/  Swagger UI
# http://localhost:8080/chat   NewStream (SSE / NDJSON / JSON array auto)

See examples/demo/README.md for full curl recipes.

Benchmarks

Generated code has no reflection to drive and no intermediate map[string]any to build, which is where the difference comes from. Measured on an Apple M3, Go 1.26.5, darwin/arm64, best of 10 runs.

Each pair produces the same output: the JSON codecs are checked against encoding/json by differential fuzzing, and the handler and template pairs are asserted equivalent by the tests sitting beside the benchmarks. Reproduce with:

go test ./internal/benchfixture -run xxx -bench . -benchmem
Throughput

The document is a 312-byte order with a nested object, a three-element array of objects, and a string array. The page is a five-row user list.

Path Standard library Generated
JSON decode (io.Reader) 3447 ns · 1688 B · 30 allocs 777 ns · 856 B · 15 allocs
JSON decode (json.Unmarshal, bytes in hand) 3287 ns · 888 B · 25 allocs —
JSON encode 579 ns · 144 B · 1 alloc 272 ns · 0 B · 0 allocs
Bind + Write (request reused) 850 ns · 1584 B · 17 allocs 584 ns · 1021 B · 16 allocs
Bind + Write (incl. request construction) 1695 ns · 7445 B · 31 allocs 1422 ns · 6883 B · 30 allocs
HTML render (html/template vs htmlbind) 7346 ns · 2705 B · 107 allocs 930 ns · 464 B · 4 allocs

The JSON comparisons are against encoding/json; the handler row against a hand-written net/http handler doing the same decode, path, and header reads; the HTML row against an html/template template rendering the same document.

Encoding allocates nothing: generated encoders append into a pooled buffer, so a response costs no garbage at all. Decoding's 15 allocations are the 13 strings and slices that end up in the result, plus the body buffer and its reader — there is nothing left to remove without changing what the caller gets back. The HTML render's four allocations are per render rather than per row — the bound fragment, the options, the renderer, and its conversion buffer — so a longer page costs the same four.

Binary size

The same small JSON program built two ways, once over encoding/json and once over generated jsonbind codecs. jsonbind does not import encoding/json at all, so the reflection-based codec never enters the binary.

Build encoding/json jsonbind Saved
go build 3,075,522 2,617,426 −458 KB (−14.9%)
go build -ldflags="-s -w" 2,061,010 1,741,186 −320 KB (−15.5%)
tinygo build -target wasi 1,345,144 867,687 −477 KB (−35.5%)
tinygo build -target wasi -no-debug 496,869 252,388 −244 KB (−49.2%)

Stripping makes the gap matter more, not less: once debug information is gone, the reflection machinery is a larger share of what is left. On a stripped TinyGo wasm build it is about half the binary.

encoding/json/v2

encoding/json/v2 is still behind GOEXPERIMENT=jsonv2 on Go 1.26, so a library cannot import it unconditionally. It was measured anyway, because the obvious question is whether generated codecs should target it instead.

GOEXPERIMENT=jsonv2 go test ./internal/benchfixture -run xxx -bench JSON -benchmem
Path v1, flag off v1, flag on v2 API Generated
decode (io.Reader) 3543 ns · 1688 B · 30 2536 ns · 1889 B · 18 1650 ns · 544 B · 11 799 ns · 856 B · 15
decode (bytes in hand) 3352 ns · 888 B · 25 1871 ns · 496 B · 10 1525 ns · 496 B · 10 —
encode 587 ns · 144 B · 1 1330 ns · 1824 B · 11 943 ns · 288 B · 2 274 ns · 0 B · 0

Turning the flag on and changing nothing else is a real improvement for decoding, because the v1 API is reimplemented over v2 — but watch the encode row, which gets 2.3× slower and allocates 12× more. The flag is not free either way.

Generating onto jsontext, the v2 tokenizer, was the interesting option: the same key-switch shape driven by ReadToken lands on 13 allocations with a reused decoder — exactly what jsonbind.Parser allocates — but takes 1320 ns to do it, and 1804 ns · 1600 B · 38 allocs when the decoder is constructed per call, as a codec entry point would have to.

Size settles it. On the same small program, the experiment costs:

Build Flag off Flag on
go build 3,075,522 3,887,730 (+26%)
go build -ldflags="-s -w" 2,061,010 2,598,722 (+26%)
tinygo build -target wasi 1,345,144 2,217,774 (+65%)
tinygo build -target wasi -no-debug 496,869 881,891 (+78%)

A stripped wasm build with the experiment on is 3.5× the size of the same program on jsonbind. For a library whose first-class target is TinyGo, that rules v2 out as a dependency, and nothing in the speed columns argues for carrying a second implementation behind a build tag to get it.

TinyGo

TinyGo is a first-class target for generated binding code. The JSON runtime is kept independent of net/http so it can be used on js/wasm toolchains where TinyGo's standard-library HTTP path is unavailable.

Verified with TinyGo 0.41.1 + Go 1.26.x.

./scripts/tinygo-check.sh
Runtime notes relevant to TinyGo
  • AsHTTPError avoids errors.As (unimplemented AssignableTo on some TinyGo builds).
  • WriteError hand-builds problem JSON (avoids fragile nested encoding/json + RawMessage interactions).
  • Registry uses reflect.Type only as a type identity key, not for field walking.
  • Generated bind/write code does not import reflect.
  • jsonbind parses and writes JSON itself and does not import encoding/json, so a JSON-only binary carries no reflection-based codec — around a third of a tinygo build -target wasi binary, and about half of a -no-debug one. See Benchmarks.
  • Do not build with GOEXPERIMENT=jsonv2. encoding/json/v2 is still behind the experiment on Go 1.26, and on TinyGo it grows the same wasi binary by about 60% while jsonbind never calls it.
Known limitations
Topic Limitation
Toolchain Project baseline is TinyGo 0.41.1 + Go 1.26.x
js/wasm HTTP TinyGo 0.41.1 + Go 1.26.x fails inside net/http/roundtrip_js.go; use jsonbind for HTTP-free WASM code
Streaming Prefer host go test for NewStream; not fully TinyGo-matrixed
ServeMux DefaultOptions discovers both net/http.ServeMux and tinygodriver/httpmux.ServeMux; use httpmux for Go 1.22 method and wildcard routing under TinyGo
Multipart File Supported via httpbind.File (payload); size/MIME check rules deferred. Body cap defaults to 1 MiB (SetMaxMultipartBodyBytes)
SQL mapping ScanRows and generated SQL scanners target host Go and are excluded from TinyGo builds
Generator Host-side only (go run / go test)

License

Licensed under the Apache License, Version 2.0.

Documentation

Overview

Package httpbind provides generated, reflection-free net/http request and response binding. Standalone JSON and SQL mapping live in the jsonbind and sqlbind packages so consumers do not compile unrelated platform dependencies.

Index

Constants

View Source
const DefaultMaxJSONBodyBytes = jsonbind.DefaultMaxJSONBodyBytes

DefaultMaxJSONBodyBytes is the default cap for JSON document reads (1 MiB).

View Source
const DefaultMaxMultipartBodyBytes int64 = 1 << 20

DefaultMaxMultipartBodyBytes is the default cap on multipart request bodies enforced by ParseMultipartMap (1 MiB). Override with SetMaxMultipartBodyBytes. Without this, io.ReadAll / unrestricted ParseMultipartForm would accept arbitrarily large bodies inside tinybind-go alone.

View Source
const DefaultMultipartMaxMemory int64 = 32 << 20

DefaultMultipartMaxMemory is the maxMemory argument passed to http.Request.ParseMultipartForm (how much of the form stays in RAM before spilling file parts to temp files). This is not a body size cap; see DefaultMaxMultipartBodyBytes.

Variables

This section is empty.

Functions

func AppendFileJSON added in v0.4.0

func AppendFileJSON(dst []byte, f File) []byte

AppendFileJSON appends an uploaded file the way encoding/json rendered it before generated encoders stopped going through reflection: exported fields in declaration order, with the content base64-encoded.

func AssembleOpenAPI added in v0.1.11

func AssembleOpenAPI() (jsonDoc []byte, err error)

AssembleOpenAPI merges every registered package fragment and returns a deterministic OpenAPI 3.1 JSON document.

func BadRequest

func BadRequest(problem Problem, cause ...error) error

BadRequest returns a 400 Bad Request error.

func Bind

func Bind[T any](r *http.Request) (T, error)

Bind maps an HTTP request into a typed request value. Dispatch uses a registry of generated binders; field mapping does not use reflect.

func BindError

func BindError(field, location, message string) error

BindError is returned when binding fails for a specific field/source.

func BytesJSONMap

func BytesJSONMap(data []byte) (*jsonbind.Object, error)

BytesJSONMap splits a full JSON object document into its raw fields.

func CheckDate

func CheckDate(s string) bool

CheckDate reports whether s is an ISO date (YYYY-MM-DD / time.DateOnly).

func CheckDateTime

func CheckDateTime(s string) bool

CheckDateTime reports whether s is RFC3339 (or RFC3339Nano on failure).

func CheckEmail

func CheckEmail(s string) bool

CheckEmail reports whether s is a pragmatic (non-RFC5322) email. Empty string returns false; callers skip empty optional fields before calling.

func CheckTime

func CheckTime(s string) bool

CheckTime reports whether s is an ISO time (HH:MM:SS / time.TimeOnly).

func CheckUUID

func CheckUUID(s string) bool

CheckUUID reports whether s is a UUID string (8-4-4-4-12 hex with dashes). Version/variant bits are not enforced.

func Conflict

func Conflict(problem Problem, cause ...error) error

Conflict returns a 409 Conflict error.

func CookieValue

func CookieValue(r *http.Request, name string) (string, bool)

CookieValue returns a cookie value if present.

func DecodeJSONBool

func DecodeJSONBool(raw json.RawMessage) (bool, error)

DecodeJSONBool unmarshals a JSON raw value as bool.

func DecodeJSONBoolSlice

func DecodeJSONBoolSlice(raw []byte) ([]bool, error)

DecodeJSONBoolSlice decodes a JSON array of bools.

func DecodeJSONFloat64

func DecodeJSONFloat64(raw json.RawMessage) (float64, error)

DecodeJSONFloat64 unmarshals a JSON raw value as float64.

func DecodeJSONFloat64Slice

func DecodeJSONFloat64Slice(raw []byte) ([]float64, error)

DecodeJSONFloat64Slice decodes a JSON array of float64.

func DecodeJSONInt

func DecodeJSONInt(raw json.RawMessage) (int, error)

DecodeJSONInt unmarshals a JSON raw value as int.

func DecodeJSONInt64

func DecodeJSONInt64(raw json.RawMessage) (int64, error)

DecodeJSONInt64 unmarshals a JSON raw value as int64.

func DecodeJSONInt64Slice

func DecodeJSONInt64Slice(raw []byte) ([]int64, error)

DecodeJSONInt64Slice decodes a JSON array of int64.

func DecodeJSONIntSlice

func DecodeJSONIntSlice(raw []byte) ([]int, error)

DecodeJSONIntSlice decodes a JSON array of ints.

func DecodeJSONMapStringString

func DecodeJSONMapStringString(raw []byte) (map[string]string, error)

DecodeJSONMapStringString decodes a JSON object with string values.

func DecodeJSONString

func DecodeJSONString(raw json.RawMessage) (string, error)

DecodeJSONString unmarshals a JSON raw value as string.

func DecodeJSONStringSlice

func DecodeJSONStringSlice(raw []byte) ([]string, error)

DecodeJSONStringSlice decodes a JSON array of strings.

func Forbidden

func Forbidden(problem Problem, cause ...error) error

Forbidden returns a 403 Forbidden error.

func HeaderValue

func HeaderValue(r *http.Request, key string) string

HeaderValue returns a request header.

func Internal

func Internal(err error) error

Internal returns a 500 Internal Server Error that wraps err.

func IsFormRequest

func IsFormRequest(r *http.Request) bool

IsFormRequest reports application/x-www-form-urlencoded.

func IsJSONRequest

func IsJSONRequest(r *http.Request) bool

IsJSONRequest reports whether the request body should be treated as JSON. Matches application/json, text/json, and *+json types such as application/problem+json (RFC 7807 / RFC 9457).

func IsMultipartRequest

func IsMultipartRequest(r *http.Request) bool

IsMultipartRequest reports multipart/form-data.

func MaxJSONBodyBytes

func MaxJSONBodyBytes() int64

MaxJSONBodyBytes returns the effective JSON body limit.

func MaxMultipartBodyBytes

func MaxMultipartBodyBytes() int64

MaxMultipartBodyBytes returns the effective global multipart body limit.

func NotFound

func NotFound(problem Problem, cause ...error) error

NotFound returns a 404 Not Found error.

func OpenAPIJSON

func OpenAPIJSON(w http.ResponseWriter, r *http.Request)

OpenAPIJSON serves the assembled OpenAPI document as application/json.

func ParseBool

func ParseBool(s string) (bool, error)

ParseBool converts a string to bool.

func ParseFloat64

func ParseFloat64(s string) (float64, error)

ParseFloat64 converts a string to float64.

func ParseFormMap

func ParseFormMap(r *http.Request) (map[string]string, error)

ParseFormMap parses urlencoded form body into a flat map (first value wins).

func ParseInt

func ParseInt(s string) (int, error)

ParseInt converts a string to int.

func ParseInt64

func ParseInt64(s string) (int64, error)

ParseInt64 converts a string to int64.

func ParseMultipartMap

func ParseMultipartMap(r *http.Request) (form map[string]string, files map[string]File, err error)

ParseMultipartMap parses a multipart/form-data body into scalar form fields (first value wins) and named file parts (first file wins per field name).

The request body is capped at MaxMultipartBodyBytes() so tinybind-go itself enforces a size limit (default 1 MiB): Content-Length is checked when known, r.Body is wrapped with http.MaxBytesReader, and per-file reads use LimitReader. Oversized bodies and oversize file parts map to HTTP 413.

func PathValue

func PathValue(r *http.Request, key string) string

PathValue returns the path value for key (Go 1.22+ ServeMux).

func PayloadTooLarge

func PayloadTooLarge(problem Problem, cause ...error) error

PayloadTooLarge returns a 413 Payload Too Large error.

func QueryValue

func QueryValue(r *http.Request, key string) (string, bool)

QueryValue returns the first query parameter value for key.

func RawJSONArray

func RawJSONArray(raw []byte) ([][]byte, error)

RawJSONArray splits a JSON array into its raw element values.

func RawJSONMap

func RawJSONMap(raw []byte) (*jsonbind.Object, error)

RawJSONMap splits a JSON object value into its raw fields.

func ReadJSONObject added in v0.4.0

func ReadJSONObject(r *http.Request) (*jsonbind.Object, error)

ReadJSONObject splits a JSON object body into its raw fields.

Generated binders need random access by name, because a field may also come from the query string or a form and the tag decides which source wins. The returned Object holds subslices of the body, so this costs one pass and one slice rather than a map plus a copy of every member.

Non-object JSON (arrays, scalars, null) fails with 400 — required when payload:"*" rest maps are used.

func RegisterBind

func RegisterBind[T any](fn func(*http.Request) (T, error))

RegisterBind registers a generated binder for T. Call from generated init(); field mapping lives entirely inside fn.

func RegisterOpenAPIFragment added in v0.1.11

func RegisterOpenAPIFragment(id string, jsonDoc []byte)

RegisterOpenAPIFragment registers a generated package fragment. ID should be the package import path. Assembly reports conflicting repeated IDs.

func RegisterWrite

func RegisterWrite[T any](fn func(http.ResponseWriter, *http.Request, T) error)

RegisterWrite registers a generated writer for T.

func ResetOpenAPIFragments added in v0.1.11

func ResetOpenAPIFragments()

ResetOpenAPIFragments clears registered fragments. It is intended for tests.

func RestFormAny

func RestFormAny(formBody map[string]string, exclude []string) map[string]any

RestFormAny builds map[string]any from leftover form keys not in exclude (string values).

func RestFormRaw

func RestFormRaw(formBody map[string]string, exclude []string) map[string]json.RawMessage

RestFormRaw builds map[string]json.RawMessage from leftover form keys (JSON-encoded strings).

func RestJSONAny

func RestJSONAny(jsonBody *jsonbind.Object, exclude []string) (map[string]any, error)

RestJSONAny builds map[string]any from leftover JSON object keys not in exclude. Nested JSON values are decoded into any (objects/arrays/numbers/bools/strings/null). Prefer non-nil empty map when nothing remains.

func RestJSONNames added in v0.4.0

func RestJSONNames(jsonBody *jsonbind.Object, exclude []string) []string

RestJSONNames lists leftover JSON object keys not in exclude, so generated code can fill a map[string]json.RawMessage without this package converting between map types.

func SetMaxJSONBodyBytes

func SetMaxJSONBodyBytes(n int64)

SetMaxJSONBodyBytes changes the process-wide JSON body limit. A non-positive value restores DefaultMaxJSONBodyBytes.

func SetMaxMultipartBodyBytes

func SetMaxMultipartBodyBytes(n int64)

SetMaxMultipartBodyBytes sets the global multipart body size limit used by ParseMultipartMap (and generated binders). The limit wraps r.Body with http.MaxBytesReader and bounds per-file reads.

n > 0  → use n bytes
n <= 0 → restore DefaultMaxMultipartBodyBytes (1 MiB)

func SetOpenAPIInfo added in v0.1.11

func SetOpenAPIInfo(info OpenAPIInfo) error

SetOpenAPIInfo sets application-level metadata for the assembled document. Repeating the same value is harmless; a different second value is an error.

func SwaggerUI

func SwaggerUI(specURL string) http.Handler

SwaggerUI returns an http.Handler that serves a minimal Swagger UI page loading the OpenAPI document from specURL (e.g. "/openapi.json").

Assets are loaded from a public CDN; this handler does not embed Swagger UI binaries. Mount freely, e.g.:

mux.Handle("GET /docs/{$}", httpbind.SwaggerUI("/openapi.json"))

func Unauthorized

func Unauthorized(problem Problem, cause ...error) error

Unauthorized returns a 401 Unauthorized error.

func Validation

func Validation(fields ...FieldError) error

Validation returns a 400 Bad Request validation error with field details.

func Write

func Write[T any](w http.ResponseWriter, r *http.Request, value T) error

Write serializes a typed response value to the HTTP response via a registered writer. Status is always 200 OK; use WriteStatus for other success codes.

func WriteError

func WriteError(w http.ResponseWriter, r *http.Request, err error)

WriteError writes err as an RFC 9457 Problem Details response. Internal causes are not exposed in the client body.

JSON is written without encoding/json for the problem document so TinyGo does not hit unimplemented reflect.AssignableTo when binders also use json.RawMessage (a known interaction in TinyGo's encoding/json).

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any) error

WriteJSON is a helper for generated writers: encode a pre-built map/slice without reflecting over application structs. Content-Type is application/json.

func WriteJSONBytes added in v0.4.0

func WriteJSONBytes(w http.ResponseWriter, status int, data []byte) error

WriteJSONBytes writes an already-encoded document. Generated writers build the body into a pooled buffer and hand it over here, so the response path never reflects over the value and never allocates an intermediate map.

func WriteStatus

func WriteStatus[T any](w http.ResponseWriter, r *http.Request, status int, value T) error

WriteStatus serializes value with an explicit HTTP status code using the registered encoder for T (no field-walking reflection on T). For status 204 No Content, the body is not written.

Types

type FieldError

type FieldError struct {
	Field    string
	Location string
	Message  string
}

FieldError describes a single field-level validation failure.

func Field

func Field(field, location, message string) FieldError

Field builds a field-level validation error.

type File

type File struct {
	Filename    string
	ContentType string
	Size        int64
	Content     []byte
}

File is an uploaded file bound from a multipart/form-data part. After a successful bind, Filename, ContentType (when the client sent one), Size, and Content are populated from the named file part.

func (File) Empty

func (f File) Empty() bool

Empty reports whether f has no filename and no content.

type HTTPError

type HTTPError struct {
	Status  int
	Title   string
	Problem Problem
	Fields  []FieldError
	// contains filtered or unexported fields
}

HTTPError is an HTTP-mapped error with optional RFC 9457 details and cause.

func AsHTTPError

func AsHTTPError(err error) (*HTTPError, bool)

AsHTTPError extracts *HTTPError from err if present. Implemented without errors.As so TinyGo does not require reflect.AssignableTo (unimplemented for interfaces in TinyGo 0.40), which otherwise panics when Bind's json.RawMessage path is also linked into the same binary.

func (*HTTPError) Error

func (e *HTTPError) Error() string

func (*HTTPError) Unwrap

func (e *HTTPError) Unwrap() error

type OpenAPIInfo added in v0.1.11

type OpenAPIInfo struct {
	Title   string
	Version string
}

OpenAPIInfo is application-owned metadata for the assembled document.

type Problem

type Problem struct {
	Code    string
	Message string
}

Problem is an application error payload carried by status helpers.

type Stream

type Stream[T any] struct {
	// contains filtered or unexported fields
}

Stream is a typed incremental response stream.

Ideal handler usage:

stream, err := httpbind.NewStream[ChatEvent](w, r)
if err != nil { ... }
defer stream.Close()
_ = stream.Write(ChatEvent{Type: "delta", Delta: "hi"})
_ = stream.Write(ChatEvent{Type: "done"})

Format (SSE vs NDJSON vs JSON array) is chosen once by rule:stream-content-negotiation. Write may be called many times; headers/status are sent only in NewStream. JSON array framing requires Close (via defer) so the trailing ']' is written.

func NewStream

func NewStream[T any](w http.ResponseWriter, r *http.Request) (*Stream[T], error)

NewStream negotiates transport format from the request, writes response headers and 200 once, and returns a stream for incremental Write calls.

func (*Stream[T]) Close

func (s *Stream[T]) Close() error

Close marks the stream finished. Idempotent. For JSON array format, Close writes the trailing ']' (or "[]" if no Write). SSE and NDJSON do not require a special trailer; still call Close for symmetry.

func (*Stream[T]) Format

func (s *Stream[T]) Format() StreamFormat

Format returns the negotiated stream format (sse | ndjson | json-array).

func (*Stream[T]) Write

func (s *Stream[T]) Write(v T) error

Write encodes one event in the negotiated format. Callable many times; does not re-send HTTP status or headers.

type StreamFormat

type StreamFormat string

StreamFormat is the negotiated on-the-wire format for Stream[T].

const (
	// StreamSSE is text/event-stream (data: <json>\n\n).
	StreamSSE StreamFormat = "sse"
	// StreamNDJSON is application/x-ndjson (one JSON object per line).
	// Same family as JSONL / NDJSON; not a single JSON array document.
	StreamNDJSON StreamFormat = "ndjson"
	// StreamJSONArray is application/json as one JSON array document:
	// [obj1,obj2,...] with items appended incrementally and closed by Close.
	StreamJSONArray StreamFormat = "json-array"
)

func NegotiateStreamFormat

func NegotiateStreamFormat(r *http.Request) StreamFormat

NegotiateStreamFormat selects SSE, NDJSON, or JSON array using:

  1. ?stream= query
  2. Accept
  3. User-Agent heuristics
  4. default NDJSON

Exported for tests and advanced callers.

Note: NDJSON/JSONL (line-delimited objects) is distinct from JSON array (a single [...] document). application/json selects the array form; application/x-ndjson / application/jsonl select NDJSON.

Directories

Path Synopsis
Package cliparser parses argv into a map of stable config keys using precomputed flag definitions.
Package cliparser parses argv into a map of stable config keys using precomputed flag definitions.
codegen
Package codegen emits Go source that constructs cliparser.Def slices for Bind-style fields.
Package codegen emits Go source that constructs cliparser.Def slices for Bind-style fields.
cmd
tinybind-gen command
Package configbind loads Bind-style config from defaults, TOML, env, and CLI into structs.
Package configbind loads Bind-style config from defaults, TOML, env, and CLI into structs.
codegen
Package codegen emits reflection-free configbind apply, flag, and env key tables.
Package codegen emits reflection-free configbind apply, flag, and env key tables.
Package configpath resolves one config file path for configbind.
Package configpath resolves one config file path for configbind.
Package dynamobind provides typed, reflection-free DynamoDB item binding on top of github.com/shibukawa/tinygodriver/nosql/dynamodb.
Package dynamobind provides typed, reflection-free DynamoDB item binding on top of github.com/shibukawa/tinygodriver/nosql/dynamodb.
examples
demo command
Package firestorebind provides typed, reflection-free entity binding for Firestore in Datastore mode, on top of github.com/shibukawa/tinygodriver/nosql/datastore.
Package firestorebind provides typed, reflection-free entity binding for Firestore in Datastore mode, on top of github.com/shibukawa/tinygodriver/nosql/datastore.
Package htmlbind is the rendering runtime for generated HTML templates.
Package htmlbind is the rendering runtime for generated HTML templates.
Package htmlupdate serves HTML templates that can update themselves in place.
Package htmlupdate serves HTML templates that can update themselves in place.
internal
benchfixture
Package benchfixture backs the benchmark table in the project README.
Package benchfixture backs the benchmark table in the project README.
contextscan
Package contextscan names the package-level functions in a directory whose first parameter is a context.Context.
Package contextscan names the package-level functions in a directory whose first parameter is a context.Context.
dynamofixture
Package dynamofixture exercises the generated DynamoDB item codec against the dynamobind runtime and the driver's wire protocol.
Package dynamofixture exercises the generated DynamoDB item codec against the dynamobind runtime and the driver's wire protocol.
firestorefixture
Package firestorefixture exercises the generated Firestore entity codec against the firestorebind runtime and the driver's wire protocol.
Package firestorefixture exercises the generated Firestore entity codec against the firestorebind runtime and the driver's wire protocol.
gensource
Package gensource identifies the Go files a generation run wrote.
Package gensource identifies the Go files a generation run wrote.
godoc
Package godoc extracts documentation text from Go doc comments so host-side analysis can carry it into generated artifacts such as OpenAPI descriptions.
Package godoc extracts documentation text from Go doc comments so host-side analysis can carry it into generated artifacts such as OpenAPI descriptions.
pagesfixture/pages/archive
Package archive is the route that needs the request itself rather than the address it was sent to.
Package archive is the route that needs the request itself rather than the address it was sent to.
Package jsonbind provides generated, reflection-free JSON document codecs.
Package jsonbind provides generated, reflection-free JSON document codecs.
Package minitoml parses a restricted TOML subset into a flat intermediate key/value form.
Package minitoml parses a restricted TOML subset into a flat intermediate key/value form.
codegen
Package codegen emits reflection-free apply functions from intermediate minitoml.Document keys.
Package codegen emits reflection-free apply functions from intermediate minitoml.Document keys.
Package routetree discovers a filesystem route tree and derives the stdlib ServeMux patterns, ancestor layout chains, and Go package names for it.
Package routetree discovers a filesystem route tree and derives the stdlib ServeMux patterns, ancestor layout chains, and Go package names for it.
Package sqlbind provides generated, reflection-free database/sql row mapping.
Package sqlbind provides generated, reflection-free database/sql row mapping.
templates
dynamobind
Package dynamobind parses and prints typed DynamoDB access-pattern sources.
Package dynamobind parses and prints typed DynamoDB access-pattern sources.
firestorebind
Package firestorebind parses and prints typed Firestore Datastore access-pattern sources.
Package firestorebind parses and prints typed Firestore Datastore access-pattern sources.
htmlbind
Package htmlbind parses typed HTML template sources into an AST.
Package htmlbind parses typed HTML template sources into an AST.
internal/rawparse
Package rawparse provides the lossless dummy format parser used to exercise the shared template parser without HTML or SQL knowledge.
Package rawparse provides the lossless dummy format parser used to exercise the shared template parser without HTML or SQL knowledge.
sqlbind
Package sqlbind parses and compiles typed SQL template sources.
Package sqlbind parses and compiles typed SQL template sources.
templatefmt
Package templatefmt formats tinybind template sources.
Package templatefmt formats tinybind template sources.

Jump to

Keyboard shortcuts

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