activemodel

package module
v0.0.0-...-4ac8fbc 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: 8 Imported by: 0

README

go-ruby-activemodel/activemodel

activemodel — go-ruby-activemodel

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby on Rails' ActiveModel — the model-behaviour toolkit that gives a plain object validations, an errors collection, and conventional naming. This v0.1 foundation mirrors the observable behaviour of the activemodel gem on MRI 4.0.5, without any Ruby runtime.

It is the ActiveModel backend for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-activesupport (whose Inflector it reuses for naming) and go-ruby-set.

v0.1 scope — the Validations + Errors + Naming core

Subsystem Rails class What ships
Naming ActiveModel::Naming / Name singular / plural / element / human / collection / param_key / route_key / singular_route_key / i18n_key, namespaced and uncountable forms — byte-for-byte with Rails via the ActiveSupport Inflector.
Errors ActiveModel::Errors / Error Add, [] (Get), Where, Added, OfKind, Include, MessagesFor, FullMessagesFor, FullMessages, Messages, Details, Clear, Size; %{...} interpolation and the full default-message table; byte-faithful full messages ("Name can't be blank").
Validations ActiveModel::Validations the standard validators (presence, absence, length, format, inclusion, exclusion, numericality, confirmation, acceptance), custom Validate blocks, ValidatesEach, conditional If/Unless/On, and AllowNil/AllowBlank — with the exact Rails error types and messages.
Validator base ActiveModel::Validator / EachValidator the Validator / EachValidator interfaces plus ValidatesWith / ValidatesEachWith for custom validators.

The seams — reaching a model without open classes

Go has no open classes, so the object under validation is reached through two small interfaces (combined as Model), which a host such as go-embedded-ruby plugs its own object behind:

// Attribute get/set — Ruby attr readers/writers and read_attribute_for_validation.
type Attr interface {
	Get(name string) any
	Set(name string, val any)
}

// Method-call seam — a symbol if:/unless: condition, and the respond_to? guard.
type Dispatcher interface {
	Call(method string) any
	RespondTo(method string) bool
}

type Model interface { Attr; Dispatcher }

Everything the validators need is expressed through these: presence reads the attribute (Get); confirmation reads #{attr}_confirmation (Get); a symbol if: condition calls a method (Call); the %{value} interpolation honours the respond_to? guard (RespondTo). Custom validate bodies and proc conditions are plain Go funcs — the seam for the Ruby block.

Install

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

Usage

package main

import (
	"fmt"
	"regexp"

	am "github.com/go-ruby-activemodel/activemodel"
)

func main() {
	v := am.New(am.NewName("Person"))
	v.Validates([]string{"name"}, am.Options{Presence: true})
	v.Validates([]string{"email"}, am.Options{
		Format:     &am.FormatOptions{With: regexp.MustCompile(`@`)},
		AllowBlank: true,
	})
	v.Validates([]string{"age"}, am.Options{
		Numericality: &am.NumericalityOptions{OnlyInteger: true, GreaterThan: 0},
	})

	person := /* your Model seam */ nil
	if ok, errs := v.Valid(person); !ok {
		for _, m := range errs.FullMessages() {
			fmt.Println(m) // e.g. "Name can't be blank"
		}
	}

	n := am.NewName("Admin::User")
	fmt.Println(n.Plural, n.RouteKey, n.Human) // admin_users admin_users User
}

Fidelity — the MRI oracle

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 ActiveModel oracle: model_name fields and full_messages across the whole validator matrix are computed here and diffed against the real activemodel gem running on the system ruby. CI installs the gem on the ubuntu/macos lanes; the oracle skips itself where ruby or the gem is absent.

Roadmap — the deferred ActiveModel subsystems

