hanami

package module
v0.0.0-...-795dc1c Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

go-ruby-hanami/hanami

hanami — go-ruby-hanami

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the core of the Ruby Hanami framework — the Hanami::Router (a fast segment-trie router) and the Hanami::Action request/response lifecycle — matching MRI's hanami-router and hanami-controller 2.x observable behaviour, without any Ruby runtime.

It builds the router's full recognition surface — verb helpers, the root route, named routes with path/URL helpers, path parameters, globbing, per-parameter regexp constraints, nested scopes, redirects and mounted Rack apps — and, on top of it, the action lifecycle: before/after callbacks, halt, redirect_to, status/body/format/header setters, content negotiation, cookies/flash/session accessors and exception handling.

It is the Hanami backend for go-embedded-ruby (a later rbgo binding), but is a standalone, reusable module built on go-ruby-rack — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler) and go-ruby-set.

What it is — and isn't (v0.1). This foundation ships the two standout pieces — the Router and the Action lifecycle — with two explicit seams supplied by the host: the endpoint Resolver (mapping a to: name to a callable) and the action-body ActionCall (the Ruby handle(request, response)). The full app/slices/container boot, view rendering, dry-validation params contracts, assets, the CLI and the settings/providers system are deferred — see the Roadmap.

Install

go get github.com/go-ruby-hanami/hanami

Router

package main

import (
	"fmt"

	"github.com/go-ruby-hanami/hanami"
	"github.com/go-ruby-rack/rack"
)

func main() {
	r := hanami.NewRouter(
		hanami.WithBase("https", "example.com"),
		hanami.WithResolver(func(name string) (hanami.RackApp, bool) {
			// Map endpoint names ("books.show") to callables. A real host
			// resolves these to Hanami::Actions; here a stub.
			return func(env rack.Env) hanami.RackResponse {
				return hanami.RackResponse{Status: 200, Headers: rack.NewHeaders(),
					Body: []string{"book " + env[hanami.RouterParams].(*rack.Params).ToMap()["id"].(string)}}
			}, true
		}),
	)

	r.Root(hanami.ToApp(home))
	r.Get("/books", hanami.ToName("books.index"), hanami.As("books"))
	r.Get("/books/:id", hanami.ToName("books.show"), hanami.As("book"),
		hanami.Constraints(map[string]string{"id": `\d+`}))
	r.Post("/books", hanami.ToName("books.create"))
	r.Get("/assets/*path", hanami.ToApp(serveAsset))
	r.Redirect("/old", "/books", 301)

	r.Scope("/api", func() {
		r.Get("/health", hanami.ToApp(health))
	})
	r.Mount("/admin", adminRackApp)

	// Path / URL helpers from named routes:
	p, _ := r.Path("book", map[string]string{"id": "42"}) //   /books/42
	u, _ := r.URL("book", map[string]string{"id": "42"})  //   https://example.com/books/42
	fmt.Println(p, u)

	// Dispatch a Rack env:
	res := r.Call(rack.Env{rack.RequestMethod: "GET", rack.PathInfo: "/books/42"})
	fmt.Println(res.Status, res.Body) // 200 [book 42]
}

Recognition gives static segments priority over dynamic, and dynamic over the glob; an unmatched path is 404, a path that matches with the wrong method is 405 with a sorted Allow header, and HEAD falls back to a GET route — exactly as Hanami::Router does.

The endpoint Resolver seam

