httpbind

package module
v0.1.16 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

tinybind-go

日本語

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

User guides: httpbind · jsonbind · configbind · htmlbind · sqlbind

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, kept in sync with binders and writers. 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
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
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

The CLI automatically discovers .tb.html and .tb.sql files in the target package and writes tinybind_templates_gen.go. SQL value expressions become driver arguments; PostgreSQL-style $1, $2, … placeholders are generated in encounter order:

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.

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.

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.
  • JSON-only generated code imports jsonbind only; the test matrix builds it with tinygo build -target wasm.
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 AssembleOpenAPI added in v0.1.11

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

AssembleOpenAPI merges every registered package fragment and returns deterministic OpenAPI 3.1 JSON and YAML documents.

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) (map[string]json.RawMessage, error)

BytesJSONMap decodes a full JSON document (bytes) as an object map.

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 json.RawMessage) ([]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 json.RawMessage) ([]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 json.RawMessage) ([]int64, error)

DecodeJSONInt64Slice decodes a JSON array of int64.

func DecodeJSONIntSlice

func DecodeJSONIntSlice(raw json.RawMessage) ([]int, error)

DecodeJSONIntSlice decodes a JSON array of ints.

func DecodeJSONMapStringString

func DecodeJSONMapStringString(raw json.RawMessage) (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 json.RawMessage) ([]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 OpenAPIYAML

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

OpenAPIYAML serves the assembled OpenAPI document as application/yaml.

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 json.RawMessage) ([]json.RawMessage, error)

RawJSONArray decodes a JSON array RawMessage into element raw values.

func RawJSONMap

func RawJSONMap(raw json.RawMessage) (map[string]json.RawMessage, error)

RawJSONMap decodes a JSON object RawMessage into a map of raw fields.

func ReadJSONMap

func ReadJSONMap(r *http.Request) (map[string]json.RawMessage, error)

ReadJSONMap decodes a JSON object body into a map of raw messages. Used by generated binders so they can pick named fields without reflect on T. Non-object JSON (arrays, scalars) 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 map[string]json.RawMessage, 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 RestJSONRaw

func RestJSONRaw(jsonBody map[string]json.RawMessage, exclude []string) map[string]json.RawMessage

RestJSONRaw builds map[string]json.RawMessage from leftover JSON object keys not in exclude.

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 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.
examples
demo command
Package htmlbind is the rendering runtime for generated HTML templates.
Package htmlbind is the rendering runtime for generated HTML templates.
internal
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 sqlbind provides generated, reflection-free database/sql row mapping.
Package sqlbind provides generated, reflection-free database/sql row mapping.
templates
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.

Jump to

Keyboard shortcuts

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