v0.1 is the model-description core. Still to come, in rough priority order:

  • Attributes — the typecasting system (attribute :born_on, :date), with the type registry and default/coercion behaviour.
  • Dirty — change tracking (changed?, *_was, changes, saved_changes).
  • Callbacksbefore_validation / after_validation (and the general ActiveModel::Callbacks define/run machinery) — surfaced as an ordering seam around Valid.
  • Serializationserializable_hash, to_json / from_json, to_xml.
  • SecurePasswordhas_secure_password, later reusing go-ruby-bcrypt.
  • Translation / I18n — full errors.messages / activemodel.models lookup chains, locale fallbacks and pluralization backends (v0.1 ships the English default table and count-based length pluralization).

Tests & coverage

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) and three OSes (Linux, macOS, Windows). The one dependency is the pure-Go go-ruby-activesupport (Inflector).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-activemodel/activemodel 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 activemodel is a pure-Go (no cgo), MRI-faithful reimplementation of Ruby on Rails' ActiveModel, targeting the observable behaviour of the activemodel gem on MRI 4.0.5.

This v0.1 foundation ships the Validations + Errors + Naming core — the part of ActiveModel that a model object leans on to describe itself and to report why it is invalid:

  • Naming (Name) — ActiveModel::Naming / ActiveModel::Name: derive the singular/plural/element/human/collection/param_key/route_key/i18n_key of a model class name via go-ruby-activesupport's Inflector, byte-for-byte with Rails.
  • Errors, Error — ActiveModel::Errors / ActiveModel::Error: add, lookup, where/added?/include?, messages/details, and byte-faithful full_messages ("Name can't be blank") with %{...} interpolation and the complete default-message table.
  • Validations — the ActiveModel::Validations engine: the standard validators (presence, absence, length, format, inclusion, exclusion, numericality, confirmation, acceptance), custom validate blocks, validates_each, conditional if/unless/on, and allow_nil/allow_blank, with the exact Rails error types and messages.
  • Validator, EachValidator — the base contracts for writing custom validators, mirroring ActiveModel::Validator / EachValidator.

Because Go has no open classes, the model object is reached through two small seams — Attr (attribute get/set) and Dispatcher (call a method / test respond to) — combined in the Model interface. A host such as go-embedded-ruby plugs its own object behind them; the tests plug a trivial map-backed fake.

See the README for the roadmap covering the deferred subsystems (Attributes typecasting, Dirty tracking, Callbacks, Serialization, SecurePassword, and Translation/i18n integration).

Index

Constants

View Source
const Version = "0.1.0"

Version is the module version.

Variables

This section is empty.

Functions

This section is empty.

Types

type AcceptanceOptions

type AcceptanceOptions struct {
	Accept  []any
	Message any
}

AcceptanceOptions configures the acceptance validator. Accept defaults to [true, "1"]; acceptance skips nil values (allow_nil defaults true in Rails).

type Attr

type Attr interface {
	Get(name string) any
	Set(name string, val any)
}

Attr is the attribute-access seam: read and write a model attribute by name. It mirrors the Ruby attr reader/writer pair a validated object exposes, and in particular ActiveModel's read_attribute_for_validation. A host such as go-embedded-ruby plugs its own object here; the tests plug a map-backed fake.

type Condition

type Condition struct {
	Method string
	Proc   func(Model) any
}

Condition is a single if:/unless: predicate. Method names a model method resolved through the Dispatcher seam (a Ruby symbol condition); otherwise Proc is called (a Ruby proc/lambda). Either way the result is judged by Ruby truthiness.

func MethodCond

func MethodCond(method string) Condition

MethodCond builds a symbol condition naming a model method.

func ProcCond

func ProcCond(fn func(Model) any) Condition

ProcCond builds a proc condition from a Go predicate.

type Conditions

type Conditions struct {
	If         []Condition
	Unless     []Condition
	On         []string
	AllowNil   bool
	AllowBlank bool
}

Conditions is the shared control block for a validator: if:/unless:/on: plus allow_nil:/allow_blank:.

type ConfirmationOptions

