roda

package module
v0.0.0-...-f12be00 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-roda/roda

roda — go-ruby-roda

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the routing-tree engine at the heart of Ruby's Roda — the segment-consuming request router of MRI 4.0.5's roda gem (the Roda 3.x line). It models Roda's defining idea — a single route block that peels path segments off the front of the request as it descends, capturing them for the branch that handles them — and produces the Rack [status, headers, body] tuple, without any Ruby runtime.

It is a routing backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-rack (the Rack core, which it builds on for the environment and header machinery), go-ruby-erb and go-ruby-regexp.

What it is — and isn't. Matching against the request and consuming the path is fully deterministic and lives here as pure Go. Running the route block is not: it is a Ruby block (in a rbgo binding) or a Go closure (standalone), supplied through the injectable Handler seam. The engine drives the routing tree and the block re-enters it. The HTTP server — the socket accept loop, TLS — is the host's job and is out of scope, exactly as it is for Rack.

The routing tree

A Roda app is one route block. Inside it, matcher methods each try to match the front of the remaining path; a matched on consumes the segments it matched, yields any captures to its block, and terminates the request:

app := roda.New(func(r *roda.RodaRequest) (bool, any) {
    r.Root(func(r *roda.RodaRequest, _ []any) (bool, any) {
        return true, "welcome"                       // GET /
    })
    r.On(func(r *roda.RodaRequest, _ []any) (bool, any) {
        // remaining path here is what's left after "/api"
        r.Is(func(r *roda.RodaRequest, c []any) (bool, any) {
            return true, "user " + c[0].(string)     // GET /api/users/42 -> "user 42"
        }, "users", roda.Sym("id"))
        r.Get(func(r *roda.RodaRequest, _ []any) (bool, any) {
            return true, "api root"                   // GET /api
        })
        return false, nil
    }, "api")
    return false, nil                                 // nothing matched -> 404
})

status, headers, body := app.Call(env)                // Rack triplet

Matchers

On, Is and the verb matchers take any number of matcher values; the engine dispatches on each value's dynamic type, exactly as the roda gem dispatches on the matcher's class:

Go value Ruby matcher Behaviour
string "segment" match one path segment literally
roda.Sym("id") :id (Symbol) match any non-empty segment, capture it as a string
roda.String String (class) like a Symbol capture
roda.Integer Integer (class) match a numeric segment, capture it as an int
true / false true / false always / never match (consumes nothing)
roda.Hash{…} {:param=>…} etc. keyed matchers: "method", "param", "extension"
[]any{a, b, …} [a, b, …] (Array) alternation: first element that matches wins
*regexp.Regexp /re/ (Regexp) match & capture groups (auto-anchored to \A/)
  • On enters a branch (partial match); Is matches only when the path is fully consumed (terminal). The verb matchers Get/Post/Put/Delete add a method test: bare (r.Get(h)) they match when the path is fully consumed; with matchers they are terminal like Is. Root matches a GET to exactly /.
  • Redirect(path[, status]) sets a redirect (default 302) and terminates; Halt terminates immediately with the current response.

The route-block seam

The route block is the one thing Roda cannot make deterministic, so it is an explicit seam:

type Handler func(r *RodaRequest, captures []any) (handled bool, body any)

When a matcher matches, the engine calls the branch's Handler with the captures accumulated so far. The handler may re-enter the tree (call more matcher methods on r) — that is how nested routing works — and returns whether it produced a body and, if so, the body (a string, a []string, or nil). A matched branch always terminates the request, mirroring how Roda throws :halt after a matched on. In a rbgo binding, the Handler runs the Ruby block.

Roda.Call builds the request and response, runs the route block, catches the terminating halt, and finishes the response — defaulting to 404 when nothing matched and 200 when a body was written, with Content-Type: text/html and Content-Length filled in, exactly as RodaResponse#finish does.

Install

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

Fidelity vs the roda gem

Modelled on the Roda 3.x request router (tracking the MRI 4.0.5 line):

  • Segment consumption is byte-for-byte the gem's: _match_string matches on segment boundaries (/foo does not match /foobar), _match_symbol / String capture \A/([^/]+), Integer captures \A/(\d+) bounded by a segment edge, and regexps are auto-anchored to \A/(?:…) with their captures concatenated and the match consumed.
  • on / is / verb / root / redirect / halt semantics match the gem, including the save/restore of the remaining path on a failed branch and the always-terminate-after-match rule.
  • RodaResponse#finish defaults match: empty body → 404, present body → 200, text/html default Content-Type, and Content-Type/Length stripped for 1xx/204/304.
  • Scope. This is the core routing engine. The gem's plugin system, the full Hash-matcher key set (only :method, :param, :extension are modelled here), and rendering are out of scope — they layer on top of this engine. The :extension matcher captures the segment base name and consumes through the extension.

Tests & coverage

go test -race -cover ./...

