grape

package module
v0.0.0-...-be187ca 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: 9 Imported by: 0

README

go-ruby-grape/grape

grape — go-ruby-grape

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic, interpreter-independent core of Ruby's Grape REST-API framework (the grape gem) — route modelling + matching, params validation/coercion, and response formatting — so a host can resolve a request, validate its parameters, and shape a response the way Grape does without any Ruby runtime.

It is the API-machinery backend for go-embedded-ruby, a sibling of go-ruby-rack. Running an endpoint body and parsing the Rack env are the host's job; this library hands back the small, explicit models the host maps to and from its own objects.

What it is — and isn't. Matching (method, path, headers) to a route, coercing/validating a params tree, and serialising a value to json / xml / txt are fully deterministic and need no interpreter, so they live here as pure Go. Binding the endpoint block to a live Ruby method — evaluating the body, reading params inside it — is the host's job.

The three deterministic pieces

Router — (method, path) → (route, path params) + 404/405/406
rt := grape.NewRouter()
rt.Get("/users/:id", getUser)   // handler is an opaque host reference
rt.Post("/users", createUser)

m := rt.Match("GET", "/users/42")
// m.Status == grape.StatusOK; m.Route.Handler == getUser; m.Params["id"] == "42"

rt.Match("DELETE", "/users/42") // StatusMethodNotAllowed, m.Allowed lists verbs
rt.Match("GET", "/nope")        // StatusNotFound