type ConfirmationOptions struct {
	CaseSensitive *bool
	Message       any
}

ConfirmationOptions configures the confirmation validator. CaseSensitive defaults to true.

type Dispatcher

type Dispatcher interface {
	Call(method string) any
	RespondTo(method string) bool
}

Dispatcher is the method-call seam for behaviour that is not plain attribute access: evaluating a symbol condition (an if:/unless: given as a method name), or any host method a validation refers to by name. Call invokes the method and returns its Ruby result; RespondTo reports whether the model responds to it (used exactly where ActiveModel guards on respond_to?).

type EachValidator

type EachValidator interface {
	ValidateEach(m Model, e *Errors, attribute string, value any)
}

EachValidator is ActiveModel::EachValidator: a custom validator invoked once per attribute with its value (after allow_nil/allow_blank filtering).

type Error

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

Error is ActiveModel::Error: a single validation error bound to an attribute, with a (Symbol or literal-String) type and its options.

func (*Error) Attribute

func (e *Error) Attribute() string

Attribute returns the attribute the error is on.

func (*Error) Details

func (e *Error) Details() map[string]any

Details is ActiveModel::Error#details: {error: type, ...options} minus the callback and message options.

func (*Error) FullMessage

func (e *Error) FullMessage() string

FullMessage is ActiveModel::Error#full_message: "%{attribute} %{message}", except a :base error is its message alone.

func (*Error) Match

func (e *Error) Match(attribute string, typ any, opts map[string]any) bool

Match is ActiveModel::Error#match?: same attribute, optionally the same type, and every supplied option equal.

func (*Error) Message

func (e *Error) Message() string

Message is ActiveModel::Error#message: a literal String type verbatim, or the generated, interpolated message for a Symbol type.

func (*Error) Options

func (e *Error) Options() map[string]any

Options returns the error's options.

func (*Error) Type

func (e *Error) Type() any

Type returns the raw error type (a Symbol or a literal String), defaulting to Symbol("invalid") when none was given.

type Errors

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

Errors is ActiveModel::Errors: the ordered collection of an object's errors.

func NewErrors

func NewErrors(base Model, name Name) *Errors

NewErrors builds an Errors bound to a model and its Name (used for the %{model} interpolation and full_message attribute humanization).

func (*Errors) Add

func (es *Errors) Add(attribute string, typ any, opts map[string]any) *Error

Add is ActiveModel::Errors#add: append an error on attribute with the given type and options and return it. A nil type defaults to Symbol("invalid").

func (*Errors) Added

func (es *Errors) Added(attribute string, typ any, opts map[string]any) bool

Added is ActiveModel::Errors#added?: for a Symbol type, whether an error strictly matches attribute/type/options; for a literal String type, whether that message is present on the attribute.

func (*Errors) Any

func (es *Errors) Any() bool

Any reports Errors#any?.

func (*Errors) AttributeNames

func (es *Errors) AttributeNames() []string