The suite enforces 100% line coverage — every matcher type, capture passing, verb and root matching, halt/redirect, the response defaults, and the unmatched → 404 path — and cross-compiles on all six supported 64-bit targets (amd64, arm64, riscv64, loong64, ppc64le, s390x, including the big-endian s390x). CI runs the suite on Linux, macOS and Windows and under QEMU for the non-native arches.

License

BSD-3-Clause — see LICENSE. Copyright (c) 2026, the go-ruby-roda/roda 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 roda is a pure-Go (no cgo) reimplementation of the routing-tree engine at the heart of Ruby's Roda web toolkit, matching the semantics of the MRI `roda` gem (Roda 3.x, tracking the 4.0.5 line) without any Ruby runtime.

Roda's defining feature is its routing tree: a request is dispatched by a single route block that peels path segments off the front of the request as it descends, using matcher methods — RodaRequest.On, RodaRequest.Is, the verb matchers RodaRequest.Get/RodaRequest.Post/… , RodaRequest.Root — each of which consumes the segments it matches and yields any captured segments to the block that handles that branch. This package models that segment-consuming engine exactly.

The route block itself is Ruby (in a rbgo binding) or Go (standalone): it is supplied through the injectable Handler seam. The engine drives the tree and the block re-enters it, so the same RodaRequest flows top-down through nested Handler calls. Matching against the request and consuming the path is fully deterministic and lives here as pure Go; running the block is the host's job.

The HTTP server — the socket accept loop, TLS — is out of scope: like Rack, the host owns it. This package builds on github.com/go-ruby-rack/rack for the Rack environment and header machinery, and produces the Rack `[status, headers, body]` tuple through RodaResponse.Finish.

Index

Constants

This section is empty.

Variables

View Source
var (
	// String is the Roda `String` class matcher (see [StringMatcher]).
	String = StringMatcher{}
	// Integer is the Roda `Integer` class matcher (see [IntegerMatcher]).
	Integer = IntegerMatcher{}
)

Sentinel matcher values, provided so callers can write the class matchers without allocating.

Functions

This section is empty.

Types

type Handler

type Handler func(r *RodaRequest, captures []any) (handled bool, body any)

Handler is the injectable seam for a Roda route block. When a matcher method matches, the engine calls the Handler for that branch with the captures accumulated so far. The Handler may re-enter the tree (call more matcher methods on r) — that is how nested routing works — and returns whether it produced a response body and, if so, the body value.

  • handled == true: the returned body (a string, a []string, or nil) becomes the response body.
  • handled == false: no body is written by this block.

Either way, a matched branch terminates the request (Roda always throws :halt after a matched `on`), unless the Handler itself re-entered and an inner branch terminated first. In a rbgo binding, the Handler runs the Ruby block.

type Hash

type Hash map[string]any

Hash is a keyed matcher (the Roda Hash matcher). The supported keys are:

  • "method": a string, or []any of strings — matches when the request method equals any of them (case-insensitive). Consumes nothing.
  • "param": a string key — matches when that request parameter is present and non-empty, capturing its value. Consumes nothing.
  • "extension": a string extension (e.g. "json") — matches when the next segment ends in "."+ext, capturing the segment's base name and consuming through the extension.

