goenax

package module
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 17 Imported by: 0

README

Goenax

Code-as-source-of-truth API registry for Go.

Declare each HTTP endpoint once — in the same place you mount it on your router — and Goenax builds an OpenAPI 3.1 document from your Go types. From that one source you generate the Postman collection and the TypeScript types with the standard tools, and Goenax gives you the checks that keep them honest.

Framework-neutral: adapters for Echo, net/http and Gin ship in the box; the core imports none of them.

go get github.com/Lev2098/Goenax

Why

Your API contract already exists in the code. Writing it down a second time — a hand-kept OpenAPI file, a Postman collection, doc comments — creates a copy that drifts the first week a handler changes and the copy doesn't. Annotation tools move the drift into comments the compiler never checks. Goenax takes the contract from the Go types you already have, so the schema can't describe a shape the compiler doesn't know.


Quickstart

import (
    "net/http"

    "github.com/labstack/echo/v4"
    "github.com/Lev2098/Goenax"
    echoadapter "github.com/Lev2098/Goenax/adapter/echo"
)

type LoginRequest struct {
    Email    string `json:"email"    validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}
type SessionResponse struct {
    Token string `json:"token"`
}

e := echo.New()
reg := goenax.New()
api := echoadapter.New(e.Group("/api"), reg, "/api")

api.POST("/login", loginHandler, []goenax.Option{
    goenax.Summary("Exchange credentials for a session"),
    goenax.Owner("#team"),
    goenax.Tags("Auth"),
    goenax.Request[LoginRequest](),
    goenax.Response[SessionResponse](http.StatusOK),
    goenax.Error(http.StatusUnauthorized, "invalid", "wrong email or password"),
}, authMiddleware)

// one call did two things: mounted the live route on Echo,
// and recorded its contract in reg.

spec, _ := reg.OpenAPI(goenax.Info{
    Title: "My API", Version: "1.0.0",
    Servers: []goenax.Server{{URL: "http://localhost:8080"}},
})

To emit the spec in CI without booting the server, keep the declarations in a table that both the live mount and a handler-free Spec() consume (the option sets don't need handlers) — then a tiny cmd prints Spec().OpenAPI(...).


The declaration API

Option Documents
Summary(s) · Description(s) human text
Owner(s) owning team/channel — required by Validate
Tags(t...) grouping (OpenAPI tags, Postman folders)
Request[T]() request body type
Response[T](status) success body type + status
NoContent(status) success with no body
HTML() success renders text/html
Also(status, desc) · AlsoJSON[T](status, desc) an extra status, without / with a body
RespHeader(name, desc) a header on the success response
Error(status, code, msg) a documented failure
Params[T]() path / query / header parameters, typed, from the struct the handler binds
Query / Header / PathParam a single parameter, as a plain string
Secured() requires a bearer session
DependsOn(ids...) endpoints a QA run must call first
Compose(opts...) bundle options (e.g. a middleware's own docs)

Request[T] / Response[T] are generic: reflect.TypeFor[T]() binds the schema to the exact struct at compile time — no annotations.

Parameters from the bound struct

Params[T] does for parameters what Request[T] does for the body: document the struct the handler already binds, so a renamed parameter can't leave the spec behind.

type ListUsersParams struct {
    Org   string `param:"org"   doc:"organisation slug"`
    Limit int    `query:"limit" validate:"min=1,max=100" doc:"page size"`
    Role  string `query:"role"  validate:"omitempty,oneof=admin member"`
}

api.GET("/orgs/:org/users", func(c echo.Context) error {
    var p ListUsersParams
    if err := c.Bind(&p); err != nil { … }   // the same struct…
    …
}, []goenax.Option{
    goenax.Params[ListUsersParams](),         // …documents the parameters
    …
})
Tag Location Router
param:"…" / uri:"…" / path:"…" path (always required) Echo / Gin / net/http
query:"…" / form:"…" query Echo, net/http / Gin
header:"…" header all

Types and validate rules become the schema (limit above is an integer, 1–100; role an enum). Query and header parameters are optional unless validate:"required". doc:"…" is the description; untagged fields are ignored and embedded structs are flattened, so shared pagination can be one embedded struct.


Schema derivation

Goenax reflects over the request/response types and reads the json and go-playground validate tags:

Go OpenAPI schema
primitives string / integer / boolean / number
*T, or json:",omitempty" optional
validate:"required" required
validate:"email" / "uuid" / "url" format
validate:"min/max/len" minLength/maxLength or minimum/maximum
validate:"oneof=a b c" enum (typed: oneof=1 2 on an int is [1, 2])
validate:"dive,…" rules after dive apply to array items / map values
example:"…" example, typed to the field (example:"42" on an int is 42); else a deterministic default
time.Time / []T / map[string]T date-time / array / object with additionalProperties
named struct a $ref into components/schemas (defined once, shared)

It is cycle-safe and gives every leaf a deterministic example, so a generated Postman collection is stable across runs.

A field without omitempty that isn't a pointer is required — in requests as well as responses. Make an optional request field a pointer or omitempty.

Every Error documents the same JSON body — { "message": "…" } by default. If your API returns something else, give its type once:

reg.OpenAPI(goenax.Info{Title: "My API", Version: "1.0.0",
    ErrorType: reflect.TypeFor[APIError]()})

Examples

  • Runnable demo — go run ./examples/echo, then open http://localhost:8080/api/docs (also /api/scalar, /api/redoc). It shows Request/Response, Params[T], Info.ErrorType, Validate and Coverage on boot, and the live docs.
  • API examples — example_test.go, also rendered on pkg.go.dev. They run as tests, so they can't go stale.

Adapters

Same shape for every framework — New, Group, Handle/GET/POST/PUT/ PATCH/DELETE, Docs. Each mounts the route and records the contract. Only the adapter imports the framework.

echoadapter.New(e.Group("/api"), reg, "/api")   // pass the already-prefixed Echo group
nethttpadapter.New(mux, reg, "/api")             // *http.ServeMux (Go 1.22+ patterns)
ginadapter.New(engine, reg, "/api")              // pass the unprefixed router; New creates the group

Write paths in the framework's own syntax — /users/:id on Echo/Gin, /users/{id} on net/http. The registry stores them in OpenAPI form (/users/{id}; see goenax.NormalizePath), so PathParam("id", …), the spec and Coverage all line up.

Middleware uses each framework's native type. echoadapter.Routes(e) / ginadapter.Routes(engine) list the registered routes for Coverage (below).


Live docs (/docs)

Like FastAPI, the running service can serve its own contract — the spec comes from the same registry the routes were mounted through, so it always describes exactly what this build serves.

api.Docs("/docs", goenax.Docs(reg, goenax.Info{
    Title:   "My API",
    Version: version, // e.g. set via -ldflags, so /docs shows which build answers
}), requireStaff)
  • GET /api/docs — interactive UI; GET /api/docs/openapi.json — the spec.
  • UI: goenax.WithUI(goenax.SwaggerUI) (default), goenax.Scalar or goenax.Redoc. Assets load from the jsDelivr CDN.
  • The spec is built lazily, cached until the registry changes, and served with an ETag.
  • Leave Info.Servers empty: "Try it out" then calls whichever host served the page, so one binary works on every environment.
  • The docs routes are not part of the spec and are ignored by Coverage. Use reg.Ignore(method, path) for other intentionally undocumented routes (health checks, metrics).
  • The spec reveals every endpoint. In production, guard it with middleware or don't mount it (if cfg.DocsEnabled { … }).

Without an adapter, goenax.Docs is a plain http.Handler: mount it on path and path + "/openapi.json".

For one portal across many services, let the portal pull each service's /docs/openapi.json (Swagger UI urls, Scalar, Backstage) — the service stays the source of truth.


Keeping it honest

Goenax gives you the checks; wire them into your tests and CI.

  • reg.Validate() — returns problems per endpoint: missing summary/description/owner/tag, a mutating method with no error response, a duplicate method+path, a path {token} that doesn't match a declared PathParam, a status code claimed by two responses (e.g. Also(200) next to a 200 success), or two routes producing the same operationId (/a-b and /a_b). Several Errors on one status are fine — they merge. Fail CI on a non-empty result and documentation becomes a build requirement, not a reviewer's ask.
  • reg.ValidateResponse(method, path, body) — validates a real response against its declared Response[T] schema. Call it from a handler test and a handler that returns the wrong shape fails — closing the gap between what an endpoint promises and what it returns.
  • reg.Coverage(actual) — diffs the routes registered on the router against the declared endpoints, flagging routes that are undocumented or documented but never mounted.
  • Regenerate & diff. Commit the generated spec (and Postman / TS) and, in CI, regenerate them and diff against the committed files — a change that isn't reflected fails the build.
  • Breaking changes. Diff the spec against your baseline with oasdiff and fail on a breaking change.

Postman and TypeScript come from the emitted OpenAPI via openapi-to-postman and openapi-typescript — Goenax doesn't reinvent them.


Changelog

Unreleased

New

  • Params[T]() — typed path / query / header parameters from the struct the handler binds (Echo, Gin and net/http binding tags), with doc:"…" descriptions. Validate flags a parameter declared twice.
  • Runnable demo in examples/echo; godoc examples in example_test.go.

Fixed

  • An unexported embedded struct was dropped from body schemas, though encoding/json promotes its fields.
  • Generated examples ignored the schema's bounds (0 for min=1, "example" for max=3), so Swagger "Try it out" sent invalid requests by default.
v0.2.0 — 2026-10-07

New

  • Live docs — goenax.Docs(reg, info) serves the spec at …/openapi.json plus a Swagger UI / Scalar / Redoc page; mount it with the adapters' Docs method. See Live docs.
  • reg.Ignore(method, path) keeps health checks, metrics and the docs routes out of Coverage.
  • goenax.NormalizePath — router syntax (:id, *file, {p...}) to OpenAPI ({id}).
  • Info.ErrorType — document your own error body instead of { "message" }.
  • validate:"dive,…" rules are applied to array items / map values.
  • PUT and PATCH helpers on every adapter.
  • Registry is safe for concurrent use.

Fixed

  • Gin: a nested Group mounted routes with the prefix applied twice (/v1/api/v1/…) while the spec said /api/v1/….
  • Echo/Gin: :id path params were recorded verbatim — an invalid OpenAPI path, a false Validate error, and a Coverage mismatch. They are now {id}.
  • oneof on a number field produced a string enum, so ValidateResponse rejected every valid value.
  • Several Errors with one status overwrote each other; their descriptions now merge, and Validate flags a status claimed by two different responses.
  • ValidateResponse accepted 1.5 for an integer field.
  • Lookup was case-sensitive on the method.
  • operationId contained { }; Validate now also flags operationId collisions.
  • example tags on number/bool fields were emitted as strings.

Breaking

  • Schema.Enum is []any (was []string) so enums carry the field's type.
  • Registry.Add stores the method upper-cased and the path in OpenAPI form.
  • ginadapter.New creates the prefixed group itself — pass the unprefixed router, New(engine, reg, "/api") (as the README always showed).

License

MIT — see LICENSE.

Documentation

Overview

Package goenax is a code-as-source-of-truth registry for HTTP endpoints.

An endpoint is declared once — typed request/response plus metadata — and the registry projects that declaration into OpenAPI (and, downstream, Postman and TypeScript). Because the request/response schemas are taken from real Go types, the contract cannot lie about the shapes the handler binds and returns.

The core package uses only the standard library; framework integration lives in the adapter packages (adapter/echo, adapter/gin, adapter/nethttp).

Example

Declare an endpoint once; the registry turns it into an OpenAPI document. In an application the adapters (adapter/echo, adapter/gin, adapter/nethttp) call Define and Add for you while they mount the route.

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"slices"

	goenax "github.com/Lev2098/Goenax"
)

type LoginRequest struct {
	Email    string `json:"email"    validate:"required,email"`
	Password string `json:"password" validate:"required,min=8"`
}

type Session struct {
	Token string `json:"token"`
	Role  string `json:"role" validate:"oneof=admin member"`
}

func main() {
	reg := goenax.New()
	reg.Add(goenax.Define(http.MethodPost, "/api/login",
		goenax.Summary("Exchange credentials for a session"),
		goenax.Description("Returns a bearer token."),
		goenax.Owner("#auth"),
		goenax.Tags("Auth"),
		goenax.Request[LoginRequest](),
		goenax.Response[Session](http.StatusOK),
		goenax.Error(http.StatusUnauthorized, "invalid", "wrong email or password"),
	))

	spec, err := reg.OpenAPI(goenax.Info{Title: "My API", Version: "1.0.0"})
	if err != nil {
		panic(err)
	}

	var doc struct {
		OpenAPI    string                    `json:"openapi"`
		Paths      map[string]map[string]any `json:"paths"`
		Components struct {
			Schemas map[string]any `json:"schemas"`
		} `json:"components"`
	}
	if err := json.Unmarshal(spec, &doc); err != nil {
		panic(err)
	}

	schemas := slices.Sorted(func(yield func(string) bool) {
		for name := range doc.Components.Schemas {
			if !yield(name) {
				return
			}
		}
	})

	fmt.Println("openapi:", doc.OpenAPI)
	fmt.Println("operations:", len(doc.Paths["/api/login"]))
	fmt.Println("schemas:", schemas)
}
Output:
openapi: 3.1.0
operations: 1
schemas: [LoginRequest Session]

Index

Examples

Constants

View Source
const SpecFile = "openapi.json"

SpecFile is the name the spec is served under, relative to the docs page: a Docs handler mounted at /docs serves the spec at /docs/openapi.json.

Variables

This section is empty.

Functions

func Docs added in v0.2.0

func Docs(reg *Registry, info Info, opts ...DocsOption) http.Handler

Docs serves the registry as live documentation — the FastAPI /docs idea: the running service is the source of truth for its own contract.

The handler answers two requests:

GET …/openapi.json  the OpenAPI document, built from the registry
GET …               an HTML page rendering that document in the chosen UI

Mount it with an adapter's Docs method (which also keeps the docs routes out of the spec and of Coverage), or by hand on any router.

The spec is built lazily and cached until the registry changes, and is served with an ETag so clients revalidate cheaply. Leave info.Servers empty to make "Try it out" call whichever host served the page — the same binary then works on every environment without configuration.

The UI assets load from the jsDelivr CDN (as FastAPI does). The page needs no server-side knowledge of its own URL: it resolves openapi.json relative to the address the browser is on, so a path-rewriting reverse proxy keeps working.

Exposing the spec reveals every endpoint; guard it in production (auth middleware, internal network only, or simply don't mount it).

Example

Docs serves the live spec and an interactive UI. With an adapter, mount it with Router.Docs instead — that also keeps the docs routes out of the spec.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	goenax "github.com/Lev2098/Goenax"
)

func main() {
	reg := goenax.New()
	reg.Add(goenax.Define(http.MethodGet, "/api/ping", goenax.Summary("Health check")))

	docs := goenax.Docs(reg, goenax.Info{Title: "My API", Version: "1.0.0"}, goenax.WithUI(goenax.Scalar))

	mux := http.NewServeMux()
	mux.Handle("GET /docs", docs)
	mux.Handle("GET /docs/openapi.json", docs)

	for _, path := range []string{"/docs", "/docs/openapi.json"} {
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
		fmt.Println(path, rec.Code, rec.Header().Get("Content-Type"))
	}
}
Output:
/docs 200 text/html; charset=utf-8
/docs/openapi.json 200 application/json

func NormalizePath added in v0.2.0

func NormalizePath(path string) string

NormalizePath rewrites a router-native path template into the OpenAPI form the registry stores, so a route declared in its framework's own syntax documents (and validates) correctly:

/users/:id           -> /users/{id}       (Echo, Gin)
/files/*filepath     -> /files/{filepath} (Gin catch-all)
/files/{path...}     -> /files/{path}     (net/http wildcard)
/posts/{$}           -> /posts/           (net/http exact-match marker)

A bare `*` (Echo's unnamed wildcard) has no OpenAPI equivalent and is kept as-is. Paths already in OpenAPI form pass through unchanged.

Example
package main

import (
	"fmt"

	goenax "github.com/Lev2098/Goenax"
)

func main() {
	fmt.Println(goenax.NormalizePath("/users/:id/files/*path"))
	fmt.Println(goenax.NormalizePath("/static/{rest...}"))
}
Output:
/users/{id}/files/{path}
/static/{rest}

Types

type DocsOption added in v0.2.0

type DocsOption func(*docsConfig)

DocsOption customises a Docs handler.

func WithUI added in v0.2.0

func WithUI(ui UI) DocsOption

WithUI picks the viewer the docs page renders (default SwaggerUI).

type Endpoint

type Endpoint struct {
	Method string
	Path   string

	Summary string
	Desc    string
	Owner   string
	Tags    []string

	Request  reflect.Type // nil = no request body
	Response reflect.Type // nil = empty response (e.g. 204)
	Status   int          // success status code (200/201/204)

	Secured     bool // true = requires a bearer session
	SuccessHTML bool // success renders text/html rather than JSON
	Params      []Param
	RespHeaders []ResponseHeader // headers on the success response
	Also        []ExtraResponse
	Errors      []ErrorSpec
	DependsOn   []string // IDs of endpoints that must run first (QA scenarios)
}

Endpoint is the full contract of a single route: how it is called plus the metadata Validate requires. Request and Response hold the Go types of the bodies (nil when there is none); their JSON schemas are derived from those types when the spec is built.

func Define

func Define(method, path string, opts ...Option) Endpoint

Define assembles an Endpoint from a method, path, and options. Status defaults to 200 unless an option overrides it. Define does no validation — that is Validate's job; here we only build the declaration.

type ErrorSpec

type ErrorSpec struct {
	Status  int
	Code    string // short machine-readable slug, e.g. "validation"
	Message string // human-readable example message
}

ErrorSpec is one documented non-2xx outcome of an endpoint.

type ExtraResponse

type ExtraResponse struct {
	Status int
	Desc   string
	Type   reflect.Type
}

ExtraResponse is an additional documented response (e.g. a 202 alternative to the primary success). Type is optional — nil means the response has no body.

type Info

type Info struct {
	Title   string
	Version string
	Servers []Server

	// ErrorType is the Go type of the JSON body every documented Error returns,
	// e.g. reflect.TypeFor[apiError](). Nil means the default { "message": … }.
	ErrorType reflect.Type
}

Info is the OpenAPI `info` block plus document-wide settings.

type Option

type Option func(*Endpoint)

Option mutates an Endpoint during Define.

This is the functional-options pattern: each helper below returns a small function that sets one field, and Define applies them in order. It keeps declarations readable and lets us add new metadata later without changing every call site.

func Also

func Also(status int, desc string) Option

Also documents an additional response status alongside the primary success, without a body.

func AlsoJSON

func AlsoJSON[T any](status int, desc string) Option

AlsoJSON documents an additional response status that carries a JSON body of type T (e.g. a 202 with a { "status": … } payload).

func Compose

func Compose(opts ...Option) Option

Compose bundles several options into one. It lets a middleware ship its own documentation — e.g. a rate limiter that contributes a 429 response, or a captcha that contributes a header and its rejection — so routes opt into the behaviour and its docs together.

func DependsOn

func DependsOn(ids ...string) Option

DependsOn records endpoint IDs that must be called before this one.

func Description

func Description(s string) Option

Description sets the longer description.

func Error

func Error(status int, code, message string) Option

Error documents one non-2xx outcome.

func HTML

func HTML() Option

HTML marks the success response as text/html (e.g. email-link landing pages).

func Header(name string, required bool, desc string) Option

Header documents a request header (e.g. X-Recaptcha-Token).

func NoContent

func NoContent(status int) Option

NoContent declares a success with an empty body, e.g. goenax.NoContent(204).

func Owner

func Owner(s string) Option

Owner sets the owning team/channel (e.g. "#team").

func Params added in v0.3.0

func Params[T any]() Option

Params declares an endpoint's path, query and header parameters from the struct the handler binds them into, written as goenax.Params[listQuery]().

type listQuery struct {
    Limit int    `query:"limit" validate:"max=100" doc:"page size"`
    Role  string `query:"role"  validate:"oneof=admin member"`
}

Each field's type and validate rules become the parameter schema (limit is an integer with maximum 100, role an enum), and its `doc` tag the description. Path parameters are always required; query and header parameters only with validate:"required". Fields without a binding tag are ignored, embedded structs are flattened.

Because the names come from the binding tags, renaming a parameter in the struct renames it in the spec — the drift that hand-written Query(...) calls allow is gone.

Example

Params documents path, query and header parameters from the struct the handler binds — here with Echo's tags (Gin's uri/form work too).

package main

import (
	"fmt"
	"net/http"

	goenax "github.com/Lev2098/Goenax"
)

type ListUsersParams struct {
	Org   string `param:"org"   doc:"organisation slug"`
	Limit int    `query:"limit" validate:"min=1,max=100" doc:"page size"`
	Role  string `query:"role"  validate:"omitempty,oneof=admin member"`
}

func main() {
	ep := goenax.Define(http.MethodGet, "/orgs/{org}/users", goenax.Params[ListUsersParams]())

	for _, p := range ep.Params {
		fmt.Printf("%-5s %-5s required=%-5v type=%s\n", p.In, p.Name, p.Required, p.Schema.Type)
	}
}
Output:
path  org   required=true  type=string
query limit required=false type=integer
query role  required=false type=string

func PathParam

func PathParam(name, desc string) Option

PathParam documents a path parameter (always required, must appear as {name} in the route path).

func Query

func Query(name string, required bool, desc string) Option

Query documents a query parameter as a plain string. To document typed parameters from the struct the handler binds, use Params[T].

func Request

func Request[T any]() Option

Request declares the request body type, written as goenax.Request[fooRequest]().

It is generic so the caller names the type instead of passing a value. reflect.TypeFor[T]() (Go 1.22+) captures that type at compile time, so the schema is bound to the exact struct the handler binds — no annotations, and the compiler guarantees the type exists.

func RespHeader

func RespHeader(name, desc string) Option

RespHeader documents a header returned on the success response.

func Response

func Response[T any](status int) Option

Response declares the success body type and status, e.g. goenax.Response[sessionResponse](200).

func Secured

func Secured() Option

Secured marks the endpoint as requiring a bearer session. Unmarked endpoints are public (they emit an empty security requirement).

func Summary

func Summary(s string) Option

Summary sets the one-line summary.

func Tags

func Tags(tags ...string) Option

Tags appends grouping tags (folders in Postman, tags in OpenAPI).

type Param

type Param struct {
	Name     string
	In       string // "path", "query" or "header"
	Required bool
	Desc     string
	Schema   *Schema // nil = a plain string (Query/Header/PathParam); set by Params[T]
}

Param is a path, query or header parameter.

type Registry

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

Registry holds the declared endpoints. It is safe for concurrent use, so the spec can be served live (see Docs) while routes are still being registered.

func New

func New() *Registry

func (*Registry) Add

func (r *Registry) Add(e Endpoint)

Add records an endpoint. Its method is upper-cased and its path normalised to OpenAPI form (see NormalizePath).

func (*Registry) Coverage

func (r *Registry) Coverage(actual []Route) (undocumented, unmounted []Route)

Coverage compares the routes actually registered on a router with the endpoints declared in the registry. A shared route table makes the two match by construction; Coverage catches the case a table can't — a route mounted (or removed) directly, out of band. It returns:

  • undocumented: on the router but not in the registry (missing from the spec)
  • unmounted: in the registry but never registered (a dead spec entry)

Pass the router's routes via an adapter helper, e.g. echoadapter.Routes(e) or ginadapter.Routes(engine). Routes marked with Ignore are never reported as undocumented. (net/http's ServeMux exposes no route list, so it has no helper — feed it a Route slice you build yourself.)

Example

Coverage diffs the router's real routes against the declared ones. Router syntax (":id") and OpenAPI syntax ("{id}") compare equal.

package main

import (
	"fmt"
	"net/http"

	goenax "github.com/Lev2098/Goenax"
)

func main() {
	reg := goenax.New()
	reg.Add(goenax.Define(http.MethodGet, "/api/users/{id}"))
	reg.Add(goenax.Define(http.MethodGet, "/api/orders"))
	reg.Ignore(http.MethodGet, "/healthz")

	undocumented, unmounted := reg.Coverage([]goenax.Route{
		{Method: http.MethodGet, Path: "/api/users/:id"},
		{Method: http.MethodPost, Path: "/api/users"},
		{Method: http.MethodGet, Path: "/healthz"},
	})

	fmt.Println("undocumented:", undocumented)
	fmt.Println("unmounted:", unmounted)
}
Output:
undocumented: [{POST /api/users}]
unmounted: [{GET /api/orders}]

func (*Registry) Endpoints

func (r *Registry) Endpoints() []Endpoint

func (*Registry) Ignore added in v0.2.0

func (r *Registry) Ignore(method, path string)

Ignore marks a route as intentionally undocumented — health checks, metrics, the docs routes themselves — so Coverage does not report it.

func (*Registry) Lookup

func (r *Registry) Lookup(method, path string) (Endpoint, bool)

Lookup finds an endpoint by method and path template. The method is matched case-insensitively and the path may be in router-native form (":id").

func (*Registry) OpenAPI

func (r *Registry) OpenAPI(info Info) ([]byte, error)

OpenAPI projects the registry into an OpenAPI 3.1 document and returns it as indented JSON. Map keys (paths, methods, status codes) serialise in a stable order, so the output is deterministic and safe to diff / drift-check.

func (*Registry) Validate

func (r *Registry) Validate() []ValidationError

Validate checks every endpoint against the policy and structural rules, and returns one ValidationError per offending endpoint (nil = all good).

Policy (kept small, not bureaucratic): every endpoint needs a summary, description, owner and tag; mutating methods need at least one error response. Structural: no two endpoints share a method+path, path {tokens} match the declared PathParam names, and no status code is claimed by two different responses (success / Also / Error) — OpenAPI keys responses by status, so one would silently overwrite the other. Several Errors on one status are fine: they are merged into one documented response. Request bodies and the success status are not checked (action POSTs carry no body; the status is auto-defaulted).

Example

Validate is the policy gate: run it in a test and fail CI on any problem.

package main

import (
	"fmt"
	"net/http"

	goenax "github.com/Lev2098/Goenax"
)

func main() {
	reg := goenax.New()
	reg.Add(goenax.Define(http.MethodDelete, "/api/users/{id}",
		goenax.Summary("Delete a user"),
	))

	for _, problem := range reg.Validate() {
		fmt.Println(problem)
	}
}
Output:
DELETE /api/users/{id}: description; owner; tags; error responses; undeclared path parameter {id}

func (*Registry) ValidateResponse

func (r *Registry) ValidateResponse(method, path string, body []byte) error

ValidateResponse checks that a JSON response body conforms to the schema of an endpoint's declared Response type. It closes the gap between what an endpoint *promises* (Response[T]) and what the handler actually returns: call it from a handler test on the real response, and a shape that has drifted from the contract fails the test.

rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if err := reg.ValidateResponse("POST", "/api/login", rec.Body.Bytes()); err != nil {
    t.Fatalf("response breaks its contract: %v", err)
}

It checks the declared shape (types, required properties, enums) and allows unknown extra properties, so a forward-compatible addition doesn't fail a test.

Example

ValidateResponse checks a real handler response against the declared type — call it from a handler test to catch a response that drifted from its contract.

package main

import (
	"fmt"
	"net/http"

	goenax "github.com/Lev2098/Goenax"
)

type Session struct {
	Token string `json:"token"`
	Role  string `json:"role" validate:"oneof=admin member"`
}

func main() {
	reg := goenax.New()
	reg.Add(goenax.Define(http.MethodPost, "/api/login", goenax.Response[Session](http.StatusOK)))

	fmt.Println(reg.ValidateResponse(http.MethodPost, "/api/login", []byte(`{"token":"t","role":"admin"}`)))
	fmt.Println(reg.ValidateResponse(http.MethodPost, "/api/login", []byte(`{"token":"t","role":"root"}`)))
}
Output:
<nil>
$.role: root is not one of [admin member]: value not in enum

type ResponseHeader

type ResponseHeader struct {
	Name string
	Desc string
}

ResponseHeader documents a header returned on the success response.

type Route

type Route struct {
	Method string
	Path   string
}

Route is a method + path pair — the unit of comparison for Coverage.

type Schema

type Schema struct {
	Ref                  string             `json:"$ref,omitempty"`
	Type                 string             `json:"type,omitempty"`
	Format               string             `json:"format,omitempty"`
	Enum                 []any              `json:"enum,omitempty"` // typed to match Type (string / int64 / float64)
	Example              any                `json:"example,omitempty"`
	MinLength            *int               `json:"minLength,omitempty"`
	MaxLength            *int               `json:"maxLength,omitempty"`
	Minimum              *float64           `json:"minimum,omitempty"`
	Maximum              *float64           `json:"maximum,omitempty"`
	Properties           map[string]*Schema `json:"properties,omitempty"`
	Items                *Schema            `json:"items,omitempty"`
	AdditionalProperties *Schema            `json:"additionalProperties,omitempty"`
	Required             []string           `json:"required,omitempty"`
}

Schema is a JSON Schema node — the shared shape behind an endpoint's request and response bodies. It maps directly onto the schema object OpenAPI 3.1 uses. When Ref is set the node is only a reference into components/schemas.

func SchemaFor

func SchemaFor(t reflect.Type) Schema

SchemaFor builds an inline Schema from a Go type (no $ref). It is cycle-safe: a self-referential type stops at a bare object rather than recursing forever.

type Server

type Server struct {
	URL         string
	Description string
}

Server is one OpenAPI server entry.

type UI added in v0.2.0

type UI string

UI selects the interactive viewer the docs page renders.

const (
	SwaggerUI UI = "swagger" // Swagger UI — "Try it out" on every operation (default)
	Scalar    UI = "scalar"  // Scalar — modern reference with a built-in client
	Redoc     UI = "redoc"   // Redoc — read-only, three-panel reference
)

type ValidationError

type ValidationError struct {
	Endpoint string   // "POST /api/contact"
	Problems []string // ["description", "owner", "duplicate route"]
}

ValidationError lists everything wrong with one endpoint — missing metadata, a duplicate route, or a path/parameter mismatch.

func (ValidationError) Error

func (e ValidationError) Error() string

Directories

Path Synopsis
adapter
echo
Package echoadapter mounts routes on an Echo router while recording each route's contract in an goenax.Registry — one declaration, two outputs: a live route and a spec entry.
Package echoadapter mounts routes on an Echo router while recording each route's contract in an goenax.Registry — one declaration, two outputs: a live route and a spec entry.
gin
Package ginadapter mounts routes on a Gin router while recording each route's contract in an goenax.Registry — one declaration, two outputs.
Package ginadapter mounts routes on a Gin router while recording each route's contract in an goenax.Registry — one declaration, two outputs.
nethttp
Package nethttpadapter mounts routes on a net/http ServeMux while recording each route's contract in an goenax.Registry — one declaration, two outputs.
Package nethttpadapter mounts routes on a net/http ServeMux while recording each route's contract in an goenax.Registry — one declaration, two outputs.
examples
echo command
Command echo is a runnable Goenax demo on Echo: a few endpoints declared once, served live, documented at /api/docs.
Command echo is a runnable Goenax demo on Echo: a few endpoints declared once, served live, documented at /api/docs.

Jump to

Keyboard shortcuts

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