dryvalidation

package module
v0.0.0-...-e2f79ba 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-dry-validation/dry-validation

dry-validation — go-ruby-dry-validation

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's dry-schema and dry-validation gems — the schema / contract layer that coerces and validates a hash of input against key-presence rules, value macros, dry-types coercion and dry-logic predicate constraints, then reports a nested errors hash whose messages are byte-identical to the gems' default English locale ("is missing", "must be filled", "must be greater than 18", "must be a hash", …) — without any Ruby runtime.

It is built directly on top of go-ruby-dry-types: every schema key's coercion + constraint is a dry-types Type, and the schema composes those into a whole-hash validator. It is the validation backend for go-embedded-ruby but is a standalone, reusable module.

What it is — and isn't. The schema definition, coercion, macro/predicate evaluation, and error-message localization are fully deterministic and need no interpreter, so they live here as pure Go. The custom-rule predicate bodies of a dry-validation Contract are the host's job: rbgo runs the Ruby block, and this library exposes the coerced values and a RuleContext whose Key/Base failures add errors — the schema + built-in macro/predicate evaluation is the deterministic core.

Features

Faithful port of dry-schema + dry-validation, validated against the dry-schema / dry-validation gems on every supported platform:

  • Key presencerequired(:k) (absent ⇒ "is missing") and optional(:k) (absent ⇒ dropped, no error).
  • Value macrosfilled (present + non-empty), maybe (nil passes through), value (coerce + predicates), array(:elem, …) (per-element coerce
    • validate, errors keyed by index), hash do … end / schema do … end (nested schema), and array(:hash) do … end (array of nested schemas).
  • Coercion via dry-types — the Params namespace form-coerces strings to integer/float/bool/symbol/date/time; the JSON namespace uses JSON-native types. :string is strict in Params (a non-string reports must be a string, matching the gem).
  • dry-logic predicatesgt? gteq? lt? lteq? format? size? min_size? max_size? included_in? excluded_from? eql? not_eql? filled? empty? odd? even? true? false?, each with its exact en-locale message.
  • Nested errors hash — a *Map whose leaves are []any of message strings, hash keys are Symbol, and array-element keys are int, plus a flat []Message with full key paths (result.errors.each).
  • ContractsContract wraps a schema and a list of rules that run after the schema against the coerced values; a rule fires only when all its dependency keys passed the schema (a base rule always runs), matching the gem.

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

Install

go get github.com/go-ruby-dry-validation/dry-validation

Usage

package main

import (
	"fmt"

	dv "github.com/go-ruby-dry-validation/dry-validation"
)

func main() {
	// Dry::Schema.Params { required(:email).filled(:string)
	//                      optional(:age).maybe(:integer)
	//                      required(:address).hash { required(:city).filled(:string) } }
	schema := dv.Params(func(b *dv.Builder) {
		b.Required("email").Filled("string")
		b.Optional("age").Maybe("integer")
		b.Required("address").Hash(func(b *dv.Builder) {
			b.Required("city").Filled("string")
		})
	})

	r := schema.Call(map[string]any{
		"email":   "a@b.com",
		"age":     "30", // Params coerces "30" → 30
		"address": map[string]any{},
	})

	fmt.Println(r.Success())               // false
	fmt.Println(r.ToH())                   // {email: "a@b.com", age: 30, address: {}}
	fmt.Println(r.Errors())                // {address: {city: ["is missing"]}}

	// A contract adds custom rules over the coerced values.
	c := dv.NewContract(schema)
	c.Rule(func(rc *dv.RuleContext) {
		if v, ok := rc.Value("age"); ok {
			if age, ok := v.(int64); ok && age < 18 {
				rc.KeyFailure("must be at least 18")
			}
		}
	}, "age")
}

Error messages & paths

Result.Errors() renders the nested hash exactly as the gems' errors.to_h:

Input errors.to_h
required(:email) absent {email: ["is missing"]}
filled(:string) on "" {email: ["must be filled"]}
filled(:integer, gt?: 18) on 10 {age: ["must be greater than 18"]}
array(:string) on ["a", 1] {tags: {1 => ["must be a string"]}}
hash do required(:city).filled end missing key {address: {city: ["is missing"]}}
base rule failure {nil => ["…"]}