All present keys must match (like Ruby's `all?`), and they are evaluated in a fixed order (method, param, extension) for deterministic path consumption.

type IntegerMatcher

type IntegerMatcher struct{}

IntegerMatcher is the Roda `Integer` class matcher: it matches a single segment made entirely of ASCII digits and captures it as an int.

type Roda

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

Roda is a Roda application: a route block plus the machinery to dispatch a Rack request through it. Construct one with New and serve requests with Roda.Call.

func New

func New(route RouteBlock) *Roda

New returns a Roda application that dispatches every request through route, mirroring `Roda.route { |r| … }`.

func (*Roda) Call

func (app *Roda) Call(env rack.Env) (status int, headers *rack.Headers, body []string)

Call dispatches a Rack environment through the routing tree and returns the Rack `[status, headers, body]` triplet, mirroring `Roda#call`. It builds the request and response, runs the route block, catches the terminating :halt, and finishes the response — defaulting to a 404 when nothing matched.

type RodaRequest

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

RodaRequest models Roda's `RodaRequest`: the request as seen by the routing tree. It carries the still-unconsumed remaining path and the captures peeled off it so far, and exposes the matcher methods that consume the path.

func (*RodaRequest) Captures

func (r *RodaRequest) Captures() []any

Captures returns the segments captured so far (RodaRequest#captures).

func (*RodaRequest) Delete

func (r *RodaRequest) Delete(handler Handler, matchers ...any)

Delete matches a DELETE request; see RodaRequest.Get.

func (*RodaRequest) Env

func (r *RodaRequest) Env() rack.Env

Env returns the underlying Rack environment.

func (*RodaRequest) Get

func (r *RodaRequest) Get(handler Handler, matchers ...any)

Get matches a GET request. With no matchers it matches only when the whole path is already consumed; with matchers it matches them terminally (like RodaRequest.Is). Mirrors RodaRequest#get.

func (*RodaRequest) Halt

func (r *RodaRequest) Halt()

Halt terminates the request immediately with the current response, mirroring RodaRequest#halt with no arguments.

func (*RodaRequest) Is

func (r *RodaRequest) Is(handler Handler, matchers ...any)

Is is like RodaRequest.On but only matches when the matchers consume the entire remaining path (a terminal match). Mirrors RodaRequest#is.

func (*RodaRequest) On

func (r *RodaRequest) On(handler Handler, matchers ...any)

On matches the given matchers against the request. If they all match, it consumes the matched segments, yields the captures to the Handler and terminates the request. If they do not all match, the path and captures are restored and On returns so the next branch can be tried. Mirrors RodaRequest#on.

func (*RodaRequest) Post

func (r *RodaRequest) Post(handler Handler, matchers ...any)

Post matches a POST request; see RodaRequest.Get for the matcher semantics.

func (*RodaRequest) Put

func (r *RodaRequest) Put(handler Handler, matchers ...any)

Put matches a PUT request; see RodaRequest.Get.

func (*RodaRequest) Redirect

func (r *RodaRequest) Redirect(target string, status ...int)

Redirect sets a redirect response (default status 302) and terminates the request. Mirrors RodaRequest#redirect.

func (*RodaRequest) RemainingPath

func (r *RodaRequest) RemainingPath() string

RemainingPath returns the still-unconsumed portion of the path (RodaRequest#remaining_path).

func (*RodaRequest) RequestMethod

func (r *RodaRequest) RequestMethod() string

RequestMethod returns the HTTP method (REQUEST_METHOD).

func (*RodaRequest) Response

func (r *RodaRequest) Response() *RodaResponse

Response returns the response being built for this request.

func (*RodaRequest) Root

func (r *RodaRequest) Root(handler Handler)

Root matches a GET request whose remaining path is exactly "/", without consuming it. Mirrors RodaRequest#root.

type RodaResponse

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

RodaResponse models Roda's `RodaResponse` — the mutable response the route block writes into. It buffers a status, an ordered header map (reusing rack.Headers) and a list of body chunks, and assembles the Rack `[status, headers, body]` triplet in RodaResponse.Finish.

It mirrors Roda's defaulting rules: a response whose body is empty defaults to status 404, one with a body defaults to 200, and the Content-Type defaults to text/html — so an app whose routing tree matches nothing yields a 404 without the block doing anything.

func NewResponse

func NewResponse() *RodaResponse

NewResponse returns an empty RodaResponse with no status set.

func (*RodaResponse) Body

func (r *RodaResponse) Body() []string

Body returns the buffered body chunks (RodaResponse#body).

func (*RodaResponse) Empty

func (r *RodaResponse) Empty() bool

Empty reports whether nothing has been written to the body (RodaResponse#empty?).

func (*RodaResponse) Finish

func (r *RodaResponse) Finish() (status int, headers *rack.Headers, body []string)

Finish assembles the Rack `[status, headers, body]` triplet, applying Roda's defaults (RodaResponse#finish): an empty body defaults to 404 and a present body to 200; Content-Type defaults to text/html; Content-Length is set from the buffered length except for statuses that forbid an entity body, whose Content-Type/Length headers are stripped.

func (*RodaResponse) GetHeader

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

GetHeader returns a header value (RodaResponse#[]).

func (*RodaResponse) Headers

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

Headers returns the underlying header map (RodaResponse#headers).

func (*RodaResponse) Redirect

func (r *RodaResponse) Redirect(target string, status int)

Redirect sets the status and Location header for a redirect, matching RodaResponse#redirect. A zero status defaults to 302 (Found).

func (*RodaResponse) SetHeader

func (r *RodaResponse) SetHeader(key string, val any)

SetHeader sets a header value (RodaResponse#[]=).

func (*RodaResponse) SetStatus

func (r *RodaResponse) SetStatus(status int)

SetStatus sets the response status (RodaResponse#status=).

func (*RodaResponse) Status

func (r *RodaResponse) Status() int

Status returns the currently set status (0 if the block set none; RodaResponse.Finish applies the default).

func (*RodaResponse) Write

func (r *RodaResponse) Write(chunk string)

Write appends a chunk to the body and tracks its length, matching RodaResponse#write.

type RouteBlock

type RouteBlock func(r *RodaRequest) (handled bool, body any)

RouteBlock is the top-level route block of a Roda application — the single block passed to `route do |r| … end`. It receives the RodaRequest and drives the routing tree by calling matcher methods on it. It is the same shape as a Handler and, like one, is a seam the host supplies (the Ruby route block in a rbgo binding).

type StringMatcher

type StringMatcher struct{}

StringMatcher is the Roda `String` class matcher: like a Sym, it matches and captures any single non-empty segment.

type Sym

type Sym string

Sym is a Symbol matcher: it matches any single non-empty path segment and captures that segment (as a string) into the captures passed to the Handler. It is the workhorse of Roda routing (`r.on :id do |id| … end`).

Jump to

Keyboard shortcuts

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