Skip to content

Releases: raven-clown/tack

v0.9.0

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 14:13

The release candidate for v1.0 and the public launch. The public API in this release is the API v1.0 freezes, unless feedback during the two-week window (until 2026-10-25) shows a reason to change it. Every exported identifier was reviewed and kept (API_REVIEW.md); the three open questions are decided in DESIGN.md D11; MIGRATION.md has the before and after code for the one breaking change.

Breaking

  • The ShutdownTimeout constant is replaced by Config.ShutdownTimeout, whose zero value keeps the ten seconds (DESIGN.md D11.1). Migration: delete references to tack.ShutdownTimeout, or set tack.Config{ShutdownTimeout: 10 * time.Second} where the value was read.

Security

  • Every response tack writes (JSON, problems, the document, the docs page, and event streams) now carries X-Content-Type-Options: nosniff; the docs page also sends Referrer-Policy: no-referrer. The shared header values cost no allocation.

Added

  • bench/, its own module: the same pet API on net/http, Gin, Echo, Huma, and tack with one harness, an equivalence test that proves every variant enforces the same rules, and bench/run.sh to rerun it. Results, including every case where tack is slower, are in BENCHMARKS.md.
  • SECURITY.md gains a "Defaults" section: body limits, content types, hidden internal errors, panics, headers, the timeouts Run sets, shutdown, docs exposure, and what CI scans.
  • FUTURE_FIT.md and internal/futurefit: the planned add-ons (API versioning, typed pagination, WebSocket, tackdb, tackcache, tackauth) written against the v0.9 API as compiling, running sketches. All six fit without changing an exported identifier; the extension points they would like are listed as additive (DESIGN.md D12).

v0.6.3

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 13:11

A hardening release: no public API changes.

Fixed

  • Compiled validate checks now agree with the validator in every case a review found: a non-nil pointer counts as present whatever it points to (*bool false with required passes, *int zero with omitempty,gte=1 fails), bounds are parsed the way the validator parses them (float32 at 32 bits, integers as int64, oneof options compared as the validator spells the value), required or omitempty after another rule keeps the validator's order, and a rule on the Body field or on a json:"-" field no longer vanishes: such inputs run the validator.
  • A validate rule the validator does not know inside a body struct panics at registration with the route and the rule, instead of on the first request.
  • form fields on a GET or DELETE route, which net/http never reads, are refused at registration.
  • A stream handler behind a ResponseWriter that cannot flush gets a 500 instead of a silent empty 200; the message names the Unwrap method the middleware needs. A stream item that only embeds or points to tack.Event is rejected at registration instead of panicking.
  • A response body that cannot be encoded sends the 500 problem without the handler's headers, and with Config.Dev a success Status the route does not document logs a warning.
  • App.Serve returns nil, as documented, when the caller shuts the server down itself.
  • A pointer field with validate:"required" is no longer documented as nullable in the OpenAPI document, the TypeScript client, or the Zod schemas, since the server rejects null.
  • tsgen refuses a component schema named like a global the client relies on (Response, Error, Headers, Blob, FormData, Promise, Record, and the rest), with the fix, instead of generating a file that does not type-check.
  • Zod date-time schemas accept offsets (.datetime({ offset: true })), which Go writes for non-UTC times; regenerate schemas.ts.
  • tack mock reports a path ServeMux cannot register, such as /files/{name}.json, instead of crashing; its preflight answer allows the headers the browser asked for, so Authorization works; example sizes from minLength and minItems are capped. The schema decoder tolerates a null entry in anyOf.
  • tack dev kills the whole process tree of the command after --, so a server started through go run no longer survives a restart and holds the port; a file that disappears between the directory listing and the stat no longer ends the loop; tack gen ts -h prints the usage.
  • The CodeQL finding in examples/auth: the demo password comes from AUTH_PASSWORD with a documented default and is no longer logged.

