ostruct

package module
v0.0.0-...-a9cf6e7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 9, 2026 License: BSD-3-Clause Imports: 3 Imported by: 0

README

go-ruby-ostruct/ostruct

ostruct — go-ruby-ostruct

License Go CI Coverage

A pure-Go (no cgo), MRI-4.0.5-faithful core for Ruby's OpenStruct (require "ostruct").

It implements the data structure underneath OpenStruct: an ordered attribute table keyed by Symbol, with the accessors, conversions, comparison, and inspection MRI exposes — []/[]=, to_h, each_pair, dig, delete_field, ==/eql?, and inspect/to_s. It matches MRI byte-for-byte on the inspect format, to_h insertion ordering, and delete_field semantics.

It is the OpenStruct backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime.

What stays in the host runtime

OpenStruct's defining feature — turning any method name into an attribute read or write — is dynamic and lives in the host (rbgo): method_missing, define_method, and respond_to_missing?. That glue is implemented in terms of this table: a reader call becomes Get, a name= call becomes Set, and respond_to_missing? consults RespondToField. This package owns the table and its MRI-faithful behavior; the host owns the dynamic dispatch.

API

Go Ruby
New(pairs...) OpenStruct.new(hash)
Get(name) / Set(name, v) reader / name= writer (the method_missing target)
Index(name) / SetIndex(name, v) [] / []=
ToH() to_h (Symbol keys, insertion order)
EachPair(fn) each_pair
Members() members
Dig(keys...) dig(*keys)
RespondToField(name) the table half of respond_to_missing?
DeleteField(name) delete_field (returns old value; NameError if absent)
Equal(o) / Eql(o) == / eql?
Inspect() / String() inspect / to_s

Keys accept a Symbol or string (interned via ToSym). Values are held as opaque any; Inspect and Dig route through the Inspector and Digger interfaces so the host's Ruby values render and dig exactly as in MRI, with a built-in renderer covering the common scalar/collection shapes for deterministic, ruby-free testing.

MRI-faithful samples

OpenStruct.new(name: "John", age: 70).inspect  #=> #<OpenStruct name="John", age=70>
OpenStruct.new.inspect                          #=> #<OpenStruct>
OpenStruct.new(b: 1, a: 2, c: 3).to_h.keys      #=> [:b, :a, :c]   (insertion order)
OpenStruct.new(a: {b: {c: 1}}).dig(:a, :b, :c)  #=> 1
o.delete_field(:age)                            #=> 70             (old value)
OpenStruct.new.delete_field(:x)                 #=> NameError: no field 'x' in #<OpenStruct>
OpenStruct.new(name: "John") == OpenStruct.new(name: "John")  #=> true

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-ostruct/ostruct 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 ostruct is a pure-Go (CGO=0), MRI-4.0.5-faithful core for Ruby's OpenStruct (require "ostruct").

It provides the data structure underneath OpenStruct: an ordered attribute table keyed by Symbol, together with the accessors, conversions, comparison, and inspection that MRI exposes. The dynamic method_missing get/set glue and respond_to_missing? — the part that turns an arbitrary method name into a table read or write at call time — stays in the host runtime (rbgo) and is implemented in terms of this table (Get/Set/RespondToField).

The table preserves insertion order: to_h, each_pair, and inspect all walk keys in the order they were first defined, matching MRI.

