Skip to content

Releases: caspel26/goninja

v0.7.0

Choose a tag to compare

@caspel26 caspel26 released this 07 Sep 14:37
e3ab377

Added

  • Partial updates now use generated patch models with field-presence tracking, so omitted fields are not overwritten.
  • Runtime, OpenAPI, Chi, Echo, and Gin integrations support the partial-update flow.
  • The generator now performs semantic validation of field types before writing output, including relation-target validation.
  • Added documentation and prototype coverage for partial updates.

v0.6.2

Choose a tag to compare

@caspel26 caspel26 released this 02 Sep 13:28
faf6c72

Fixed

  • The generator now rejects unresolved relation types, named scalar types, and external types before it writes generated files.
  • Relations must target an annotated goninja model in the same generation run, preventing invalid generated references such as StatusRetrieve.
  • Validation retains the no-partial-output guarantee: invalid models leave the output directory unchanged.

Verification

  • go test ./...

v0.6.1

Choose a tag to compare

@caspel26 caspel26 released this 02 Sep 12:41
15300bc

Fixed

  • Generated list queries now use GORM column names instead of public JSON names. SELECT, filters, and ordering honour gorm:"column:..." or GORM’s default naming strategy, while API and OpenAPI names stay unchanged.

Added

  • A Performance & Benchmarks guide with the tracked request-path baselines, raw samples, run commands, and CI regression gate. The figures are documented as reproducible workload baselines, not cross-framework throughput claims.

v0.6.0

Choose a tag to compare

@caspel26 caspel26 released this 27 Aug 11:20

Added

  • List now selects only the <Model>List columns, not SELECT *internal/codegen/ir.go, internal/codegen/templates/model.go.tmpl

    A generated List query read every column of every row via a plain
    Find(&items), then discarded whatever <Model>List doesn't carry once
    scanning it into the DB-shaped struct — on a model with a large
    retrieve-only field (a bio, a body, a blob), real per-request I/O
    spent reading data the response never contains. Model.ListSelectColumns
    derives the list-tagged column list at generation time; the generated
    file gets a <model>ListColumns whitelist and List's query becomes
    q.Select(<model>ListColumns).Limit(...).Offset(...).Find(&items).
    Applied after Count, so total still reflects COUNT(*) over every
    matching row, unaffected by the narrower Select.

    Skipped (falling back to the original SELECT *) when a list-tagged
    field is itself a relation, since a relation isn't a column and can't be
    named in a SELECTList never preloads relations regardless (see
    Phase 0's list/retrieve split), so that field is already empty in a list
    response either way; the fallback only avoids emitting a broken query.
    Verified with a real gorm query-logger capture over AuthorResource
    (examples/prototype/list_select_test.go): the emitted SELECT names
    only id, name, created_at, never bio (AuthorRetrieve-only), and the
    COUNT query is unaffected.

Changed

  • OpenAPI construction moved out of generated code into goninja.BuildResourceOpenAPIresource_openapi.go (new), internal/codegen/templates/model.go.tmpl

    Every generated file carried ~165 lines building its own OpenAPI paths,
    operations, list envelope, id parameter and per-route security — code
    where only the strings differed between models. That contradicted the
    architecture the rest of the runtime already follows: a generated
    handler calls RespondJSON/Validate rather than carrying a copy, so a
    fix lands once instead of per model.

    Generated OpenAPI() now passes the genuinely per-model half — the
    List/Retrieve/Create/Update schemas and the filter-derived query
    parameters — as a goninja.ResourceDoc, and the shared structure lives
    in the runtime. A generated file drops ~25% (the prototype's three
    models: 2362 → 1780 lines). Verified behaviour-preserving by diffing the
    full generated OpenAPI document before and after: byte-identical except
    the intended grammar fix below.

    The tradeoff, stated plainly: OpenAPI construction is no longer readable
    in your own repo. It runs once at mount rather than per request, and the
    whole request path stays generated and inspectable, so this doesn't
    touch the "read the code that serves your API" guarantee — but it is
    less code you can read locally.

  • Register and the OpenAPI document now share goninja.ActionPathresource_openapi.go

    Both computed an Action's mount path independently (<base>, plus
    /{id} when Detail, plus UrlPath). They now call one function, so a
    route's mounted path and its documented path can't drift apart.