Changed

  • Request bodies are read into pooled buffers and results are encoded through a pooled pointer, so the encoder no longer copies every result; problem responses use the same pool and a shared Content-Type value. On the machine that measured it, a bound route went from 2 allocations to 1, a JSON body route from 5 to 2, and a validation error from 10 to 8; see BENCHMARKS.md. TestAllocations holds each shape to its budget.
  • The compiled checks live in internal/bind next to the plan; cmd/tack has one file per subcommand. The public API is unchanged.
  • Security scanning: the Security workflow runs gosec on the library and on cmd/ separately, excluding for the command only the path, permission, subprocess, and request rules a CLI cannot satisfy; golangci-lint runs gosec with the same split; the two findings in the library are annotated with their reasons. Scorecard's open items are the ones a one-day-old single-maintainer project cannot close yet.
  • Tests: six fuzz targets (query scanner against url.ParseQuery, JSON and form binding, header lists, path patterns, OpenAPI path helpers) run for 20 seconds each in CI; the README's Go blocks are compiled against the checkout; goleak guards the stream, server, and tack dev tests.
  • API_REVIEW.md lists every exported identifier with a verdict for the v0.9 release candidate. Nothing changes in this release.

v0.6.2

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 11:54

Changed

  • Routes whose validate tags are all simple rules on scalar parameters (required, omitempty, min, max, gte, lte, gt, lt, len, eq, oneof) run compiled checks instead of the validator; bodies with rules, slices, and other rules keep the validator, and both produce the same errors. The Content-Type header is set without canonicalizing or allocating, and inputs with nothing to bind skip the input pool. An empty handler now allocates nothing and costs less than plain net/http setting its own header; a route with a path value, a query value, a default, and validation takes about 650 ns and one allocation, down from 1690 ns and ten in v0.6.0. See BENCHMARKS.md.

v0.6.1

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 11:45

Changed

  • The request path allocates less: the query string is scanned for the bound fields instead of parsed into a map, route inputs are pooled, and responses are encoded by a pooled encoder. A route with a path value, a query value, a default, and validation went from 10 allocations and 504 bytes per request to 4 and 48, about a third faster; see BENCHMARKS.md. Behavior is unchanged, and malformed query pairs are skipped the way url.ParseQuery skips them.
  • The benchmarks write to a discarding ResponseWriter, so their numbers now measure the handlers rather than httptest.ResponseRecorder.

Fixed

  • Two tests that failed on Go 1.27 and in CI: one depended on the exact text of a json error, and tack new's test disabled the module proxy, which the generated project's go mod tidy needs.

v0.6.0

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 10:57

Added

  • File uploads: a form:"name" tag binds a field from a multipart/form-data or application/x-www-form-urlencoded body, and a *multipart.FileHeader or []*multipart.FileHeader field receives uploaded files. Form values use the same converters, defaults, and validate rules as query values, with locations such as form.photo. The request body is documented as a form with files as binary strings, so the TypeScript client sends FormData (Blob fields), the Zod schemas use z.instanceof(Blob), and /docs shows a file picker. Temporary files are removed when the handler returns. An input cannot have both a Body field and form fields.
  • Server-sent events: a handler that returns tack.Stream[T], an iterator func(yield func(T) bool), sends one event per value with its JSON on the data line, flushing each. tack.Event[T] adds the event name and ID. The request context is done when the client disconnects or the server shuts down, and yield returns false once the connection is gone. The response is documented as text/event-stream with the schema of T; the client returns AsyncIterable<ServerEvent<T>> and tack mock sends one example event.
  • App.Run(ctx, addr) and App.Serve(ctx, srv) serve until the context is done, then shut down gracefully: no new connections, every request context canceled (which ends streams), and up to ShutdownTimeout (10 seconds) for in-flight handlers. Run uses a server with a 10 second header timeout and a two minute idle timeout and no body timeouts, so uploads and streams are not cut off.
  • A request whose Content-Length exceeds Config.MaxBodyBytes is refused with 413 before the body is read.
  • tack new <dir> [-module path] creates a runnable project: one resource with a typed error, groups, a Location header, graceful shutdown, a README, and a test that regenerates and checks client/api.ts and client/schemas.ts.
  • tack version prints the version. Publishing a GitHub release builds the command for Linux, macOS, and Windows (amd64 and arm64) and attaches the archives and a checksum file.
  • examples/petstore gains POST /pets/{id}/photo and GET /pets/events, and uses Run.