API

func Params(build func(*Builder)) *Schema // Dry::Schema.Params
func JSON(build func(*Builder)) *Schema   // Dry::Schema.JSON

func (*Builder) Required(name Symbol) *Key
func (*Builder) Optional(name Symbol) *Key

func (*Key) Filled(typeName string, preds ...Predicate)
func (*Key) Maybe(typeName string, preds ...Predicate)
func (*Key) Value(typeName string, preds ...Predicate)
func (*Key) Array(elemType string, elemPreds ...Predicate)
func (*Key) ArrayOf(build func(*Builder)) // array(:hash) do … end
func (*Key) Hash(build func(*Builder))    // hash do … end
func (*Key) Schema(build func(*Builder))  // schema do … end

func (*Schema) Call(input any) *Result

type Predicate struct { Name string; Arg any } // gt / format / size / …

func NewContract(schema *Schema) *Contract
func (*Contract) Rule(body func(*RuleContext), keys ...Symbol)
func (*Contract) RuleBase(body func(*RuleContext))
func (*Contract) Call(input any) *Result

type RuleContext struct{ /* … */ }
func (*RuleContext) Values() *Map
func (*RuleContext) Value(key Symbol) (any, bool)
func (*RuleContext) KeyFailure(text string)
func (*RuleContext) KeyFailureAt(path []any, text string)
func (*RuleContext) BaseFailure(text string)

type Result struct{ /* … */ }
func (*Result) ToH() *Map      // coerced output
func (*Result) Errors() *Map   // nested errors hash
func (*Result) Success() bool
func (*Result) Messages() []Message // flat, with paths

Ruby value model