Fixed

  • Operation summaries read "Create an author", not "Create a author"resource_openapi.go

    The generated summary hardcoded "a ". Now chosen from the model name,
    including the "u"-sounds-like-"you" cases so the most common model name
    of all reads "Create a user" rather than "Create an user". Fixing it in
    the runtime means it lands for existing generated code on the next
    regeneration, rather than being baked into every consumer's files.

  • A time.Time filter field made the generated file import "strconv" without using itinternal/codegen/ir.go

    A regression introduced by v0.5.0's own time.Time filter fix.
    Model.NeedsStrconv counted every non-string filter field as needing
    "strconv" — correct before that fix, when anything not
    bool/string/float fell through to a strconv.ParseInt branch, but a
    time.Time field now parses with time.Parse and never touches
    strconv. A model whose only non-string filter field is a
    time.Time therefore imported it and never used it: "strconv" imported and not used, a hard compile error. Found by the same
    external consumer project that reported the v0.5.0 bug this fix
    introduced.

    TestGenerate_EveryScalarFilterTypeCompiles, added in v0.5.0 to catch
    exactly this class of bug, provably could not: it puts every scalar
    type in one model, so bool/int/float fields always make "strconv"
    legitimately used and mask the unused-import case. The new
    TestGenerate_EachScalarFilterTypeCompilesAlone compiles one model per
    type in isolation, which is what actually pins import correctness —
    verified by reverting the fix and confirming the per-type test fails on
    the exact error while the all-types one still passes.
    examples/prototype/models/author.go's CreatedAt is now permanently
    filter-tagged as the end-to-end proof (Author has only string
    filters otherwise, so it reproduces the shape; Book can't — its
    float/bool filters mask it), with author_resource_test.go covering
    the runtime half over real HTTP.

v0.5.0

Choose a tag to compare

@caspel26 caspel26 released this 22 Aug 02:40

Added

  • Action.Auth, Config.StrictAuth, goninja.Actions, and variadic New<Model>Resource constructorsactions.go, config.go, resource.go, internal/codegen/templates/model.go.tmpl

    Action.Auth *RouteAuth, if set, decides an action's auth directly,
    right where the action is declared — ProtectAction/SecurityForAction
    consult it before falling back to ResourceConfig.Auth/
    Config.DefaultAuth.Routes keyed by Route(a.Name) (the only option
    before this). Found via a real external consumer project: a growing set
    of custom actions each needing to be re-listed by name in a separate
    auth policy is easy to get out of sync with — add a new action, forget
    to list it there, and it's silently public.

    Config.StrictAuth: true catches exactly that class of mistake (for
    CRUD routes too, not just actions) at startup instead of silently:
    generated Register(mux) now calls BaseResource.CheckStrictAuth with
    every route it's about to mount, before mounting any of them, and it
    panics naming every one with no explicit auth decision anywhere — not
    protected-or-public, just undecided. RouteAuth{Public: true} is
    itself an explicit decision and doesn't trigger it; only a route nobody
    mentioned at all does. false (the default) changes nothing for
    existing apps.

    Every generated New<Model>Resource(db *gorm.DB) is now
    New<Model>Resource(db *gorm.DB, opts ...<Model>Option) — variadic, so
    every existing zero-option call site keeps compiling unchanged.
    goninja.Actions[R interface{ SetActions(...Action) }, A any](build func(R, A) []Action, arg A) func(R) builds one of these
    Options from an action-builder function, so actions attach right at
    construction — api.NewBookResource(db, goninja.Actions(bookActions, actionAuth)) — instead of a separate
    SetActions call afterward. examples/prototype/main.go uses this for
    both Task and Book.

Fixed

  • A filter-tagged time.Time field failed to compileinternal/codegen/templates/model.go.tmpl, internal/codegen/ir.go

    parse<Model>Filters' exact-match branch only special-cased
    bool/string/float; a time.Time field fell through to the int64
    fallback, generating the invalid conversion time.Time(n). Found via a
    real external consumer project filtering on a timestamp field — exactly
    the kind of bug dogfooding was meant to surface. Fixed by special-casing
    time.Time to time.Parse(time.RFC3339, v); Model.UsesTime() also
    needed to recognize a filter-only time.Time field (previously only
    checked list/retrieve/create/update) so "time" gets imported
    in that case too. examples/prototype/models/book.go's CreatedAt is
    now permanently filter-tagged as the regression proof (was
    list,retrieve only); internal/codegen's
    TestGenerate_EveryScalarFilterTypeCompiles actually compiles a
    generated file covering every recognized scalar type in one pass —
    every prior codegen test only checked generated source text, which is
    why this slipped through: go/format.Source formats syntax, it doesn't
    type-check.

v0.4.0

Choose a tag to compare

@caspel26 caspel26 released this 21 Aug 15:13