Path patterns cover :name captures, *wildcard tails, and .:format suffixes (/users/:id.:format). A GET route answers HEAD; an empty/* method matches any verb. [Version] models the four version strategies (path / header / param / accept), and [Negotiate] resolves the response format from an extension, a forced format, or the Accept header (the 406 decision).

ParamsValidator — coerce + validate, or raise Grape's exact errors
set := &grape.ParamSet{Params: []*grape.Param{
    {Name: "id", Required: true, Type: grape.TypeInteger},
    {Name: "name", Type: grape.TypeString, Values: []any{"a", "b", "c"}},
}}
v := grape.NewParamsValidator(set)

coerced, errs := v.Validate(grape.Raw{"id": "42", "name": "z"})
// errs.Error() == "name does not have a valid value"   (matches the gem verbatim)

coerced, _ = v.Validate(grape.Raw{"id": "42"})
// coerced["id"] == int64(42)

Validators, all producing Grape's exact message fragments:

declaration message on failure
requires (absent) id is missing
type: coercion id is invalid
values: / range name does not have a valid value
except_values: x has a value not allowed
length: {min,max} name is expected to have length within 2 and 5
length: {min} / {max} … greater/less than or equal to N
regexp: email is invalid
allow_blank: false nm is empty
mutually_exclusive a, b are mutually exclusive
exactly_one_of a, b are missing, exactly one parameter must be provided
at_least_one_of a, b are missing, at least one parameter must be provided
all_or_none_of a, b provide all or none of parameters

Types coerce to the Ruby value model: Integerint64 / *big.Int, Floatfloat64, Booleanbool, Date/Timetime.Time, JSON → the generic tree, Array[T][]any, Hash → a nested group. A blank optional scalar coerces to nil (as Grape does); default: supplies a literal or a callable (DefaultFunc); coerce_with: swaps in a host lambda; nested groups nest error names as grp[inner].

Formatter — json / txt / xml
var f grape.Formatter
m := grape.NewOrderedMap()
m.Set("id", int64(42))
m.Set("name", "ada")
body, mime, _ := f.Format("json", m) // {"id":42,"name":"ada"}, application/json

OrderedMap preserves hash key order through JSON/XML; a plain map[string]any is emitted in sorted-key order for determinism. error! / status shaping lives in [Response] / [ErrorBody].

Value model

The validator and formatter exchange a small, fixed set of Go types the host maps to its own objects:

Ruby Go
nil nil
true/false bool
Integer int64 / *big.Int
Float float64
String string
Time/Date time.Time
Array []any
Hash map[string]any / *grape.OrderedMap

What the host (rbgo) binds

The Ruby-facing seam — mapping Grape::API.get/post/params/… DSL calls onto Router + ParamSet, running the endpoint block, and reading the Rack env — is the host's job (ties to go-ruby-rack). The host feeds (method, path) to Router.Match, the parsed params tree to ParamsValidator.Validate, and the endpoint's return value to Formatter.Format.

Install

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

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: the same raw params are fed to these validators and to a live grape API (via rack-test), and their coerced output + error messages are compared; (method, path) is fed to both routers and the matched route + path params + status are compared. The oracle skips itself where ruby or the grape gem is absent.

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

CGO-free, dependency-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-grape/grape 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 grape is a pure-Go (no cgo) reimplementation of the deterministic, interpreter-independent core of Ruby's Grape REST-API framework (the `grape` gem): route modelling + matching, params validation/coercion, and response formatting.

It is the API-machinery backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-rack. Three deterministic pieces live here:

  • A Router: add routes (get/post/…, path patterns with :params, .:format suffixes and *wildcards, namespaces, versioning) and resolve (method, path, headers) → (matched route, path params) plus the 404 / 405 / 406 decisions.
  • A ParamsValidator: turn a params declaration (requires/optional, type coercion, values, length, regexp, mutual-exclusion, defaults, nested groups) plus raw params into the coerced params hash or a ValidationErrors carrying Grape's exact messages.
  • A Formatter: json / txt / xml serialisation of the response value shape.

Binding endpoint blocks to live Ruby objects and parsing the Rack env are the host's job (rbgo, tied to go-ruby-rack); this library hands back a small, explicit value model the host maps to and from its own objects.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DefaultStatus

func DefaultStatus(method string) int

DefaultStatus returns the status Grape assigns to a method when the endpoint does not set one explicitly: 201 for POST, 200 otherwise. HEAD mirrors GET.

func MimeFor

func MimeFor(format string) string

MimeFor returns the MIME type Grape emits for a format symbol, or "" if the format is unknown.

func Negotiate

func Negotiate(extFormat, forced, accept string, offered []string) (string, bool)

Negotiate resolves the response format for a request. It applies Grape's precedence: an explicit ".:format" path extension (extFormat, may be "") wins; otherwise a forced API-wide format (forced, may be "") wins; otherwise the Accept header is matched against the API's offered formats. offered lists the format symbols the API serves, in preference order.

It returns the chosen format and true, or ("", false) when the request cannot be satisfied — the 406 Not Acceptable decision.

func NotFoundBody

func NotFoundBody() (int, string)

NotFoundBody / MethodNotAllowedBody / NotAcceptableBody produce the plain bodies Grape returns for the routing failures. Grape returns a bare "404 Not Found" text for 404 and a JSON error for 405/406 under a JSON API.

Types

type ErrorBody

type ErrorBody struct {
	Message any // string, or a structured value for error!(hash, status)
	Status  int
}

ErrorBody is the value Grape serialises for an error! / a raised failure: a single "error" key (plus optional detail). It is emitted through the same Formatter as a normal response.

func MethodNotAllowedBody

func MethodNotAllowedBody() (int, ErrorBody)

MethodNotAllowedBody returns the 405 status and the value Grape serialises ({"error":"405 Not Allowed"}).

func NotAcceptableBody

func NotAcceptableBody() (int, ErrorBody)

NotAcceptableBody returns the 406 status and error value.

func ValidationErrorBody

func ValidationErrorBody(v *ValidationErrors) ErrorBody

ValidationErrorBody shapes a *ValidationErrors into the 400 body Grape returns: {"error":"<joined messages>"}. The status is always 400.

func (ErrorBody) Value

func (e ErrorBody) Value() any

Value renders the error into the tree the formatter serialises: {"error": …}.

type Formatter

type Formatter struct{}

Formatter serialises a response value to the wire form for a format symbol. The value model is the generic Go tree the host produces from the endpoint's return value: nil, bool, integers, floats, string, []any, and map[string]any (or *OrderedMap for order-preserving hashes). Binding a live Ruby object to this tree is the host's job; the formatter only serialises the tree.

func (Formatter) Format

func (f Formatter) Format(format string, v any) (body string, mime string, err error)

Format serialises v for the given format symbol ("json", "txt", "xml"). It returns the body and the Content-Type MIME. An unknown format is an error.

func (Formatter) JSON

func (f Formatter) JSON(v any) (string, error)

JSON renders v as compact JSON, matching Grape's default (MultiJson) output: no spaces, sorted keys for plain maps, insertion order for OrderedMap.

func (Formatter) Txt

func (f Formatter) Txt(v any) string

Txt renders v the way Grape's Txt formatter does: a String is emitted verbatim, anything else via its to_s form. For the generic tree that is a JSON-free inspection; the host may supply richer to_s via the value already being a string. Here a string passes through and other values use a Go string form.

func (Formatter) XML

func (f Formatter) XML(v any) (string, error)

XML renders v in the ActiveSupport-compatible shape Grape's Xml formatter emits: a map becomes a "<hash>" element whose children are its typed keys, an array nests repeated child elements, and scalars carry a type attribute.

type Match

type Match struct {
	Status  MatchStatus
	Route   *Route
	Params  map[string]string
	Allowed []string
}

Match resolves a request. On StatusOK, Route and Params are the matched route and its captured path params. On StatusMethodNotAllowed, Allowed lists the methods the path does accept (for the Allow header). GET implicitly satisfies a HEAD request (Grape/Rack semantics).

type MatchStatus

type MatchStatus int

MatchStatus is the routing decision for a request.

const (
	// StatusOK: a route matched both method and path.
	StatusOK MatchStatus = iota
	// StatusNotFound (404): no route's path matched.
	StatusNotFound
	// StatusMethodNotAllowed (405): the path matched at least one route but not
	// for the request method.
	StatusMethodNotAllowed
)

type OrderedMap

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

OrderedMap is an insertion-ordered string-keyed map, so a hash's key order survives JSON/XML serialisation (a plain map[string]any is emitted in sorted key order for determinism).

func NewOrderedMap

func NewOrderedMap() *OrderedMap

NewOrderedMap returns an empty ordered map.

func (*OrderedMap) Get

func (m *OrderedMap) Get(key string) (any, bool)

Get returns the value for key.

func (*OrderedMap) Keys

func (m *OrderedMap) Keys() []string

Keys returns the keys in insertion order.

func (*OrderedMap) Len

func (m *OrderedMap) Len() int

Len returns the number of entries.

func (*OrderedMap) Set

func (m *OrderedMap) Set(key string, val any)

Set inserts or updates a key, preserving first-insertion order.

type Param

type Param struct {
	Name     string
	Required bool // requires vs optional

	Type     Type // scalar type; "" means untyped (raw string passthrough)
	ElemType Type // element type for Array[T]; "" for a bare Array
	IsArray  bool // type: Array or Array[T]
	IsHash   bool // type: Hash (a nested group container)

	// Values / ExceptValues constrain the coerced value. A value list may be an
	// explicit set (Values) or an inclusive integer range (RangeMin..RangeMax
	// when HasRange is set).
	Values       []any
	ExceptValues []any
	HasRange     bool
	RangeMin     int64
	RangeMax     int64

	// Length constraints (String/Array). Present flags gate each bound.
	HasMinLen bool
	MinLen    int
	HasMaxLen bool
	MaxLen    int

	Regexp     Regexp // regexp: /…/ — matched via the injected matcher
	AllowBlank *bool  // allow_blank: false rejects "" / whitespace; nil = default true

	// Default supplies a value when the param is absent. If DefaultFunc is set it
	// is called lazily (callable default); otherwise Default is used verbatim.
	HasDefault  bool
	Default     any
	DefaultFunc func() any

	// CoerceWith replaces built-in coercion with a host lambda; it receives the
	// raw string and returns the coerced value (and false to signal invalid).
	CoerceWith func(raw string) (any, bool)

	// Group holds the nested declarations of a Hash param (`requires :g, type:
	// Hash do … end`), including its own cross-parameter validators, so nested
	// exclusivity reports names as "grp[a], grp[b]".
	Group *ParamSet
}

Param is one declared parameter in a `params do … end` block. It mirrors the options a `requires`/`optional` line carries. The host builds these from the Ruby DSL; the validator consumes them.

type ParamSet

type ParamSet struct {
	Params []*Param

	MutuallyExclusive [][]string
	ExactlyOneOf      [][]string
	AtLeastOneOf      [][]string
	AllOrNoneOf       [][]string
}

ParamSet is a full `params do … end` declaration plus the cross-parameter group validators (mutually_exclusive and friends).

type ParamsValidator

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

ParamsValidator validates and coerces raw request params against a ParamSet.

func NewParamsValidator

func NewParamsValidator(set *ParamSet) *ParamsValidator

NewParamsValidator builds a validator for a declaration.

func (*ParamsValidator) Validate

func (v *ParamsValidator) Validate(raw Raw) (map[string]any, *ValidationErrors)

Validate coerces raw against the declaration. On success it returns the coerced params map and nil. On failure it returns the partially-coerced map and a *ValidationErrors carrying every failure in declaration order — the same order (and messages) Grape renders into its 400 body.

type Raw

type Raw = map[string]any

Raw is the request parameter tree the host hands in. A leaf is a string; a nested Hash param is a map[string]any; an array is a []any of strings (or of nested maps for Array-of-Hash). This mirrors what Rack parses from a query / form / JSON body.

type Regexp

type Regexp struct {
	Source string              // the pattern source, for diagnostics
	Match  func(s string) bool // returns true when s matches
}

Regexp is a compiled-pattern reference. The deterministic core does not embed a regexp engine (that is go-ruby-regexp's job); the host supplies a Match function. A zero Regexp (Match nil) means "no regexp constraint".

type Response

type Response struct {
	Status int
	Value  any
	Format string
}

Response is the shaped result of an endpoint: an HTTP status, a value to serialise, and the negotiated format. The host runs the endpoint body (that is the seam) and hands the outcome here to be shaped into a body via a Formatter.

type Route

type Route struct {
	Method  string // upper-case HTTP verb ("GET", "POST", …); "" or "*" matches any
	Pattern string // the raw declared pattern, e.g. "/users/:id"

	// Handler is an opaque host reference to the endpoint body. The router never
	// runs it; it is handed back on a match so the host (rbgo) can invoke it.
	Handler any
	// contains filtered or unexported fields
}

Route is a single Grape endpoint: an HTTP method plus a compiled path pattern. A pattern is a slash-separated list of segments; a segment may be a literal, a named capture (":name"), or a trailing wildcard ("*name") that greedily swallows the remainder of the path. A ".:format" suffix on the final segment binds the request extension (e.g. "/users/:id.:format").

func NewRoute

func NewRoute(method, pattern string, handler any) *Route

NewRoute compiles a method + pattern into a Route. The pattern is normalised: a leading slash is optional, and a trailing slash is ignored (Grape treats "/users" and "/users/" alike).

type Router

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

Router holds an ordered set of routes and resolves a request to a match. Route order is significant: the first route whose method and path both match wins, mirroring Grape's first-declared-first-served dispatch.

func NewRouter

func NewRouter() *Router

NewRouter returns an empty Router.

func (*Router) Add

func (rt *Router) Add(r *Route)

Add appends a route. Routes are matched in insertion order.

func (*Router) Delete

func (rt *Router) Delete(pattern string, h any) *Route

func (*Router) Get

func (rt *Router) Get(pattern string, h any) *Route

Get/Post/Put/Patch/Delete/Head add a route for the corresponding verb and return it, so a handler can be attached fluently.

func (*Router) Head

func (rt *Router) Head(pattern string, h any) *Route

func (*Router) Match

func (rt *Router) Match(method, path string) Match

Match resolves (method, path) to a routing decision. HTTP status content negotiation (406) is a separate step; see [Router.Negotiate].

func (*Router) Patch

func (rt *Router) Patch(pattern string, h any) *Route

func (*Router) Post

func (rt *Router) Post(pattern string, h any) *Route

func (*Router) Put

func (rt *Router) Put(pattern string, h any) *Route

func (*Router) Routes

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

Routes returns the routes in declaration order.

type Type

type Type string

Type names the coercion targets Grape's params DSL understands. A scalar type coerces a single raw string; Array[T] / Hash coerce collections (handled in validate.go via ElemType).

const (
	TypeString  Type = "String"
	TypeInteger Type = "Integer"
	TypeFloat   Type = "Float"
	TypeBoolean Type = "Boolean"
	TypeDate    Type = "Date"
	TypeTime    Type = "Time"
	TypeJSON    Type = "JSON"
	TypeArray   Type = "Array"
	TypeHash    Type = "Hash"
	TypeFile    Type = "File"
)

type ValidationError

type ValidationError struct {
	Param   string // e.g. "id" or "grp[inner]"
	Message string // e.g. "is missing", "is invalid"
}

ValidationError is a single parameter failure: the fully-qualified parameter name (nested groups joined as "group[inner]") plus the message fragment Grape emits for it (without the parameter prefix).

func (ValidationError) Error

func (e ValidationError) Error() string

Error renders the failure the way Grape does: "<param> <message>".

type ValidationErrors

type ValidationErrors struct {
	Errors []ValidationError
}

ValidationErrors is the aggregate raised as Grape::Exceptions::ValidationErrors. Its Error() string is the comma-joined per-parameter messages, matching the body Grape returns for a 400 (`{"error":"id is missing, name is invalid"}`).

func (*ValidationErrors) Empty

func (v *ValidationErrors) Empty() bool

Empty reports whether any error was collected.

func (*ValidationErrors) Error

func (v *ValidationErrors) Error() string

Error joins the individual messages with ", " in declaration order.

type Version

type Version struct {
	Name     string // the version string, e.g. "v1"
	Strategy VersionStrategy
	Vendor   string // vendor for VersionHeader (application/vnd.<vendor>-<name>+<fmt>)
	Param    string // parameter name for VersionParam (default "apiver")
}

Version describes an API's versioning configuration.

func (*Version) Matches

func (v *Version) Matches(path, accept, param string) bool

Matches reports whether a request carries this version. path is the raw request path (only consulted for VersionPath), accept is the Accept header, and param is the version parameter value (only consulted for VersionParam / when set). For VersionPath the caller strips the matched prefix separately via Prefix.

func (*Version) ParamName

func (v *Version) ParamName() string

ParamName returns the effective parameter name for VersionParam.

func (*Version) Prefix

func (v *Version) Prefix() string

Prefix returns the path prefix a route acquires under this version, i.e. the version segment for VersionPath and the empty string otherwise (the version then lives in a header or param, not the path).

type VersionStrategy

type VersionStrategy int

VersionStrategy names how Grape locates the requested API version.

const (
	// VersionPath: the version is the first path segment ("/v1/…").
	VersionPath VersionStrategy = iota
	// VersionHeader: the version is read from an Accept header vendor tree,
	// e.g. "Accept: application/vnd.vendor-v1+json".
	VersionHeader
	// VersionParam: the version is a query/body parameter (default name "apiver").
	VersionParam
	// VersionAccept: the version is the whole Accept media-type parameter
	// ("Accept: application/json; version=v1"), less common than VersionHeader.
	VersionAccept
)

Jump to

Keyboard shortcuts

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