Changed

  • The detail of the unsupported_media_type (415) problem is now "unsupported content type", since bodies can be JSON or forms.

v0.5.0

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 10:38

Added

  • tsgen.GenerateZod(doc) writes Zod schemas: one per component schema and one per operation input, with the bounds, lengths, patterns, enums, formats, and defaults the server enforces, so a form can validate before it sends. Each schema exports a value and a type of the same name; schemas in a reference cycle use z.lazy with an explicit type. The file imports zod and works with Zod 3.23 and later, Zod 4 included. tsgen.CheckZod keeps it fresh like Check does for the client.
  • tack gen zod -spec <file|url|-> [-o schemas.ts] and tack check -spec <file|url|-> -zod schemas.ts.
  • tack dev [-spec URL] [-o api.ts] [-zod schemas.ts] [-dir .] [-- command...] builds the directory with go build, runs the binary, waits for the document URL to answer, and regenerates the client and schemas, writing only when they changed. A change to a Go file, go.mod, or go.sum does it again; a failed build keeps the running app and reports the error. A command after -- replaces the build and run step.
  • tack mock -spec <file|url|-> [-addr 127.0.0.1:8081] serves every operation with its success status and a body built from the response schema (examples and defaults first, then values derived from the type, format, bounds, and enum). A Prefer: code=404 header returns that documented error with its code and detail. Responses allow cross-origin requests and the document is served at /openapi.json.
  • examples/petstore commits client/schemas.ts next to its client; CI checks both with tack check and type-checks them.

v0.4.0

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 10:16
2271ee3

Added

  • tsgen: Generate(doc) turns the OpenAPI document into one dependency-free TypeScript file built on fetch. Every operation is a method on Client that returns a Result: { ok: true, data, headers, response } with the response type and the documented headers parsed, or { ok: false, error, response } where error is the union of the problem schemas the route declares, so switch (res.error.code) is checked by the compiler. Responses the document does not describe are thrown as ApiError, and unwrap(res) throws it for error results. ClientOptions takes baseUrl, fetch, default headers, and credentials by security scheme name, which the client sends as Authorization: Bearer, Authorization: Basic, or the API key header or query parameter the scheme names. Output is deterministic and sorted, so the file changes only when the API does.
  • tsgen.Check(doc, path) returns an error wrapping tsgen.ErrStale when the file is missing or differs from the document, for a test that fails CI when the committed client is older than the routes.
  • cmd/tack: tack gen ts -spec <file|url|-> [-o api.ts] writes the client and tack check -spec <file|url|-> api.ts exits 1 when it is stale. The document can come from a file, a URL such as http://localhost:8080/openapi.json, or standard input.
  • Generation fails with the fix when the document holds something the client cannot express: two operations with one ID, an ID that is not a TypeScript identifier, a schema named like something the client defines (Client, Result, GetPetsError for operation getPets), two parameters with one name in different locations, a request body that is not JSON, or several success responses.
  • examples/petstore commits its generated client/api.ts and a test that fails when it is stale (go test . -update regenerates it). CI checks the same file with tack check from the served document and runs the client end to end with node.

v0.3.0

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 09:18

tack v0.3.0 adds route groups, middleware at three levels, typed errors that reach the docs by code, auth schemes in /docs, and responses with headers.