Inputs and coerced outputs use the same small, fixed set of Go types the go-ruby-* ecosystem shares (re-exported from go-ruby-dry-types), so a host maps its object graph with no glue: nil, bool, int64/*big.Int, float64, string, Symbol, []any, *Map (ordered Hash), Date, time.Time.

Tests & coverage

The suite pairs deterministic, ruby-free golden tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential oracle: schemas and contracts are run against valid + invalid inputs here and by the system ruby with the dry-schema / dry-validation gems (gated on RUBY_VERSION >= "4.0"), comparing the coerced output and the full errors hash across filled / maybe / array / nested / predicate cases in both the Params and JSON namespaces. The oracle skips itself where ruby or the gems are absent.

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

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-dry-validation/dry-validation 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 dryvalidation is a pure-Go (CGO-free) MRI-faithful reimplementation of Ruby's dry-schema and dry-validation gems: a schema/contract DSL that coerces and validates a hash of input against key-presence rules, value macros (filled/maybe/value/each/array/hash/schema), dry-types coercion and dry-logic predicate constraints, then reports a nested errors hash whose messages are byte-identical to the gems' default English (en) locale.

The type layer under every schema key is the sibling package github.com/go-ruby-dry-types/dry-types: a schema key's coercion+constraint is a dry-types drytypes.Type, and the schema composes those into a whole-hash validator.

Ruby value model

Inputs and coerced outputs use the same small, fixed set of Go types the go-ruby-* ecosystem shares (see github.com/go-ruby-dry-types/dry-types), so a host (go-embedded-ruby / rbgo) maps its object graph with no glue:

Ruby            Go
----            --
nil             nil
true / false    bool
Integer         int64, *big.Int
Float           float64
String          string
Symbol          Symbol
Array           []any
Hash            *Map (ordered)
Date            Date
Time / DateTime Time

Rule seam

The custom-`rule` predicate bodies of a dry-validation Contract are the host / rbgo evaluation seam: rbgo runs the Ruby block, and this package exposes the coerced values, a schema-success view, and a RuleContext whose Key/Base .failure methods add errors. The schema and its built-in macro/predicate evaluation are the deterministic core here.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Builder

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

Builder is the DSL receiver inside a Params/JSON block: it collects key rules via Builder.Required and Builder.Optional.

func (*Builder) Optional

func (b *Builder) Optional(name Symbol) *Key

Optional declares an optional key. An absent optional key is skipped entirely (no error, no output entry); a present one is coerced+validated like a required one.

func (*Builder) Required

func (b *Builder) Required(name Symbol) *Key

Required declares a required key. The returned Key attaches the value macro; a required key absent from the input reports "is missing".

type Contract

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

Contract is a dry-validation contract: a Schema (built with `params`/`json`) plus a list of custom rules that run after the schema against the coerced values. Build one with NewContract, attach the schema, add rules with Contract.Rule/Contract.RuleBase, then apply it with Contract.Call.

The rule bodies are the host / rbgo evaluation seam: the schema and its macro/ predicate evaluation are deterministic Go here, while each rule's predicate logic is a Go closure (in the host, the compiled Ruby block) that inspects the coerced values through a RuleContext and calls Failure to add an error.

func NewContract

func NewContract(schema *Schema) *Contract

NewContract creates a contract wrapping the given schema (built with Params or JSON).

func (*Contract) Call

func (c *Contract) Call(input any) *Result

Call applies the contract: it runs the schema, then every rule whose dependency keys all passed the schema, collecting rule failures into the same errors tree. It returns the combined Result.

func (*Contract) Rule

func (c *Contract) Rule(body func(*RuleContext), keys ...Symbol)

Rule registers a rule keyed on one or more top-level keys. The body runs only if every named key passed the schema; a [RuleContext.Key] failure attaches to the last named key (matching the gem, where `rule(:a, :b)` reports under :a's error path is actually the first — see below). dry-validation reports a keyed `key.failure` under the rule's first key.

func (*Contract) RuleBase

func (c *Contract) RuleBase(body func(*RuleContext))

RuleBase registers a base rule (no dependency keys) whose failures attach to the base (nil) path — dry-validation's `rule do base.failure(...) end`. It runs unconditionally after the schema.

type Date

type Date = drytypes.Date

Date is a Ruby Date, re-exported from dry-types (the coercion target of :date-typed keys).

type Key

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

Key is the value-macro attachment point returned by Builder.Required / Builder.Optional. Call exactly one macro method on it (Filled, Maybe, Value, Array, Hash, Schema) to define how the key's value is coerced and validated.

func (*Key) Array

func (k *Key) Array(elemType string, elemPreds ...Predicate)

Array coerces the value to an array and coerces+validates each element as elemType with elemPreds (`array(:elem, pred?: arg)`). For an array of hashes, follow with [Key.Each] on the returned member builder; use Key.ArrayOf for the block form.

func (*Key) ArrayOf

func (k *Key) ArrayOf(build func(*Builder))

ArrayOf coerces the value to an array and validates each element against a nested member schema built by build — dry-schema's `array(:hash) do ... end`. The elements are coerced to hashes and each run through the member schema.

func (*Key) Filled

func (k *Key) Filled(typeName string, preds ...Predicate)

Filled requires the value to be present and non-empty, coerced to typeName (a dry-types type name like "string"/"integer", or "" for any), then satisfy the predicates. It is dry-schema's `filled(:type, pred?: arg)`.

func (*Key) Hash

func (k *Key) Hash(build func(*Builder))

Hash coerces the value to a hash and applies a nested schema built by build — dry-schema's `hash do ... end`. A non-hash value reports "must be a hash".

func (*Key) Maybe

func (k *Key) Maybe(typeName string, preds ...Predicate)

Maybe permits nil (nil passes through unchanged); a non-nil value is coerced to typeName and checked against the predicates. It is `maybe(:type, ...)`.

func (*Key) Schema

func (k *Key) Schema(build func(*Builder))

Schema is an alias of Key.Hash (`schema do ... end`): a nested schema over the value.

func (*Key) Value

func (k *Key) Value(typeName string, preds ...Predicate)

Value coerces the value to typeName and checks the predicates without the implicit filled? of Key.Filled. It is `value(:type, pred?: arg)`.

type Map

type Map = drytypes.Map

Map is the insertion-ordered Ruby Hash re-exported from dry-types; schema coercion yields a *Map so key order round-trips.

func NewMap

func NewMap() *Map

NewMap returns an empty ordered Map.

type Message

type Message struct {
	// Text is the failure message, byte-identical to the gem's en locale
	// (e.g. "is missing", "must be filled", "must be greater than 18").
	Text string
	// Path is the key/index path to the value (Symbol or int elements). A base
	// error carries the single-element path []any{nil}.
	Path []any
}

Message is one validation failure: the message text and the path of keys from the root of the input to the offending value. A path element is a Symbol for a hash key or an int for an array index; the empty path (a base error) is reported by the gem under the nil key.

type Pair

type Pair = drytypes.Pair

Pair is one entry of an ordered Map.

type Predicate

type Predicate struct {
	Name string
	Arg  any
}

Predicate is one dry-logic value constraint attached to a schema key (`gt?: 18`, `format?: /.../`, `size?: 3`, …). Name is the predicate without the trailing `?`; Arg is its argument (nil for the arity-1 predicates).

type Result

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

Result is the outcome of applying a Schema or a Contract: the coerced output hash plus the errors tree. It mirrors Dry::Schema::Result / Dry::Validation::Result (`.to_h`, `.errors`, `.success?`).

func (*Result) Errors

func (r *Result) Errors() *Map

Errors returns the nested errors hash the way Dry::Schema's `result.errors.to_h` renders it: a *Map whose leaves are []any of message strings, hash keys are Symbol, and array-element keys are int. An empty result yields an empty *Map.

func (*Result) Messages

func (r *Result) Messages() []Message

Messages returns the flat list of every failure with its full key path, in the order dry-validation's `result.errors.each` yields them.

func (*Result) Output

func (r *Result) Output() *Map

Output is an alias of Result.ToH.

func (*Result) Success

func (r *Result) Success() bool

Success reports whether validation produced no errors (Result#success?).

func (*Result) ToH

func (r *Result) ToH() *Map

ToH returns the coerced output hash (dry-schema's Result#to_h). Keys are the schema keys that were present in the input, in schema-declaration order, with their coerced values.

type RuleContext

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

RuleContext is the seam a rule body operates through. It exposes the coerced values and records failures. In the host, rbgo runs the Ruby block and drives this context: reading Values and calling Key/Base .failure.

func (*RuleContext) BaseFailure

func (rc *RuleContext) BaseFailure(text string)

BaseFailure adds a base failure under the nil path (dry-validation's `base.failure(text)`).

func (*RuleContext) KeyFailure

func (rc *RuleContext) KeyFailure(text string)

KeyFailure adds a failure at the rule's default key path (dry-validation's `key.failure(text)`). For a base rule (no key) it falls back to the base path.

func (*RuleContext) KeyFailureAt

func (rc *RuleContext) KeyFailureAt(path []any, text string)

KeyFailureAt adds a failure at an explicit key path (dry-validation's `key([:a, :b]).failure(text)`), so a rule can report under a nested or different key than its dependency.

func (*RuleContext) Value

func (rc *RuleContext) Value(key Symbol) (any, bool)

Value returns the coerced value at a top-level key and whether it was present.

func (*RuleContext) Values

func (rc *RuleContext) Values() *Map

Values returns the coerced output hash the rule inspects (dry-validation's `values`). Reading a key: use RuleContext.Value.

type Schema

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

Schema is a dry-schema definition: an ordered list of key rules that, applied to an input hash, coerces the values and produces a Result with the coerced output and a nested errors hash. Build one with Params or JSON and the Key DSL, then apply it with Schema.Call.

func JSON

func JSON(build func(*Builder)) *Schema

JSON starts a JSON-namespace schema (JSON-native coercion — `json do`).

func Params

func Params(build func(*Builder)) *Schema

Params starts a Params-namespace schema (form-string coercion — the default dry-validation `params do` block). Call build to add keys, then finish.

func (*Schema) Call

func (s *Schema) Call(input any) *Result

Call applies the schema to input (any hash-shaped value: *Map, map[string]any, map[Symbol]any). It returns a Result carrying the coerced output *Map and the errors tree. A non-hash input yields an empty output and no per-key errors — the caller (a nested hash macro) reports the "must be a hash" type error.

type Symbol

type Symbol = drytypes.Symbol

Symbol is a Ruby Symbol (`:name`), re-exported from dry-types so keys and values share one representation across the stack.

Jump to

Keyboard shortcuts

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