to: accepts three shapes: ToName (an endpoint name resolved lazily through the router's Resolver), ToApp (a Rack callable) and ToAction (a hanami.Action). The Resolver is where a host maps "books.show" to the concrete endpoint; a router without one treats every named endpoint as unresolved (404).

Action

show := hanami.NewAction("books.show",
	// The ActionCall seam — the Ruby `handle(request, response)` body:
	func(name string, req *hanami.Request, resp *hanami.Response) error {
		if !req.ParamsValid() {
			resp.Halt(422, "invalid")
		}
		resp.SetFormat("json")
		resp.SetBody(`{"id":` + req.Param("id") + `}`)
		return nil
	},
	hanami.Before(authenticate),
	hanami.After(logRequest),
	hanami.Accept("json", "html"),
	hanami.HandleException(func(err error, _ *hanami.Request, resp *hanami.Response) bool {
		resp.SetStatus(500)
		return true
	}),
)

status, headers, body := show.Call(env).ToTuple()

The lifecycle runs content negotiation → before → the body → exception handling → after, and finalises the Rack tuple. halt and redirect_to unwind immediately (mirroring Ruby's throw :halt); before/after callbacks read the request and mutate the response; exception handlers are tried in order, falling back to 500.

The ActionCall seam

The action body is a single Go function — func(name string, req *Request, resp *Response) error — that reads the Request and mutates the Response. This is the one seam a later rbgo binding plugs a Ruby action into; everything around it (params merge, callbacks, negotiation, halt, cookies/flash/session, finish) is pure Go.

Request / Response over Rack

Request layers over rack.Request: it merges path params (from the router) with the query and body params — path wins — through an optional ParamsValidator seam, and exposes Session, Cookies, Flash, and content negotiation (Format, Accepts). Response layers over rack.Response: SetStatus/SetBody/SetFormat/SetHeader, RedirectTo, Halt, cookie scheduling and a two-generation Flash, finalised (content-type from format, session commit, cookie encoding, content-length) via rack.Response.

Fidelity vs hanami-router / hanami-controller 2.x

Area Status
Verb helpers get/post/put/patch/delete/options/trace/link/unlink
root, named routes (as:), path/url helpers (+ leftover params → query)
Path params :id, globbing *rest, per-param regexp constraints
Segment-trie recognition (static > dynamic > glob), 404/405+Allow, HEADGET
scope (nested), redirect, mount (Rack, SCRIPT_NAME/PATH_INFO split)
Endpoint resolver seam (to: name → callable)
Action lifecycle: before/after, halt, redirect_to, status/body/format/headers
Params merge + validation seam, content negotiation, handle_exception
Request/Response over Rack; cookies, flash, session seam
App / slices / container boot (dry-system), settings & providers ⏳ Roadmap
View rendering (hanami-view), assets, params contracts (dry-validation) ⏳ Roadmap
CLI / generators ⏳ Roadmap

Roadmap

The v0.1 foundation is the Router + Action core. Deferred, in rough priority order:

  1. hanami-view rendering (templates, parts, scopes, context) — the natural pair to the action body seam.
  2. dry-validation params contracts (this pass ships the validation seam; the contract DSL is deferred).
  3. The app / slices / container boot (dry-system) — auto-registration, providers, the settings system.
  4. Assets (hanami-assets) and the CLI / generators.
  5. The rbgo binding wiring the Ruby handle body and endpoint resolution into the seams.

Tests & coverage

Deterministic, ruby-free tests hold coverage at 100% — router recognition across every verb, path params, constraints, globs, scopes, mounts, redirects and named helpers; the action lifecycle with callbacks, halt, redirect, exception handling, params validation, negotiation, session/flash/cookies — so the qemu cross-arch and Windows lanes pass the gate.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

CGO-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — including big-endian s390x) and three OSes (Linux, macOS, Windows).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-hanami/hanami authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package hanami is a pure-Go (no cgo) reimplementation of the deterministic core of the Ruby Hanami framework — the Router (hanami-router) and the Action lifecycle (hanami-controller) — matching MRI's `hanami-router` and `hanami-controller` 2.x observable behaviour, without any Ruby runtime.

It builds a fast segment-trie router with the full recognition surface — verb helpers (get/post/put/patch/delete/…), the root route, named routes (`as:`) with path/URL helpers, path parameters (`/books/:id`), globbing (`*rest`), per-parameter regexp constraints, nested scopes, redirects and mounted Rack apps — and dispatches a matched request to a resolved endpoint. On top of it, Action runs the request/response lifecycle: `before`/`after` callbacks, `halt`, `redirect_to`, status/body/format/header setters, content negotiation, cookies/flash/session accessors and exception handling.

The library reuses github.com/go-ruby-rack/rack for the Rack env, request, response, headers and parameter model — this package never touches the network. Two things are explicit seams, supplied by the host:

  • the endpoint Resolver, mapping a `to:` string ("books.index") to a callable RackApp; and
  • the action body ActionCall, the Ruby `handle(request, response)` method, which reads the Request and mutates the Response.

This is the v0.1 foundation for a later rbgo binding. The full app / slices / container boot, hanami-view rendering, dry-validation params contracts, assets, the CLI/generators and the settings/providers system are deferred — see the README roadmap.

Index

Constants

View Source
const RouterParams = "router.params"

RouterParams is the env key under which Call stores the matched path params (Hanami's router.params), read by Action to build request params.

Variables

This section is empty.

Functions

This section is empty.

Types

type Action

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

Action is a pure-Go port of the Hanami::Action lifecycle: it builds a Request/Response over Rack, runs content negotiation, `before` callbacks, the ActionCall body, exception handling and `after` callbacks, honouring `halt`/`redirect_to`, then finalises the Rack tuple. It is a RackApp via Action.Call.

func NewAction

func NewAction(name string, handle ActionCall, opts ...ActionOption) *Action

NewAction builds an action named name whose body is handle.

func (*Action) Call

func (a *Action) Call(env rack.Env) RackResponse

Call runs the full action lifecycle over env and returns the Rack response.

func (*Action) Name

func (a *Action) Name() string

Name returns the action's name.

type ActionCall

type ActionCall func(name string, req *Request, resp *Response) error

ActionCall is the action business body — the Ruby `handle(request, response)` method as a Go seam. It receives the action name, the built Request and the Response to mutate, and returns an error to trigger exception handling. This is the single seam a later rbgo binding plugs the Ruby action into.

type ActionOption

type ActionOption func(*Action)

ActionOption configures an Action.

func Accept

func Accept(formats ...string) ActionOption

Accept restricts the action to the given formats; a request that accepts none of them is answered 406 Not Acceptable (Hanami's `format`/`accept`).

func After

func After(cbs ...Callback) ActionOption

After appends after callbacks.

func Before

func Before(cbs ...Callback) ActionOption

Before appends before callbacks.

func HandleException

func HandleException(hs ...ExceptionHandler) ActionOption

HandleException registers exception handlers, tried in registration order.

func WithDefaultFormat

func WithDefaultFormat(format string) ActionOption

WithDefaultFormat sets the initial response format (e.g. "json").

func WithDefaultStatus

func WithDefaultStatus(status int) ActionOption

WithDefaultStatus sets the initial response status (default 200).

func WithParamsValidator

func WithParamsValidator(v ParamsValidator) ActionOption

WithParamsValidator sets the params-validation seam.

func WithSessionLoader

func WithSessionLoader(l SessionLoader) ActionOption

WithSessionLoader sets the session-store seam.

type Callback

type Callback func(req *Request, resp *Response)

Callback is a before/after hook (Hanami's `before`/`after`). It may read the request and mutate the response, and may call Response.Halt to short-circuit.

type ExceptionHandler

type ExceptionHandler func(err error, req *Request, resp *Response) bool

ExceptionHandler handles an error raised by the action body (Hanami's `handle_exception`). It returns true when it has handled the error (having set the response); the first handler to return true wins. When none handles it, the action responds 500.

type Flash

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

Flash is Hanami's flash: a two-generation message store. Values set this request are readable next request; values carried in from last request are readable now and swept after the response is built.

func NewFlash

func NewFlash(prev map[string]any) *Flash

NewFlash builds a Flash whose "now" generation is prev (may be nil).

func (*Flash) Empty

func (f *Flash) Empty() bool

Empty reports whether both generations hold no messages.

func (*Flash) Get

func (f *Flash) Get(key string) (any, bool)

Get reads a value from the current generation, falling back to the value just set for the next request.

func (*Flash) Keep

func (f *Flash) Keep(key string)

Keep re-carries a current-generation value into the next request.

func (*Flash) Next

func (f *Flash) Next() map[string]any

Next returns the map to persist for the following request.

func (*Flash) Set

func (f *Flash) Set(key string, value any)

Set stores a value for the next request (Hanami's `flash[key]=`).

type ParamsValidator

type ParamsValidator func(raw *rack.Params) (*rack.Params, error)

ParamsValidator is the params-validation seam (Hanami's params contract). It receives the raw merged params and returns the validated params and an error. A nil validator passes the raw params through unchanged.

type RackApp

type RackApp func(env rack.Env) RackResponse

RackApp is a Rack application: it maps a Rack rack.Env to a response tuple. It is the callable an endpoint resolves to, and the shape a mounted app must have. A Router and an Action are both RackApps via their Call method.

type RackResponse

type RackResponse struct {
	Status  int
	Headers *rack.Headers
	Body    []string
}

RackResponse is the SPEC `[status, headers, body]` tuple in struct form. It is what Router.Call and Action.Call return; use RackResponse.ToTuple to get the three plain values.

func (RackResponse) ToTuple

func (r RackResponse) ToTuple() (int, *rack.Headers, []string)

ToTuple returns the response as the three Rack SPEC values.

type Request

type Request struct {
	*rack.Request
	// contains filtered or unexported fields
}

Request is Hanami::Action::Request: a thin layer over rack.Request that adds the merged params (path + query + body), content negotiation, and session, cookie and flash accessors. It is built by Action.Call and handed to the action body and callbacks.

func (*Request) Accepts

func (r *Request) Accepts(format string) bool

Accepts reports whether the request's Accept header accepts the given short format (or "*/*" wildcard), matching Action's content negotiation.

func (*Request) Cookies

func (r *Request) Cookies() *rack.Params

Cookies returns the parsed request cookies.

func (*Request) Flash

func (r *Request) Flash() *Flash

Flash returns the request's Flash.

func (*Request) Format

func (r *Request) Format() string

Format returns the negotiated short format name (e.g. "json", "html") derived from the request CONTENT_TYPE then the Accept header, or "" when neither maps to a known format.

func (*Request) Param

func (r *Request) Param(key string) string

Param returns the string value of a single param, or "" if absent or non-string, a convenience over Request.Params.

func (*Request) Params

func (r *Request) Params() *rack.Params

Params returns the merged, validated request params.

func (*Request) ParamsError

func (r *Request) ParamsError() error

ParamsError returns the validator's error, or nil when the params are valid.

func (*Request) ParamsValid

func (r *Request) ParamsValid() bool

ParamsValid reports whether the params validator (if any) accepted the input. A request with no validator is always valid.

func (*Request) Session

func (r *Request) Session() map[string]any

Session returns the mutable session map (Hanami's `session`).

type Resolver

type Resolver func(name string) (RackApp, bool)

Resolver maps a `to:` endpoint name (e.g. "books.index") to a RackApp. It is the host seam by which action names become callables. It returns false when the name is unknown, which the router reports as a 404. A router with no resolver treats every string endpoint as unresolved.

type Response

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

Response is Hanami::Action::Response: a mutable response the action body and callbacks build up — status, body, headers, format, cookies, session and flash — finalised to a Rack tuple by Action.Call. Reuses rack.Response and rack.Headers for finishing (content-length, cookie encoding).

func (*Response) Body

func (r *Response) Body() string

Body returns the buffered body string.

func (*Response) DeleteCookie

func (r *Response) DeleteCookie(key string, value rack.CookieValue)

DeleteCookie schedules an expiring Set-Cookie for key at finish.

func (*Response) Flash

func (r *Response) Flash() *Flash

Flash returns the response Flash.

func (*Response) Format

func (r *Response) Format() string

Format returns the current short format name.

func (*Response) GetHeader

func (r *Response) GetHeader(key string) any

GetHeader returns a response header value.

func (*Response) Halt

func (r *Response) Halt(status int, body string)

Halt sets the status and body and unwinds the lifecycle immediately, matching Hanami's `halt`. A zero or empty body defaults to the status' reason phrase.

func (*Response) Headers

func (r *Response) Headers() *rack.Headers

Headers returns the response headers.

func (*Response) RedirectTo

func (r *Response) RedirectTo(url string, status int)

RedirectTo sets a redirect to url with the given status (default 302) and halts the lifecycle, matching Hanami's `redirect_to`.

func (*Response) Session

func (r *Response) Session() map[string]any

Session returns the mutable session map.

func (*Response) SetBody

func (r *Response) SetBody(body string)

SetBody replaces the response body (Hanami's `response.body=`).

func (*Response) SetCookie

func (r *Response) SetCookie(key string, value rack.CookieValue)

SetCookie schedules a Set-Cookie for key with the given value at finish (Hanami's `response.cookies[key]=`).

func (*Response) SetFormat

func (r *Response) SetFormat(format string)

SetFormat sets the response format, which selects the content-type at finish (Hanami's `response.format=`). An unknown format leaves the content-type unset.

func (*Response) SetHeader

func (r *Response) SetHeader(key string, value any)

SetHeader sets a response header.

func (*Response) SetStatus

func (r *Response) SetStatus(status int)

SetStatus sets the response status code (Hanami's `response.status=`).

func (*Response) Status

func (r *Response) Status() int

Status returns the response status code.

func (*Response) Write

func (r *Response) Write(chunk string)

Write appends to the response body.

type Route

type Route struct {
	Method  string
	Pattern string
	Name    string
	// contains filtered or unexported fields
}

Route is a single declared route: a method, the full path pattern (including any scope prefix), its parsed segments, the target endpoint and an optional name for the path/URL helpers.

type RouteOption

type RouteOption func(*routeOptions)

RouteOption configures a single route declaration (`as:`, `constraints:`).

func As

func As(name string) RouteOption

As names the route for the path/URL helpers (Hanami's `as:`).

func Constraints

func Constraints(c map[string]string) RouteOption

Constraints attaches per-parameter regexp constraints (Hanami's `constraints:`). Each value is an un-anchored regexp matched against the whole segment.

type Router

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

Router is a pure-Go port of Hanami::Router: a segment-trie of routes with verb helpers, named path/URL helpers, params, constraints, scopes, redirects and mounts. Build it with NewRouter, declare routes with the verb methods, and dispatch with Router.Call. It is itself a RackApp.

func NewRouter

func NewRouter(opts ...RouterOption) *Router

NewRouter builds an empty Router. Pass WithResolver and/or WithBase, then declare routes.

func (*Router) Call

func (rt *Router) Call(env rack.Env) RackResponse

Call matches env's method and PATH_INFO and dispatches to the resolved endpoint, returning 404 when nothing matches the path and 405 (with an Allow header) when the path matches but the method does not. HEAD falls back to a GET route, matching Hanami. It is the RackApp entry point.

func (*Router) Delete

func (rt *Router) Delete(path string, to To, opts ...RouteOption) *Route

func (*Router) Get

func (rt *Router) Get(path string, to To, opts ...RouteOption) *Route

Verb helpers. Each declares a route for its HTTP method.

func (rt *Router) Link(path string, to To, opts ...RouteOption) *Route

func (*Router) Mount

func (rt *Router) Mount(prefix string, app RackApp)

Mount attaches a Rack app at a prefix. Requests whose path is the prefix or begins with "prefix/" are dispatched to app with SCRIPT_NAME/PATH_INFO adjusted (the prefix is moved into SCRIPT_NAME), matching Hanami's `mount`. Mounts are matched for any HTTP method, longest prefix first.

func (*Router) Options

func (rt *Router) Options(path string, to To, opts ...RouteOption) *Route

func (*Router) Patch

func (rt *Router) Patch(path string, to To, opts ...RouteOption) *Route

func (*Router) Path

func (rt *Router) Path(name string, params map[string]string) (string, error)

Path builds the path for the named route, substituting params into its dynamic and glob segments and appending any leftover params as a sorted query string, matching Hanami's `routes.path(:name, **params)`. It errors on an unknown name or a missing required parameter.

func (*Router) Post

func (rt *Router) Post(path string, to To, opts ...RouteOption) *Route

func (*Router) Put

func (rt *Router) Put(path string, to To, opts ...RouteOption) *Route

func (*Router) Redirect

func (rt *Router) Redirect(path, target string, status int, opts ...RouteOption) *Route

Redirect declares a route that responds with a redirect to target. The default status is 301 (Hanami's `redirect` default).

func (*Router) Root

func (rt *Router) Root(to To, opts ...RouteOption) *Route

Root declares the GET "/" route, named :root, matching Hanami's `root`.

func (*Router) Routes

func (rt *Router) Routes() []*Route

Routes returns the declared routes in declaration order.

func (*Router) Scope

func (rt *Router) Scope(prefix string, fn func())

Scope declares routes under a path prefix. Prefixes nest.

func (*Router) Trace

func (rt *Router) Trace(path string, to To, opts ...RouteOption) *Route

func (*Router) URL

func (rt *Router) URL(name string, params map[string]string) (string, error)

URL builds the absolute URL for the named route, prefixing Router.Path with the configured scheme and host, matching Hanami's `routes.url(:name, …)`.

func (rt *Router) Unlink(path string, to To, opts ...RouteOption) *Route

type RouterOption

type RouterOption func(*Router)

RouterOption configures a Router at construction.

func WithBase

func WithBase(scheme, host string) RouterOption

WithBase sets the scheme and host used by Router.URL (default "http", "localhost").

func WithResolver

func WithResolver(r Resolver) RouterOption

WithResolver sets the endpoint Resolver.

type SessionLoader

type SessionLoader func(env rack.Env) map[string]any

SessionLoader loads the session map for a request (the session-store seam). A nil loader reads env["rack.session"] when it is a map[string]any.

type To

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

To is the `to:` argument of a route declaration. Build it with ToName (an endpoint name resolved by the router's Resolver), ToApp (a Rack callable) or ToAction (a Action).

func ToAction

func ToAction(a *Action) To

ToAction targets a Action; the action's Call is used as the endpoint.

func ToApp

func ToApp(app RackApp) To

ToApp targets a Rack callable directly. This is Hanami's `to: ->(env){…}`.

func ToName

func ToName(name string) To

ToName targets an endpoint name (e.g. "books.index"), resolved lazily at dispatch time through the router's Resolver. This is Hanami's `to: "…"`.

Jump to

Keyboard shortcuts

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