ransack

package module
v0.0.0-...-06ca1d7 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: 6 Imported by: 0

README

go-ruby-ransack/ransack

ransack — go-ruby-ransack

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the query-building core of Ruby's ransack gem — the search/filter object builder for ActiveRecord. It parses a q params hash into a structured search AST (attributes + predicates + values, sorts, groups) and evaluates it — without any Ruby runtime.

It is the search builder for go-embedded-ruby, but a standalone, reusable module. Everything ransack does before it hits the database is deterministic and needs no interpreter, so it lives here as pure Go: stripping the predicate suffix off a key, splitting the _or_ / _and_ attribute grammar, resolving association prefixes, validating attributes against an allowlist, and building the condition tree and sort list.

What it is — and isn't. Turning a search into a result set is a host seam: a Backend performs the query. The rbgo/ActiveRecord binding implements Backend by translating the condition tree into SQL WHERE/JOIN/ORDER BY; the built-in MemoryBackend (and Search.Match / Search.Apply) evaluate a search over in-memory records so the engine is testable and usable without an ORM.

Features

Faithful port of the ransack query builder:

  • Search parsingNew(ctx, q) parses a q params hash into a Search: a tree of conditions (Root), a list of Sorts, a Distinct flag and any parse Errors.
  • Predicate suffixes — the full predicate set: eq/not_eq, lt/lteq/gt/gteq, cont/not_cont/i_cont/not_i_cont, start/not_start/end/not_end, matches/does_not_match (SQL LIKE), in/not_in, the boolean-flag predicates true/not_true/false/ not_false/null/not_null/present/blank, and the _any/_all compounds of every scalar predicate. The longest matching suffix wins.
  • Attribute combinators — the name_or_description_cont / first_and_last_eq grammar joins several attributes under one predicate.
  • Grouped conditionsm: 'or' / m: 'and' combinators plus g[] nested groups, each with its own combinator.
  • Associationsassoc_attr_pred keys (e.g. articles_title_cont, articles_comments_body_eq) resolve association prefixes against the context; the in-memory evaluator fans out over has-many collections (EXISTS semantics).
  • Sortss / sorts ("name asc", ["age desc", …]) → []Sort of {attr, dir}.
  • Allowlist seamContext mirrors ransackable_attributes / ransackable_associations: attributes and associations that are not whitelisted are rejected (recorded in Search.Errors). A nil context is "open".
  • Blank-value skip — conditions whose value is blank are dropped, exactly as ransack does; boolean-flag predicates keep their false direction.
  • Application seamBackend (Result(*Search)); the built-in MemoryBackend filters, orders and de-duplicates in memory.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Install

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

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-ransack/ransack"
)

func main() {
	rows := []map[string]any{
		{"name": "Alice", "age": 30, "role": "admin"},
		{"name": "Bob", "age": 18, "role": "editor"},
		{"name": "Carol", "age": 42, "role": "admin"},
	}

	// Model.ransack(q).result — parse a q params hash, then evaluate it.
	q := map[string]any{
		"name_cont": "a",     // name LIKE '%a%'
		"age_gteq":  20,       // age >= 20
		"role_in":   []any{"admin"},
		"s":         "age desc",
	}

	search := ransack.New(nil, q) // nil context = open (allow every attribute)
	result, _ := search.Result(&ransack.MemoryBackend{Records: rows})
	for _, r := range result.([]map[string]any) {
		fmt.Println(r["name"]) // Carol
	}
}
Allowlist and associations
ctx := ransack.NewContext("name", "age").
	Associate("articles", ransack.NewContext("title"))

// articles_title_cont resolves the association; age_lteq is allowlisted;
// a disallowed key (e.g. password_eq) is recorded in search.Errors.
search := ransack.New(ctx, map[string]any{
	"articles_title_cont": "Ruby",
	"age_lteq":            65,
})
Grouped conditions
// (age >= 40) OR (role == "editor")
search := ransack.New(nil, map[string]any{
	"m": "or",
	"g": []any{
		map[string]any{"age_gteq": 40},
		map[string]any{"role_eq": "editor"},
	},
})