Added

  • API.SetErrorMapper, Config.DefaultErrorMapper, and goninja.NewErrorMapper/NewErrorMappingapi.go, config.go, mapper.go

    API.SetErrorMapper(mappings ...ErrorMapping) registers one or more
    per-error-type handlers on the app object itself, applied by both
    Mount and MountWithConfig to every resource that hasn't called its
    own SetErrorMapper, without requiring a Config built by hand just to
    reach MountWithConfig. It takes ErrorMappings rather than a whole
    ErrorMapper so mappings from different files compose safely into one
    list: a plain ErrorMapper has no way to say "I didn't recognize this
    error, try the next one" (DefaultErrorMapper answers every error), so
    chaining whole ErrorMappers would let an earlier one silently swallow
    everything after it — ErrorMapping's own Matches avoids that.
    Config.DefaultErrorMapper still exists for setting a whole
    ErrorMapper explicitly on the Config passed to MountWithConfig
    (wins over API.SetErrorMapper when both are set — a resource's own
    BaseResource.SetErrorMapper still takes a whole ErrorMapper too, for
    full control at that scope). NewErrorMapper/NewErrorMapping[T] build
    an ErrorMapping/compose them into a plain ErrorMapper, matching via
    errors.As like DefaultErrorMapper itself, instead of a hand-written
    MapError switch. Resolution order per resource: its own
    SetErrorMapper wins if set, else Config.DefaultErrorMapper
    (explicit, or API.SetErrorMapper's value), else the package
    DefaultErrorMapper.

v0.3.1

Choose a tag to compare

@caspel26 caspel26 released this 20 Aug 15:23

Changed

  • docsui.SpecSource renamed to docsui.SpecProviderdocsui/docs.go

    A SonarQube naming-convention fix (single-method interfaces should end
    in -er), matching the existing openapi.OpenAPIProvider precedent. A
    breaking rename for any caller referencing the type by name directly —
    pre-alpha, no compatibility shim.

  • adapters/{gin,echo,chi} test suites excluded from the duplication metricsonar-project.properties

    adapter_test.go/docs_test.go are intentionally near-identical across
    the three adapter modules (the same httptest suite written once and
    adapted per router, per each being a deliberately separate Go module
    with no shared test dependency) — this was tripping the
    new_duplicated_lines_density quality gate despite the actual adapter
    implementation code having 0% duplication. No code changes.

v0.3.0

Choose a tag to compare

@caspel26 caspel26 released this 20 Aug 14:54
f235553

Added

  • Router adapters for gin, echo, and chirouter (new), adapters/gin, adapters/echo, adapters/chi (new modules)

    goninja.Resource.Register now mounts on goninja.Router, a one-method
    interface *http.ServeMux already satisfies — plain net/http usage is
    unchanged. Each adapter translates a generated route's stdlib-style
    pattern ("GET /books/{id}") into its router's own syntax and binds the
    matched path value back onto the request via SetPathValue, so a
    generated handler's req.PathValue("id") call needs no changes at all —
    only route registration is router-specific. Each adapter is its own Go
    module (own go.mod), so gin/echo/chi are never a dependency of a plain
    net/http project.

  • Benchmark suiteexamples/prototype/benchmark_test.go

    Three benchmarks (go test -bench=. via make bench) covering base
    list serialization, filter-clause building, and the automatic-Preload
    cost Retrieve pays on a relation field — the baseline for future
    optimization work.

  • Benchmark regression check in CIscripts/bench-regression.sh, scripts/testdata/bench-baseline.txt, .github/workflows/bench.yml

    make bench-check runs the benchmark suite (-count=10) and compares
    it against the committed baseline with benchstat, failing a PR if any
    benchmark's sec/op/B/op/allocs/op regresses by more than 25% and
    the difference is statistically significant (benchstat's own ~
    already filters out noise below that). The comparison table is also
    written to the GitHub Actions job summary, so a reviewer sees the actual
    numbers, not just pass/fail, and a self-contained HTML report
    (reports/bench-report.html, gitignored) is uploaded as a CI artifact
    for a more readable view than raw logs. make bench-baseline moves the
    baseline deliberately, after confirming a numbers change is expected.

  • make bench-profileMakefile

    Runs the benchmark suite with -cpuprofile/-memprofile and prints a
    top-10 go tool pprof summary for each, for finding what's actually
    worth optimizing rather than just whether something regressed.

Changed

  • Resource.Register takes goninja.Router instead of *http.ServeMuxapi.go, docsui/docs.go, internal/codegen/templates/model.go.tmpl

    A breaking interface change for any hand-written Resource (not
    generated ones, which pick this up automatically on regeneration) —
    pre-alpha, no compatibility shim, same approach as the 0.2.0 auth
    redesign.

v0.2.0

Choose a tag to compare

@caspel26 caspel26 released this 20 Aug 02:54

Warning

Pre-alpha. Everything below is implemented and tested, but the API may
change without notice and there is no compatibility guarantee yet.

Install

go install github.com/caspel26/goninja/cmd/goninja@v0.2.0
go get github.com/caspel26/goninja@v0.2.0

Requires Go 1.25+.

What's new since v0.1.0

Explicit generator validation. goninja generate now rejects a model it
cannot turn into working code, instead of silently emitting a file that
fails to compile. Every problem across every model is reported in one run,
naming the offending file and field, and nothing is written when validation
fails. Rejected: a missing goninja-tagged ID field, an ID typed
anything but int64/string, a pointer relation field, byid on a
non-relation field, and filter on a relation field.

?order= is validated, not silently ignored. An unrecognized ordering
field used to fall through and return unordered results with a 200 —
indistinguishable from a correctly sorted response. It now returns a 400
before the query ever runs. The whitelist that makes ordering
injection-safe is unchanged.

goninja.CodedError and an optional Code field. NotFound,
ValidationError, BadRequest, and the new Unauthorized error type all
implement CodedError (error plus ErrorCode() string). Left unset, each
keeps its existing default JSON "code"; setting Code lets a specific
failure carry a more precise machine-readable identifier than the HTTP
status alone provides — the new order-validation error, for example, sets
Code: "INVALID_ORDER_FIELD".

goninja.Unauthorized, and a consistent 401 body. Every configured
Authenticator declining a request used to produce a plain-text 401 via
http.Error. It now goes through the same Respond path as every other
framework error, returning
{"code":"UNAUTHORIZED","error":"unauthorized"} like the rest.

Versioned documentation. goninja.dev now serves
the current working tree at the root and a frozen snapshot of each released
minor series at /vX.Y/, switchable from a navbar selector that also shows
the live GitHub star count. A Changelog
page was added to the site, searchable by version, alongside the root
CHANGELOG.md.

CI hardening. GitHub Actions workflow permissions are now scoped
per-job rather than granted workflow-wide.

Documentation

goninja.devGetting
Started
·
Guides · Tag
Reference
·
Examples ·
Changelog

Known limitations

  • GORM and net/http are assumed; there's no adapter layer for other ORMs or
    routers.
  • A model's primary key must be a field literally named ID (int64 or a
    string UUID).
  • Relation fields must be a struct value or a slice of one — pointer relations
    aren't supported.
  • No OpenAPI example values are generated.