Values are held as opaque any. So that Inspect and dig can match MRI without depending on a particular value model, the package renders values through the Inspector and Digger interfaces (the host's Ruby values implement these), and falls back to a built-in renderer covering the common Ruby scalar/collection shapes for deterministic, ruby-free testing.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClassName

func ClassName(v any) string

ClassName returns the Ruby class name MRI would use in a #dig TypeError. Hosts expose it via a Classer; built-in Go shapes map to their Ruby class.

func InspectValue

func InspectValue(v any) string

InspectValue renders v as Ruby's Object#inspect would. A value implementing Inspector supplies its own text; otherwise the built-in renderer covers the common Ruby scalar and collection shapes (nil, true/false, Integer, Float, String, Symbol, and []any/[]Pair as Array/Hash) for deterministic, ruby-free rendering.

Types

type ArgumentError

type ArgumentError struct{ Message string }

ArgumentError is raised by OpenStruct.Dig when called with no keys, matching MRI's `wrong number of arguments (given 0, expected 1+)`.

func (*ArgumentError) Error

func (e *ArgumentError) Error() string

type Digger

type Digger interface {
	Dig(keys ...any) (any, error)
}

Digger is the dig protocol: a value that can itself be dug into (Array, Hash, Struct, or a nested OpenStruct in MRI). OpenStruct.Dig delegates the remaining keys to it, matching Ruby's `obj.dig(*keys)`.

type Inspector

type Inspector interface {
	Inspect() string
}

Inspector renders a value the way Ruby's Object#inspect would. The host runtime's Ruby values implement it so OpenStruct.Inspect reproduces MRI byte-for-byte; values that do not implement it are rendered by InspectValue.

type NameError

type NameError struct{ Message string }

NameError is raised by OpenStruct.DeleteField for an absent field. Its message matches MRI: `no field 'NAME' in #<OpenStruct ...>`.

func (*NameError) Error

func (e *NameError) Error() string

type OpenStruct

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

OpenStruct is the ordered attribute table backing Ruby's OpenStruct. Keys are Symbols; insertion order is preserved across to_h, each_pair, and inspect.

The table stores its entries as an insertion-ordered slice of [Pair]s (each Key a Symbol) alongside a Symbol→position index. Keeping the values *in* the ordered slice — rather than in a separate map keyed by the ordered keys — lets OpenStruct.ToH serialise with a single slice copy and no per-key hashing, while random access (OpenStruct.Get/OpenStruct.Set) stays a single map probe.

The zero value is not ready for use; construct with New.

func New

func New(hash ...Pair) *OpenStruct

New builds an OpenStruct seeded from hash, whose keys are interned to Symbols in iteration order. A nil hash yields an empty struct. This is MRI's `OpenStruct.new(hash)`.

The seed is given as ordered key/value Pairs so that insertion order — which MRI preserves from the source hash — is deterministic. Pass no pairs for an empty struct.

func (*OpenStruct) DeleteField

func (o *OpenStruct) DeleteField(name any) (any, error)

DeleteField removes field name, returning its prior value. It raises a *NameError if the field is not defined — MRI's `delete_field`.

func (*OpenStruct) Dig

func (o *OpenStruct) Dig(keys ...any) (any, error)

Dig walks keys: the first key reads this struct's field, and any remaining keys are delegated to that value's dig protocol (Digger, or a nested *OpenStruct). With no keys it returns an *ArgumentError; a key whose value is missing yields nil; an intermediate value that cannot be dug into yields a *TypeError. This is MRI's `dig(*keys)`.

func (*OpenStruct) EachPair

func (o *OpenStruct) EachPair(fn func(key Symbol, value any) bool)

EachPair calls fn for every field in insertion order, stopping early if fn returns false. It mirrors MRI's `each_pair` (the host wraps it to yield to a Ruby block or return an Enumerator).

func (*OpenStruct) Eql

func (o *OpenStruct) Eql(other any) bool

Eql is an alias of OpenStruct.Equal for MRI's `eql?`, which OpenStruct defines identically to `==`.

func (*OpenStruct) Equal

func (o *OpenStruct) Equal(other any) bool

Equal reports OpenStruct equality the way MRI's `==`/`eql?` do: other must be an *OpenStruct (subclasses included, since MRI uses is_a?) and the two tables must be equal as ordered Symbol→value maps. Values compare by Go ==, with a nil-safe fallback for non-comparable values via fmt.

func (*OpenStruct) Get

func (o *OpenStruct) Get(name any) any

Get returns the value of field name (Symbol or string), or nil if the field is not defined — MRI's reader for an undefined attribute returns nil.

func (*OpenStruct) Index

func (o *OpenStruct) Index(name any) any

Index is the `[]` accessor: it reads field name (Symbol or string).

func (*OpenStruct) Inspect

func (o *OpenStruct) Inspect() string

Inspect renders the struct as MRI does: `#<OpenStruct k=v, ...>` with fields in insertion order and each value rendered by its Inspector (or InspectValue); an empty struct renders as `#<OpenStruct>`.

func (*OpenStruct) Len

func (o *OpenStruct) Len() int

Len returns the number of defined fields.

func (*OpenStruct) Members

func (o *OpenStruct) Members() []Symbol

Members returns the field names (Symbols) in insertion order — MRI's `members`/`to_h.keys`.

func (*OpenStruct) RespondToField

func (o *OpenStruct) RespondToField(name any) bool

RespondToField reports whether field name is defined. The host's respond_to_missing? combines this with its own `name end_with? "="` rule for writers; this core answers only "is this attribute present?".

func (*OpenStruct) Set

func (o *OpenStruct) Set(name, value any) any

Set defines (or overwrites) field name with value, returning value. A new field is appended to the insertion order; overwriting an existing field keeps its position. This is MRI's writer (`o.name = value` / `o[name] = value`).

func (*OpenStruct) SetIndex

func (o *OpenStruct) SetIndex(name, value any) any

SetIndex is the `[]=` accessor: it writes field name (Symbol or string) and returns value.

func (*OpenStruct) String

func (o *OpenStruct) String() string

String is the `to_s` alias of OpenStruct.Inspect.

func (*OpenStruct) ToH

func (o *OpenStruct) ToH() []Pair

ToH returns the table as ordered key/value [Pair]s with Symbol keys in insertion order — MRI's `to_h` (whose Hash preserves that order).

Because the entries are already stored in insertion order, this is a single slice copy: no per-key hash probe is needed to rebuild the order. A fresh slice is returned (never the internal backing array), so callers may mutate the result exactly as Ruby's `to_h` hands back an independent Hash.

type Pair

type Pair struct {
	Key   any
	Value any
}

Pair is one ordered key/value entry used to seed New and returned by OpenStruct.ToH/OpenStruct.EachPair. Key may be a Symbol or string.

type Symbol

type Symbol string

Symbol is the key type of an OpenStruct table. MRI keys every OpenStruct attribute by Symbol; strings handed to OpenStruct.Get, OpenStruct.Set, and the bracket accessors are interned to Symbol via ToSym.

func ToSym

func ToSym(name any) Symbol

ToSym converts a name to a Symbol. A Symbol is returned unchanged; a string is interned. This mirrors MRI's `name.to_sym` on the bracket and accessor paths.

type TypeError

type TypeError struct{ Message string }

TypeError is raised by OpenStruct.Dig when an intermediate value does not implement the dig protocol. Its message matches MRI: `CLASS does not have #dig method`.

func (*TypeError) Error

func (e *TypeError) Error() string

Jump to

Keyboard shortcuts

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