Value model

gem this package
Model.ransack(q) ransack.New(ctx, q)*Search
search.result search.Result(backend) / search.Apply(records)
q[:name_cont] = "x" Condition{Attributes, Predicate, Values}
q[:name_or_desc_cont] Condition.Combinator over Attributes
q[:g][0][:m] = 'or' grouped conditions Group{Combinator, Conditions, Groups}
q[:s] = "name asc" / q[:sorts] []Sort{Attr, Dir}
ransackable_attributes / associations Context{Attributes, Associations} (allowlist)
the ActiveRecord relation Backend (host seam; MemoryBackend built in)

Tests & coverage

The suite is pure and self-contained: every predicate class, the _or_/_and_ attribute grammar, and/or grouping, g[] nested groups, asc/desc sorts, association attributes, in/multi-value predicates, unknown-predicate and allowlist-reject paths, and blank-value skipping are all exercised, holding coverage at 100% on every arch/OS lane.

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

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, …)

License

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

Documentation

Overview

Package ransack is a pure-Go (CGO=0), MRI-faithful reimplementation of the Ruby "ransack" gem: a search/filter query builder.

A Ransack search is expressed as a "q" params hash whose keys encode an attribute and a predicate, for example:

q := map[string]any{
	"name_cont":  "foo",   // name LIKE '%foo%'
	"age_gteq":   18,       // age >= 18
	"role_in":    []any{"admin", "editor"},
	"s":          "name asc",
}
search := ransack.New(ctx, q)
results, _ := search.Result(&ransack.MemoryBackend{Records: rows})