Attribute names present in the collection, in first-seen order (Errors#attribute_names).

func (*Errors) Clear

func (es *Errors) Clear()

Clear empties the collection (Errors#clear).

func (*Errors) Details

func (es *Errors) Details() map[string][]map[string]any

Details is Errors#details: attribute => its detail hashes.

func (*Errors) Each

func (es *Errors) Each(fn func(*Error))

Each iterates the errors in order (Errors#each).

func (*Errors) Empty

func (es *Errors) Empty() bool

Empty reports Errors#empty? / #blank?.

func (*Errors) Entries

func (es *Errors) Entries() []*Error

Entries returns the underlying ordered errors (Errors#errors).

func (*Errors) FullMessages

func (es *Errors) FullMessages() []string

FullMessages is Errors#full_messages: every error's full message, in order.

func (*Errors) FullMessagesFor

func (es *Errors) FullMessagesFor(attribute string) []string

FullMessagesFor is Errors#full_messages_for(attribute).

func (*Errors) Get

func (es *Errors) Get(attribute string) []string

Get is ActiveModel::Errors#[]: the message strings on the attribute (an alias for MessagesFor).

func (*Errors) Include

func (es *Errors) Include(attribute string) bool

Include reports whether any error is on attribute (Errors#include? / #has_key?).

func (*Errors) Messages

func (es *Errors) Messages() map[string][]string

Messages is Errors#messages: attribute => its message strings.

func (*Errors) MessagesFor

func (es *Errors) MessagesFor(attribute string) []string

MessagesFor is Errors#messages_for(attribute): the message strings on the attribute, in order.

func (*Errors) OfKind

func (es *Errors) OfKind(attribute string, typ any) bool

OfKind is ActiveModel::Errors#of_kind?: like Added but ignoring options for a Symbol type.

func (*Errors) Size

func (es *Errors) Size() int

Size is Errors#size / #count.

func (*Errors) Where

func (es *Errors) Where(attribute string, typ any, opts map[string]any) []*Error

Where is Errors#where: the errors matching attribute and, when given, type and options.

type FormatOptions

type FormatOptions struct {
	With    *regexp.Regexp
	Without *regexp.Regexp
	Message any
}

FormatOptions configures the format validator (validates format:). With must match; Without must not.

type LengthOptions

type LengthOptions struct {
	Minimum     *int
	Maximum     *int
	Is          *int
	In          *Range
	Message     any
	TooLong     any
	TooShort    any
	WrongLength any
}

LengthOptions configures the length validator (validates length:). Minimum, Maximum and Is are optional; In is the range form. The per-key message overrides (TooLong/TooShort/WrongLength) and Message mirror Rails.

type MembershipOptions

type MembershipOptions struct {
	In      []any
	Range   *NumRange
	Message any
}

MembershipOptions configures inclusion/exclusion (validates inclusion:/ exclusion:): either a discrete In set or a numeric Range.

type Model

type Model interface {
	Attr
	Dispatcher
}

Model is the object under validation: the two seams combined.

type Name

type Name struct {
	// Name is the class name as given, e.g. "Person" or "Admin::User".
	Name string
	// Singular is the underscored, "/"-flattened name: "person", "admin_user".
	Singular string
	// Plural pluralizes Singular: "people", "admin_users".
	Plural string
	// Element is the underscored, demodulized name: "person", "user".
	Element string
	// Human is the humanized Element: "Person", "User".
	Human string
	// Collection is the tableized name: "people", "admin/users".
	Collection string
	// ParamKey is the key for params: Singular (or the unnamespaced singular).
	ParamKey string
	// RouteKey is the plural route key ("people"), with "_index" appended for an
	// uncountable name.
	RouteKey string
	// SingularRouteKey singularizes RouteKey.
	SingularRouteKey string
	// I18nKey is the underscored name used as an i18n scope key.
	I18nKey string
}

Name is ActiveModel::Name: the bundle of conventional names Rails derives from a model class name (and an optional namespace module name). Every field is computed through go-ruby-activesupport's Inflector, byte-for-byte with Rails' ActiveModel::Name#initialize.

func NewName

func NewName(name string) Name

NewName builds the Name for a class name with no namespace, mirroring ActiveModel::Name.new(klass) where klass.name == name.

func NewNamespacedName

func NewNamespacedName(name, namespace string) Name

NewNamespacedName builds the Name for a class name nested in a namespace module, mirroring ActiveModel::Name.new(klass, namespace). namespace is the module's own name (e.g. "Admin"); an empty namespace means none.

type NumRange

type NumRange struct {
	Min, Max   float64
	ExcludeEnd bool
}

NumRange is a numeric range used by inclusion/exclusion in: a Ruby Range. When ExcludeEnd is set it models the "..." (end-exclusive) form.

type NumericalityOptions

type NumericalityOptions struct {
	OnlyInteger          bool
	GreaterThan          any
	GreaterThanOrEqualTo any
	EqualTo              any
	LessThan             any
	LessThanOrEqualTo    any
	OtherThan            any
	Odd                  bool
	Even                 bool
	Message              any
}

NumericalityOptions configures the numericality validator. Each comparison bound may be a literal number, a func(Model) any (a Ruby proc), or a Symbol (a method name resolved through the Dispatcher seam).

type Options

type Options struct {
	Presence     bool
	Absence      bool
	Length       *LengthOptions
	Format       *FormatOptions
	Inclusion    *MembershipOptions
	Exclusion    *MembershipOptions
	Numericality *NumericalityOptions
	Confirmation *ConfirmationOptions
	Acceptance   *AcceptanceOptions

	Message    any
	AllowNil   bool
	AllowBlank bool
	If         []Condition
	Unless     []Condition
	On         []string
}

Options is the bundle passed to Validates: which standard validators to apply to the attribute(s), plus the shared control keys. It mirrors the Ruby hash `validates :attr, presence: true, length: {...}, if: ..., allow_blank: true`.

type Range

type Range struct{ Min, Max int }

Range is an inclusive integer range used by length in:/within:.

type Symbol

type Symbol string

Symbol is a Ruby Symbol surfaced to Go. ActiveModel distinguishes a Symbol error type (a key looked up in the message table and interpolated) from a String error type (a literal message returned verbatim). Passing a Go string to Errors.Add / a :message option therefore means a *literal* Ruby String, while passing a Symbol("blank") means the Ruby Symbol :blank — exactly the MRI distinction in ActiveModel::Error#message.

type Validations

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

Validations is the ActiveModel::Validations engine for one model class: it holds the registered validators and the model's Name.

func New

func New(name Name) *Validations

New builds a Validations bound to a model Name (used for %{model} and the Errors it produces).

func (*Validations) Invalid

func (v *Validations) Invalid(m Model) (bool, *Errors)

Invalid runs invalidation in the default context.

func (*Validations) InvalidContext

func (v *Validations) InvalidContext(m Model, context string) (bool, *Errors)

InvalidContext is ActiveModel::Validations#invalid? in the given context.

func (*Validations) ModelName

func (v *Validations) ModelName() Name

ModelName returns the model's Name (ActiveModel::Naming#model_name).

func (*Validations) Valid

func (v *Validations) Valid(m Model) (bool, *Errors)

Valid runs validation in the default (empty) context.

func (*Validations) ValidContext

func (v *Validations) ValidContext(m Model, context string) (bool, *Errors)

ValidContext is ActiveModel::Validations#valid? in the given context: run every validator against the model, collecting errors, and report whether none were added.

func (*Validations) Validate

func (v *Validations) Validate(c Conditions, block func(m Model, e *Errors))

Validate is ActiveModel::Validations#validate with a block: register a whole-record custom validation.

func (*Validations) Validates

func (v *Validations) Validates(attrs []string, opts Options)

Validates is ActiveModel::Validations#validates: register the configured standard validators for the given attribute(s).

func (*Validations) ValidatesEach

func (v *Validations) ValidatesEach(attrs []string, c Conditions, block func(m Model, e *Errors, attribute string, value any))

ValidatesEach is ActiveModel::Validations#validates_each: run block once per attribute with its value, after allow_nil/allow_blank filtering.

func (*Validations) ValidatesEachWith

func (v *Validations) ValidatesEachWith(attrs []string, validator EachValidator, c Conditions)

ValidatesEachWith registers a custom EachValidator over the given attributes.

func (*Validations) ValidatesWith

func (v *Validations) ValidatesWith(validator Validator, c Conditions)

ValidatesWith registers a whole-record custom Validator (ActiveModel::Validations#validates_with).

type Validator

type Validator interface {
	Validate(m Model, e *Errors)
}

Validator is ActiveModel::Validator: a whole-record custom validator.

Jump to

Keyboard shortcuts

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