Added

  • Route groups: App.Group(prefix, mw...), nestable with Group.Group, plus Group.Use and Group.With(opts...) to share route options. A prefix may hold wildcards such as /orgs/{org}.
  • Middleware at three levels with plain func(http.Handler) http.Handler: App.Use, group middleware, and the Middleware route option. Order: app, outer group, inner group, route, handler. chi's RequestID, Logger, and Recoverer work unchanged.
  • ContextValue[T](ctx, key) reads typed values that middleware stored in the context.
  • Typed errors: DefineError(status, code, detail) returns an *Error with a Code; Error.WithDetail makes a per-request copy, Error.Is matches by code and status, and Error.Unwrap returns the handler's original error. The Errors(...) route option lists them in the OpenAPI document, one <Code>Problem schema per code, so clients can build a typed union per route. With Config.Dev, returning an undeclared typed error logs a warning.
  • Config.ErrorHandler replaces the error format for the whole app, and App.WriteError lets middleware answer in that format.
  • Security schemes: Config.Security with Bearer, Basic, and APIKey, bound to routes or groups with Security(name, scopes...). They appear in components.securitySchemes and each operation's security, so /docs offers a token field and sends it. Secured routes document a 401, and ErrUnauthorized is the matching error.
  • Responses with headers: an output struct with a Body field, an optional Status int, and header-tagged fields, all documented in the spec. Registration panics when a struct has header fields or an untagged Status int but no Body.
  • openapi gains SecurityScheme, SecurityRequirement, Header, Components.SecuritySchemes, Operation.Security, Response.Headers, and Schema.AllOf/OneOf.
  • examples/auth: a login that signs a JWT and a route that requires it, as its own module.

Changed

  • Every error tack sends now carries a code: malformed_request (400), body_too_large (413), unsupported_media_type (415), validation_failed (422), and internal (500). The ProblemDetails schema gains the optional code property.
  • An output struct with a field named Body is now treated as a response wrapper, so only Body is encoded. Rename the field if it was meant as a JSON property.
  • examples/petstore uses a group, DefineError, and a Location header.

Full changelog: https://github.com/raven-clown/tack/blob/main/CHANGELOG.md

v0.2.0

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 08:04

tack v0.2.0 generates an OpenAPI 3.1 document and an interactive docs page from the same structs that bind and validate requests.

Added

  • /openapi.json and /docs, configured with Config.Description, Servers, SpecPath, and DocsPath; App.OpenAPI returns the document.
  • Route options Summary, Description, Tags, OperationID, and Deprecated. Operation IDs are generated from the method and path, such as getPetsById.
  • JSON Schema 2020-12 for structs, embedded structs, slices, arrays, maps, pointers, time.Time, []byte, json.RawMessage, recursive types, and generics. doc, example, and validate tags map to schema keywords, and openapi.SchemaProvider lets a type define its own schema.
  • Every operation documents its problem+json error responses through the ProblemDetails schema.
  • Request bodies with a non-JSON Content-Type get 415 Unsupported Media Type.
  • CI generates Go and TypeScript clients from the petstore spec with openapi-generator.

Changed

  • A null request body counts as missing: 422 when the body is required, nil when Body is a pointer.
  • Errors implementing HTTPStatus() send only their own message to the client.
  • Validation responses list at most 50 errors.
  • Slice header fields also accept comma-separated values.
  • Registration panics, naming the route and the fix, for invalid or duplicate paths, slice fields whose element rules need validate:"dive", default tags inside JSON bodies, and success statuses that cannot carry the handler's output.

Fixed

  • Validation error locations for anonymous and generic input types.

Full changelog: https://github.com/raven-clown/tack/blob/main/CHANGELOG.md

v0.1.0

Choose a tag to compare

@raven-clown raven-clown released this 11 Oct 08:04

tack v0.1.0 is the first usable version: typed routes on net/http with request binding, validation, and RFC 9457 errors.

Added

  • New, Config, and App, an http.Handler built on http.ServeMux.
  • Typed routes Get, Post, Put, Patch, and Delete with inferred input and output types, and the Status route option.
  • Binding from path, query, header, and cookie tags and a JSON Body field, with default values. Supports strings, bools, all integer and float types, time.Time, encoding.TextUnmarshaler, pointers for optional values, and slices for repeated values.
  • Validation with go-playground/validator, reporting every failing field at once with locations such as body.name and query.page.
  • RFC 9457 application/problem+json errors: Error, ErrorDetail, NewError, and the HTTPStatus() int convention for domain errors. Request bodies are limited to 1 MiB by default.
  • Registration panics that name the route, the field, and the fix when an input struct does not match its path.
  • examples/petstore and BENCHMARKS.md.

Full changelog: https://github.com/raven-clown/tack/blob/main/CHANGELOG.md