Full diff: v0.1.0...v0.2.0

v0.1.0 — first pre-alpha release

Choose a tag to compare

@caspel26 caspel26 released this 20 Aug 01:40

Annotate a Go struct, run one command, and get a complete CRUD REST API — as
ordinary Go source you commit and can read.

Warning

Pre-alpha. Everything below is implemented and tested, but the API may
change without notice and there is no compatibility guarantee yet.

Install

go install github.com/caspel26/goninja/cmd/goninja@v0.1.0
go get github.com/caspel26/goninja@v0.1.0

Requires Go 1.25+.

What's in it

Generation. goninja generate writes one <model>_generated.go per
model — separate List/Retrieve/Create/Update output types, handlers,
queries and an OpenAPI fragment, all go/format-ed. -watch regenerates on
save.

Querying. filter-tagged fields become exact-match filters, with
_min/_max range filters on numeric fields. Limit/offset pagination behind
a {items, total, limit, offset} envelope, and ?order=-field ordering
resolved against a per-model whitelist.

Relations without N+1. list never preloads; retrieve preloads every
relation it carries. Belongs-to and has-many are both supported, and the
byid modifier exposes a related ID instead of nesting the full object.

Validation and errors. validate tags apply to input only, returning
per-field 422s. NotFound/ValidationError/BadRequest map to 404/422/400
through a pluggable ErrorMapper; anything else is a generic 500 that never
leaks the underlying error.

Auth. Authenticator objects, tried in order, with per-route policy —
and the security schemes they describe end up in the generated OpenAPI
document, so what's enforced and what's documented can't drift apart.
HTTPBearer, HTTPBasic, APIKeyHeader and CookieKey ship built in.

Extension points. Before/after hooks, method overrides, custom mount
paths, restricted route sets, and Action for non-CRUD endpoints — none of
which require touching a generated file. Writes run inside a transaction.

Docs UI. One MountDocs call serves the merged OpenAPI document plus a
rendered UI. Swagger UI and ReDoc are both embedded — no external CDN — and
DocsUI is an interface if you want something else.

Testing. goninjatest.NewDB and NewServer drive a real generated
resource over HTTP against in-memory SQLite, no Postgres required.

Documentation

goninja.devGetting
Started
·
Guides · Tag
Reference
·
Examples

Known limitations

  • GORM and net/http are assumed; there's no adapter layer for other ORMs or
    routers.
  • A model's primary key must be a field literally named ID (int64 or a
    string UUID).
  • Relation fields must be a struct value or a slice of one — pointer relations
    aren't supported.
  • An unknown ?order= field is ignored rather than rejected.
  • No OpenAPI example values are generated.