New parses the hash into a Search: a tree of conditions (Search.Root), a list of Sorts and a Distinct flag. Parsing strips the predicate suffix using the longest matching predicate, splits multi-attribute keys on the _or_ / _and_ combinators, resolves association prefixes and validates every attribute against the Context allowlist (ActiveRecord's ransackable_attributes seam).

Predicates include eq/not_eq, lt/lteq/gt/gteq, cont/not_cont/i_cont, start/ end, matches/does_not_match, in/not_in, the boolean-flag predicates true/false/null/not_null/present/blank and the _any/_all compounds of every scalar predicate.

Application is a seam. A Backend turns a Search into results; the rbgo/ ActiveRecord binding implements it as SQL, while the built-in MemoryBackend (and Search.Match / Search.Apply) evaluate the search in memory so the engine is testable and usable without an ORM.

The intended Ruby surface is Model.ransack(q).result, with predicate suffixes, s/sorts ordering, distinct and g[] grouped conditions all supported.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Attr

type Attr struct {
	Path []string
	Name string
}

Attr is a searchable attribute reference. Path holds the association segments (empty for a direct attribute) and Name is the final column.

type Backend

type Backend interface {
	// Result evaluates the search and returns its result set.
	Result(s *Search) (any, error)
}

Backend is the application seam. A Search describes what to fetch; a Backend turns that description into results. The rbgo/ActiveRecord binding implements Backend by translating the condition tree into SQL WHERE/JOIN clauses and the sorts into ORDER BY. MemoryBackend is the built-in, ORM-free implementation.

type Condition

type Condition struct {
	// Key is the original params key that produced this condition.
	Key string
	// Attributes are the attributes tested by this condition.
	Attributes []Attr
	// Combinator joins multiple attributes ("or"/"and"); empty when there is
	// a single attribute.
	Combinator string
	// Predicate is the test applied to each attribute.
	Predicate *Predicate
	// Values are the (blank-filtered) condition values.
	Values []any
}

Condition is a parsed Ransack condition: one or more attributes tested with a single predicate against one or more values. When more than one attribute is present (the a_or_b / a_and_b grammar) Combinator ("or"/"and") joins them.

func (*Condition) Match

func (c *Condition) Match(rec map[string]any) bool

Match evaluates the condition against a record.

type Context

type Context struct {
	// Attributes is the set of searchable/sortable columns. Nil means open.
	Attributes []string
	// Associations maps an association name to its own Context.
	Associations map[string]*Context
}

Context is the allowlist seam. It mirrors ActiveRecord's ransackable_attributes / ransackable_associations: only whitelisted attributes and associations may be searched or sorted. This keeps a search engine safe when it is driven by untrusted params.

A Context with a nil Attributes slice is "open" and allows every attribute name. Associations is a map from association name to the Context describing that associated model.

func NewContext

func NewContext(attrs ...string) *Context

NewContext returns a Context allowing the given attributes. With no arguments it returns an open Context that allows every attribute.

func (*Context) Associate

func (c *Context) Associate(name string, sub *Context) *Context

Associate registers an association and returns the receiver for chaining.

type Group

type Group struct {
	Combinator string
	Conditions []*Condition
	Groups     []*Group
}

Group is a tree node combining conditions and nested groups with a boolean combinator ("and"/"or").

func (*Group) Match

func (g *Group) Match(rec map[string]any) bool

Match evaluates the group against a record. An empty group matches every record.

type MemoryBackend

type MemoryBackend struct {
	Records []map[string]any
}

MemoryBackend evaluates a Search over an in-memory slice of records using the built-in evaluator. Each record is a column-name -> value map; associations are nested maps (belongs-to) or slices of maps (has-many).

func (*MemoryBackend) Result

func (b *MemoryBackend) Result(s *Search) (any, error)

Result filters, sorts and de-duplicates the backing records per the search.

type Predicate

type Predicate struct {
	// Name is the predicate suffix, e.g. "cont", "not_eq" or "gteq_any".
	Name string
	// WantsArray reports whether the predicate consumes a list of values
	// (in/not_in and the _any/_all compounds).
	WantsArray bool
	// contains filtered or unexported fields
}

Predicate describes a Ransack search predicate, such as eq, cont or gteq. The exported fields let a Backend translate a condition to its own query language; Match evaluates the predicate in memory.

func (*Predicate) Match

func (p *Predicate) Match(field any, values []any) bool

Match evaluates the predicate against a single field value and its condition values. For array predicates every value participates; for boolean predicates only the first value (a flag) is consulted.

type Search struct {
	Root     *Group
	Sorts    []Sort
	Distinct bool
	Errors   []string
}

Search is the parsed form of a Ransack "q" params hash: a tree of conditions (Root), an ordered list of sorts, a distinct flag and any parse errors. It is the value the Ruby-facing Model.ransack(q) returns; .result is obtained by handing the Search to a Backend.

func New

func New(ctx *Context, params map[string]any) *Search

New parses a params hash against ctx. A nil ctx is treated as an open context (every attribute allowed). Recognised structural keys are "m" (combinator), "g" (nested groups), "s"/"sorts" (ordering) and "distinct"/"d".

func (*Search) Apply

func (s *Search) Apply(records []map[string]any) []map[string]any

Apply is the built-in, ORM-free evaluator: it filters records by the condition tree, orders them by the sorts and, when Distinct is set, removes duplicate rows.

func (*Search) Match

func (s *Search) Match(rec map[string]any) bool

Match reports whether a single record satisfies the search's condition tree.

func (*Search) Result

func (s *Search) Result(b Backend) (any, error)

Result hands the search to a Backend and returns whatever the backend produces. This is the seam the rbgo/ActiveRecord binding implements to build a real SQL relation; the in-memory MemoryBackend is the ORM-free default.

type Sort

type Sort struct {
	// Attr is the resolved attribute to order by.
	Attr Attr
	// Name is the original column name from the params.
	Name string
	// Dir is "asc" or "desc".
	Dir string
}

Sort is a single ordering directive.

Jump to

Keyboard shortcuts

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