drystruct

package module
v0.0.0-...-86f70e7 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-dry-struct/dry-struct

dry-struct — go-ruby-dry-struct

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's dry-struct gem — typed, immutable value objects whose attributes are coerced and validated by go-ruby-dry-types. Declare a struct's attributes, construct it from a hash, and every attribute is coerced and checked through its dry-type; the first failure raises a Dry::Struct::Error whose message is byte-identical to the gem's ([User.new] :age is missing in Hash input). No Ruby runtime required.

It is the Dry::Struct layer for go-embedded-ruby, but is a standalone, reusable module — a sibling of go-ruby-dry-types (the type system it builds on).

What it is — and isn't. Coercing, validating, comparing, and inspecting a typed value object over dry-types attributes is fully deterministic and needs no interpreter, so it lives here as pure Go. Registering the resulting Ruby class, evaluating a default { ... } block, or running an arbitrary constructor is the host's job; this library hands back a small, explicit value model (*StructType, *Struct, drytypes.Symbol, …) the host maps to and from its own objects.

Features

Faithful port of Dry::Struct, validated against the dry-struct gem on every supported platform:

  • Typed attributesattribute :name, Types::String, attribute :age, Types::Coercible::Integer; every dry-types type composes (strict, coercible, params, enum, constrained, optional, default, …).
  • Optional attributesattribute? :port, …: the key may be absent; it reads back nil and is omitted from to_h.
  • Nested structs and arrays of structs — a *StructType is itself a drytypes.Type, so attribute :address, AddressStruct and Types::Array.of(MemberStruct) compose; to_h deep-converts.
  • DefaultsTypes::String.default("anon") fills in a missing attribute.
  • ConstructionNew/Call/MustNew; passing an instance of the same type through unchanged; strict schemas (schema schema.strict) reject unexpected keys.
  • Immutable value semantics — readers, ToH/ToHash (deep), Attributes, With (immutable copy with overrides), value equality (Eql), and a byte-faithful Inspect (#<User name="Alice" age=30>).
  • Struct configTransformKeys (symbolize/stringify), Strict, AsValue (Dry::Struct::Value), and Inherit (subclassing that adds/overrides attributes).

Usage

import (
	drystruct "github.com/go-ruby-dry-struct/dry-struct"
	drytypes "github.com/go-ruby-dry-types/dry-types"
)

user := drystruct.New("User").
	Attribute("name", drytypes.StrictString()).
	Attribute("age", drytypes.CoercibleInteger())

u, err := user.New(map[drytypes.Symbol]any{"name": "Alice", "age": "30"})
// u.Inspect() == `#<User name="Alice" age=30>`
// u.Fetch("age") == int64(30)

Tests & coverage

The suite is 100% ruby-free by default: deterministic golden vectors keep coverage at 100% with no interpreter. A differential oracle layer additionally compares construction results, to_h, error messages, inspect, and equality against the real dry-struct gem when ruby is present (skipped otherwise, and gated to RUBY_VERSION >= "4.0"). CI runs the 100%-coverage gate + a -race lane (host cgo) + the six 64-bit arches (CGO=0) across Linux, macOS and Windows.

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-dry-struct/dry-struct 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 drystruct is a pure-Go (CGO-free) MRI-faithful reimplementation of the Ruby dry-struct gem: typed, immutable value objects whose attributes are coerced and validated by github.com/go-ruby-dry-types/dry-types types.

A *StructType is the analogue of a `Dry::Struct` subclass: you register attributes on it (each a name plus a dry-types drytypes.Type), then construct instances from an attribute hash. Construction coerces and validates every attribute through its type, and — on the first failure — raises a *Error whose message is byte-identical to the dry-struct gem's (`[Name.new] <schema error>`).

Ruby value model

Attribute values use the same small Go value model the go-ruby-* ecosystem (and go-ruby-dry-types) uses, so a host (go-embedded-ruby / rbgo) maps its object graph to and from this package with no glue:

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

A *Struct is one immutable instance: an ordered map of attribute name to coerced value, tagged with its *StructType. It answers the reader, Struct.ToH, Struct.With, Struct.Eql and Struct.Inspect operations the gem exposes.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AttrType

type AttrType interface {
	// Coerce coerces and validates v, returning the coerced value or an error
	// whose message matches the gem's.
	Coerce(v any) (any, error)
}

AttrType is the type of an attribute: it coerces-and-validates the attribute's value. Both a dry-types drytypes.Type (via Wrap) and a nested *StructType satisfy it, which is how nested structs compose as attribute types without dry-types needing to know about structs.

func ArrayOf

func ArrayOf(elem AttrType) AttrType

ArrayOf builds an AttrType for a member-typed array whose element type is any AttrType (a nested *StructType or a wrapped dry-types type). For a plain dry-types element you can also use drytypes.ArrayOf directly and pass the result to StructType.Attribute.

func Wrap

func Wrap(t drytypes.Type) AttrType

Wrap adapts a dry-types drytypes.Type into an AttrType so it can be used as an attribute type. StructType.Attribute accepts a drytypes.Type directly and wraps it for you; this is exposed for callers building [Attribute]s by hand.

type Attribute

type Attribute struct {
	// Name is the attribute's symbol name (Ruby `:name`).
	Name drytypes.Symbol
	// Type coerces and validates the attribute's value.
	Type AttrType
	// Optional reports whether the attribute was declared with `attribute?`.
	Optional bool
}

Attribute is one declared member of a *StructType: its symbol name, the AttrType (a dry-types type or a nested *StructType) that coerces-and- validates its value, and whether it is optional (declared with `attribute?`, i.e. the key may be absent).

type Error

type Error struct {
	// Message is the full, gem-faithful error message.
	Message string
	// Cause is the underlying dry-types error (schema / coercion / constraint /
	// missing-key / unknown-keys) that this wraps.
	Cause error
}

Error is raised when constructing a *Struct fails: an attribute is missing, an unexpected key is present under a strict schema, or an attribute's value fails its dry-types coercion/constraint. Its message is byte-identical to Dry::Struct::Error#message: the struct's failing-schema message prefixed with `[<Name>.new] ` (e.g. `[User.new] :age is missing in Hash input`).

func (*Error) Error

func (e *Error) Error() string

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the underlying dry-types error for errors.Is / errors.As.

type KeyTransform

type KeyTransform int

KeyTransform names how a *StructType normalizes incoming hash keys before matching them to attributes (dry-struct's `transform_keys`).

const (
	// KeyNone leaves keys unchanged (the default): keys are matched as given,
	// with a String key accepted for a Symbol attribute of the same name.
	KeyNone KeyTransform = iota
	// KeySymbolize maps every String key to a Symbol (`transform_keys(&:to_sym)`).
	KeySymbolize
	// KeyStringify maps every Symbol key to a String, then back to a Symbol for
	// matching (`transform_keys(&:to_s)`) — dry-struct still requires the
	// declared attribute name.
	KeyStringify
)

type Struct

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

Struct is one immutable dry-struct instance: an ordered map from attribute name to coerced value, tagged with the *StructType that produced it. It answers the reader, Struct.ToH, Struct.With, Struct.Eql and Struct.Inspect operations the gem's instances expose.

func (*Struct) Attributes

func (s *Struct) Attributes() *drytypes.Map

Attributes returns the instance's attributes as an ordered *drytypes.Map (Ruby `#attributes`), with nested *Struct values kept as-is (not deep converted — that is what Struct.ToH does). The returned map must not be mutated.

func (*Struct) Eql

func (s *Struct) Eql(other *Struct) bool

Eql reports whether two instances are equal (Ruby `#==` / `#eql?`): same *StructType and equal attribute maps. Structs of different types are never equal, matching the gem.

func (*Struct) Fetch

func (s *Struct) Fetch(name drytypes.Symbol) any

Fetch returns the value of attribute name, or nil if it is absent — the direct analogue of Ruby's attribute reader / `struct[:name]`.

func (*Struct) Get

func (s *Struct) Get(name drytypes.Symbol) (any, bool)

Get returns the value of attribute name and whether it is present. An optional attribute that was absent at construction is not present (and reads back nil), matching the gem's `struct[:key]` returning nil for an unset optional.

func (*Struct) Inspect

func (s *Struct) Inspect() string

Inspect renders the instance the way dry-struct's `#inspect` does: `#<Name attr=<inspect> …>` in declaration order, with an absent optional attribute shown as `attr=nil`. An attribute-less struct renders `#<Name>`.

func (*Struct) String

func (s *Struct) String() string

String is an alias for Struct.Inspect so a *Struct prints faithfully via fmt.

func (*Struct) ToH

func (s *Struct) ToH() *drytypes.Map

ToH deep-converts the instance to an ordered *drytypes.Map (Ruby `#to_h` / `#to_hash`): nested *Struct values become their own to_h, and arrays of structs map element-wise. Absent optional attributes are omitted.

func (*Struct) ToHash

func (s *Struct) ToHash() *drytypes.Map

ToHash is an alias for Struct.ToH (Ruby exposes both `#to_h` and `#to_hash`).

func (*Struct) Type

func (s *Struct) Type() *StructType

Type returns the *StructType this instance was built from.

func (*Struct) With

func (s *Struct) With(changes *drytypes.Map) (*Struct, error)

With returns a new *Struct with the given attributes overridden (Ruby's `struct.new(changes)`): the receiver's attributes are merged with changes and re-coerced through the schema, yielding a fresh immutable instance. The receiver is unchanged.

type StructType

type StructType struct {
	// Name is the struct's class name, used in error messages and inspect
	// (`[Name.new] …`, `#<Name …>`).
	Name string
	// contains filtered or unexported fields
}

StructType is the analogue of a `Dry::Struct` subclass: a named, ordered list of [Attribute]s plus its key-transform and strictness config. It is the factory for *Struct instances (via StructType.New / StructType.Call).

A StructType implements drytypes.Type, so it can itself be used as the type of an attribute on another StructType — that is how nested structs and `Types::Array.of(SomeStruct)` compose. Applying it to a hash coerces that hash into a *Struct; applying it to an existing *Struct of the same type passes it through unchanged (mirroring the gem).

func Define

func Define(name string, build func(*StructType)) *StructType

Define is the `Dry.Struct do … end` DSL: it builds an anonymous *StructType named name and runs build against it to register attributes and config, returning the finished type. It is sugar over New + the chaining methods.

func New

func New(name string) *StructType

New builds an empty *StructType with the given class name. Register attributes with StructType.Attribute / StructType.AttributeOpt (they return the receiver, so calls chain).

func (*StructType) AsValue

func (s *StructType) AsValue() *StructType

AsValue marks the struct a `Dry::Struct::Value` (comparable-by-value; the gem also freezes it — every instance here is already immutable). Returns the receiver for chaining.

func (*StructType) Attribute

func (s *StructType) Attribute(name drytypes.Symbol, t drytypes.Type) *StructType

Attribute declares a required attribute (`attribute :name, type`) whose type is a dry-types drytypes.Type, and returns the receiver for chaining. Re-declaring a name replaces it in place (keeping its position), matching a subclass overriding an inherited attribute.

func (*StructType) AttributeOpt

func (s *StructType) AttributeOpt(name drytypes.Symbol, t drytypes.Type) *StructType

AttributeOpt declares an optional attribute (`attribute? :name, type`) whose type is a dry-types drytypes.Type: the key may be absent, in which case the attribute is omitted from Struct.ToH and reads back as nil. Returns the receiver for chaining.

func (*StructType) AttributeType

func (s *StructType) AttributeType(name drytypes.Symbol, t AttrType) *StructType

AttributeType declares a required attribute whose type is any AttrType — a nested *StructType (for `attribute :address, AddressStruct`) or a wrapped dry-types type. Returns the receiver for chaining.

func (*StructType) AttributeTypeOpt

func (s *StructType) AttributeTypeOpt(name drytypes.Symbol, t AttrType) *StructType

AttributeTypeOpt declares an optional attribute whose type is any AttrType. Returns the receiver for chaining.

func (*StructType) Attributes

func (s *StructType) Attributes() []Attribute

Attributes returns the declared attributes in declaration order. The slice must not be mutated.

func (*StructType) Call

func (s *StructType) Call(attrs any) (any, error)

Call is `Struct.call(attrs)` / `Struct[attrs]` — an alias for StructType.New.

func (*StructType) Coerce

func (s *StructType) Coerce(v any) (any, error)

Coerce lets a *StructType serve as an attribute type (nested struct): it constructs the nested *Struct from v, returning the gem-shaped error on failure so the enclosing schema can wrap it.

func (*StructType) Inherit

func (s *StructType) Inherit(name string) *StructType

Inherit returns a new *StructType named name that begins with a copy of the parent's attributes and config (dry-struct subclassing). Further StructType.Attribute calls append to (or override) the inherited set.

func (*StructType) IsValue

func (s *StructType) IsValue() bool

IsValue reports whether the struct was declared a `Dry::Struct::Value`.

func (*StructType) MustNew

func (s *StructType) MustNew(attrs any) *Struct

MustNew is StructType.New but panics on error; convenient for tests and for statically-known-valid construction.

func (*StructType) New

func (s *StructType) New(attrs any) (*Struct, error)

New constructs a *Struct from an attribute hash, coercing and validating every attribute through its AttrType. It is `Struct.new(attrs)`:

  • Each required attribute must be present (else `:key is missing in Hash input`); an optional attribute may be absent.
  • Under a strict schema, unexpected keys raise `unexpected keys […]`.
  • Each present value is coerced through its type; the first failure raises a *Error shaped like the gem's.

As a special case matching the gem, New(existing) where existing is already a *Struct of this exact type returns it unchanged (no re-coercion).

func (*StructType) Strict

func (s *StructType) Strict() *StructType

Strict marks the struct's schema strict (`schema schema.strict`): construction rejects unexpected keys. Returns the receiver for chaining.

func (*StructType) TransformKeys

func (s *StructType) TransformKeys(t KeyTransform) *StructType

TransformKeys sets the incoming-key normalization (dry-struct's `transform_keys`). Returns the receiver for chaining.

Jump to

Keyboard shortcuts

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