Help us shape the future of Go! Take the 2026 Go Developer Survey

yaml

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 5, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

README

go-ruby-yaml/yaml

yaml — go-ruby-yaml

Docs License Go Coverage yaml-test-suite

A pure-Go (no cgo) reimplementation of Ruby's Psych YAML emitter and loader — the deterministic, interpreter-independent core of MRI 4.0.5's Psych.dump / Psych.load. It serialises a tree of Ruby values to a Psych-compatible YAML document and parses one back, so Load(Dump(x)) round-trips the structures Ruby programs (and Puppet's state.yaml / last_run_summary.yaml) persist — without any Ruby runtime.

It is the YAML backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine) and go-ruby-erb (the ERB compiler).

What it is — and isn't. Emitting and parsing YAML for the Ruby value model (scalar typing, block/flow layout, anchors/aliases, the !ruby/object: tag grammar) is fully deterministic and needs no interpreter, so it lives here as pure Go. Binding the documents to live Ruby objects — instantiating a class, reading an object's instance variables — is the host's job; this library hands back a small, explicit value model (*Object, *Range, Symbol, …) the host maps to and from its own objects.

Features

Faithful port of Psych's emit + load, validated against the ruby binary on every supported platform:

  • Block + flow mappings and sequences, with Psych's default layout (a block sequence under a key aligns its dashes with the key; nested mappings indent two deeper; a dash child continues on the dash line).
  • Every scalar style — plain, single-quoted, double-quoted (with the \n \t \r \0 \xNN \" \\ escapes), and literal (| / |- / |+) and folded (>) block scalars, indent-correct at any depth.
  • Psych implicit typing — null / ~, booleans, integers (decimal, 0x hex, 0o octal, 0b binary, _ separators, and big integers), floats (.inf / -.inf / .nan, exponents), and ISO-8601 timestamps.
  • Symbols — the bare :name form and the !ruby/symbol tag.
  • Anchors & aliases — a value shared (or cyclic) in the graph is emitted once behind &N and referenced thereafter with *N; the loader resolves them.
  • !ruby/object:Class mappings of instance variables (deterministic key order — host-supplied Order, else lexicographic), plus !ruby/range, !ruby/class / !ruby/module, and !ruby/regexp.
  • Complex / nil mapping keys via the explicit ? key / : value form.
  • Document markers (---, ...), comments, and #coding headers.

CGO-free, dependency-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-yaml/yaml

Usage

package main

import (
	"fmt"

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

func main() {
	// Build a Ruby value tree from the package's value model.
	m := yaml.NewMap()
	m.Set(yaml.Symbol("checked"), true)
	m.Set("hosts", []any{"web", "db"})
	m.Set("range", &yaml.Range{Begin: 1, End: 5})

	doc, _ := yaml.Dump(m) // Psych.dump
	fmt.Print(doc)
	// ---
	// :checked: true
	// hosts:
	// - web
	// - db
	// range: !ruby/range
	//   begin: 1
	//   end: 5
	//   excl: false

	v, _ := yaml.Load(doc) // Psych.load — mappings come back as *yaml.Map
	fmt.Printf("%T\n", v)  // *yaml.Map
}

Ruby value model

YAML round-trips an any drawn from a small, fixed set of Go types, so a host can map its own object graph to and from this package:

Ruby Go (Dump accepts) Go (Load returns)
nil nil nil
true / false bool bool
Integer int, int64, *big.Int int64 / *big.Int
Float float64, float32 float64
String string string
Symbol yaml.Symbol yaml.Symbol
Array []any []any
Hash *yaml.Map, map[string]any, map[Symbol]any *yaml.Map (ordered)
Time time.Time time.Time
Range *yaml.Range *yaml.Range
object *yaml.Object *yaml.Object
Class/Module yaml.Class / yaml.Module yaml.Class / yaml.Module
Regexp *yaml.Regexp *yaml.Regexp

A plain Go map is emitted in sorted-key order; a *yaml.Map preserves insertion order, and Load always returns mappings as *yaml.Map so key order round-trips.

API

// Dump serialises a Ruby value to a Psych-compatible document (Psych.dump /
// Object#to_yaml). A value outside the model returns an error.
func Dump(v any, opts ...Option) (string, error)

// Load parses a Psych-compatible document (Psych.load / YAML.load). It applies
// NO allow-list and will materialise any class the document names: it is
// Psych.unsafe_load's counterpart.
func Load(s string) (any, error)

// SafeLoad parses like Load, then enforces Psych.safe_load's restriction. It
// ALWAYS restricts: with no options it permits no class at all, which is
// Psych.safe_load's own default (permitted_classes: []). The restriction covers
// everything Psych's class loader gates -- Symbol (values and keys), time.Time,
// Range, Regexp and the class named by a !ruby/class tag -- not just
// !ruby/object:. An unpermitted class is reported as a *DisallowedClassError;
// it is never degraded to a plain mapping.
func SafeLoad(s string, opts ...Option) (any, error)

func WithPermittedClasses(names ...string) Option // Psych permitted_classes:
func WithPermittedSymbols(names ...string) Option // Psych permitted_symbols:
func WithAliases(allowed bool) Option             // Psych aliases: (default false)

// SafeLoad's refusals. Each message is its Psych exception's own, so a host
// binding can surface it verbatim.
type DisallowedClassError   struct { Name   string } // Psych::DisallowedClass
type AliasesNotEnabledError struct{}                 // Psych::AliasesNotEnabled
type AnchorNotDefinedError  struct { Anchor string } // Psych::AnchorNotDefined

type Symbol string
type Class  string
type Module string
type Regexp struct { Source, Flags string }
type Range  struct { Begin, End any; Exclusive bool }
type Object struct { Class string; IVars map[string]any; Order []string }
type Map    struct { /* insertion-ordered Hash */ }
func NewMap() *Map
func (m *Map) Set(key, val any)
func (m *Map) Get(key any) (any, bool)
func (m *Map) Pairs() []Pair
func (m *Map) Len() int

Tests & coverage

The loader is validated against the upstream yaml-test-suite on its accept/reject axis and passes all 402/402 tests — 308/308 well-formed documents load and 94/94 ill-formed inputs are rejected (0 panics, 0 known gaps). Every case is run on every lane and the pass rate is held by a ratchet test that fails on any regression.

The suite pairs those deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: a wide corpus is dumped here and parsed by the system ruby (Psych.parse + YAML.unsafe_load), and MRI-dumped documents are loaded here — round-tripping anchors, !ruby/object, Time, big integers, and ranges in both directions. The oracle scripts $stdout.binmode so Windows text-mode never pollutes the bytes, and skip themselves where ruby is 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-yaml/yaml 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 yaml is a pure-Go (CGO-free) Psych-compatible YAML emitter and loader for the Ruby value model. It is the deterministic, interpreter-independent core of MRI 4.0.5's Psych: Dump renders a tree of Ruby values to a Psych-compatible document and Load parses such a document back, so Load(Dump(x)) round-trips the structures Ruby programs (and Puppet's state/run-summary files) persist — without any Ruby runtime.

Ruby value model

A Ruby value is represented by an [any] drawn from a small, fixed set of Go types so a host (such as go-embedded-ruby) can map its own object graph to and from this package:

Ruby            Go (in)                          Go (out, from Load)
----            -------                          -------------------
nil             nil                              nil
true / false    bool                             bool
Integer         int, int64, *big.Int             int64 or *big.Int
Float           float64, float32                 float64
String          string                           string
Symbol          Symbol                           Symbol
Array           []any, []Value                   []any
Hash            *Map (ordered), map[...]any      *Map (insertion order)
Time            time.Time                        time.Time
Range           *Range                           *Range
Object          *Object (tag + ivars)            *Object
Class / Module  Class / Module                   Class / Module
Regexp          *Regexp                          *Regexp

Dump also accepts a plain Go map[string]any / map[Symbol]any (sorted by key for determinism); Load always yields an ordered *Map for a mapping so key order is preserved on round-trip.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Dump

func Dump(v Value, opts ...Option) (string, error)

Dump serialises a Ruby value to a Psych-compatible YAML document string, matching Psych.dump / Object#to_yaml. v is drawn from the package value model (see the package doc); a value outside that model returns an error rather than panicking. The opts are accepted for Psych parity and do not affect emission.

Types

type AliasesNotEnabledError

type AliasesNotEnabledError struct{}

AliasesNotEnabledError is returned by SafeLoad for a document that dereferences an alias without WithAliases(true), matching Psych::AliasesNotEnabled.

func (*AliasesNotEnabledError) Error

func (e *AliasesNotEnabledError) Error() string

type AnchorNotDefinedError

type AnchorNotDefinedError struct {
	// Anchor is the name the alias referenced.
	Anchor string
}

AnchorNotDefinedError is returned for an alias naming an anchor the document never defines, matching Psych::AnchorNotDefined. The loader resolves such an alias to nil, which silently turns a broken reference into a null value.

func (*AnchorNotDefinedError) Error

func (e *AnchorNotDefinedError) Error() string

type Class

type Class string

Class is a Ruby Class reference, emitted/loaded as the `!ruby/class 'Name'` tag. Its value is the fully-qualified class name (e.g. "String").

type DisallowedClassError

type DisallowedClassError struct {
	// Name is the class the document asked for.
	Name string
}

DisallowedClassError is returned by SafeLoad for a class the document names and the options do not permit. Its message is Psych::DisallowedClass's (psych/exception.rb:23-27), so a host binding can surface it verbatim.

SafeLoad used to REWRITE an unpermitted object into its bare ivar mapping instead of reporting it. A caller that asked for a refusal got a Map, which is type confusion in place of an error -- and silent, because the document had loaded "successfully".

func (*DisallowedClassError) Error

func (e *DisallowedClassError) Error() string

type Map

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

Map is an insertion-ordered Ruby Hash. Load returns mappings as *Map so key order round-trips; Dump accepts *Map, a plain Go map (emitted in sorted key order), or a []Pair via NewMap.

func NewMap

func NewMap() *Map

NewMap returns an empty ordered Map.

func (*Map) Get

func (m *Map) Get(key Value) (Value, bool)

Get returns the value for a comparable key and whether it was present.

func (*Map) Len

func (m *Map) Len() int

Len reports the number of entries.

func (*Map) Pairs

func (m *Map) Pairs() []Pair

Pairs returns the entries in insertion order. The slice must not be mutated.

func (*Map) Set

func (m *Map) Set(key, val Value)

Set inserts or replaces the entry for key. A comparable key replaces an existing equal key; a non-comparable key (slice / map / *Object …) is always appended (Psych's complex-key mappings do not deduplicate here).

type Module

type Module string

Module is a Ruby Module reference, emitted/loaded as `!ruby/module 'Name'`.

type Object

type Object struct {
	Class string
	IVars map[string]Value
	// Order, when non-nil, fixes the emission order of IVars keys; names absent
	// from Order are appended in lexicographic order. When nil, all keys are
	// emitted in lexicographic order.
	Order []string
}

Object is a generic tagged Ruby object — anything Psych writes as a `!ruby/object:ClassName` mapping of instance variables. Class is the tag's class name ("" or "Object" emits the bare `!ruby/object`). IVars holds the instance variables by bare name (no leading "@"); Dump orders them by Order when set, else lexicographically (documented, deterministic). A host binds its own object instances to and from this shape.

type Option

type Option func(*options)

Option configures Dump / SafeLoad, mirroring the keyword options of Psych.dump / Psych.safe_load.

func WithAliases

func WithAliases(allowed bool) Option

WithAliases mirrors Psych.safe_load(aliases: true), permitting a document to dereference `*alias` references. SafeLoad refuses them by default.

func WithPermittedClasses

func WithPermittedClasses(names ...string) Option

WithPermittedClasses adds names to SafeLoad's allow-list (Psych's permitted_classes:). Calling it with no names is meaningful and is NOT the same as not calling it: SafeLoad restricts either way, so both deny every class. Repeated calls accumulate.

func WithPermittedSymbols

func WithPermittedSymbols(names ...string) Option

WithPermittedSymbols narrows which Symbol names SafeLoad may intern (Psych's permitted_symbols:). With no names it narrows nothing, as Psych's empty permitted_symbols does. It is additional to the class check: a document's symbols load only when "Symbol" is also permitted.

type Pair

type Pair struct {
	Key Value
	Val Value
}

Pair is one entry of an ordered mapping.

type Range

type Range struct {
	Begin     Value
	End       Value
	Exclusive bool
}

Range is a Ruby Range, emitted/loaded as the `!ruby/range` mapping with begin / end / excl members. A nil Begin or End models a beginless / endless range.

type Regexp

type Regexp struct {
	Source string
	Flags  string
}

Regexp is a Ruby Regexp, emitted/loaded inline as `!ruby/regexp /source/flags` (Psych's representation). Flags is the trailing option letters (e.g. "mix").

type Symbol

type Symbol string

Symbol is a Ruby Symbol (`:name`). Dump emits the bare `:name` form (or the double-quoted `:"a\nb"` form when the name carries control characters), and Load maps both `:name` scalars and the `!ruby/symbol` tag back to a Symbol.

type SyntaxError

type SyntaxError struct{ Message string }

SyntaxError is the error type Load / SafeLoad return for a malformed document (mirroring Psych::SyntaxError). It carries a human-readable message.

func (*SyntaxError) Error

func (e *SyntaxError) Error() string

Error implements error.

type Value

type Value = any

Value is the interface satisfied by every Ruby value this package handles. It is purely documentary — the public API uses any — but a host may use it to constrain its own adapters.

func Load

func Load(s string) (Value, error)

Load parses a Psych-compatible YAML document into a Ruby value, matching Psych.load / YAML.load. Mappings load as an ordered *Map (key order preserved), sequences as []any, and the Psych tags into the package's Symbol / Object / Range / Class / Module / Regexp / time.Time shapes. A blank document loads as nil.

Load applies NO allow-list: it is Psych.unsafe_load's counterpart and will materialise any class a document names. Use SafeLoad for a document you did not write.

func SafeLoad

func SafeLoad(s string, opts ...Option) (Value, error)

SafeLoad parses like Load and then enforces Psych.safe_load's restriction, returning a *DisallowedClassError for the first class the document names that opts do not permit.

SafeLoad ALWAYS restricts. With no options it permits no class at all, which is Psych.safe_load's own default (its signature is permitted_classes: [], and Psych::ClassLoader::Restricted seeds its allow-list from that list alone), and it is why this function has no "no policy" state to get wrong.

The restriction is wider than `!ruby/object:` tags, because Psych's class loader is: Symbol (in values and in keys), time.Time, Range, Regexp, and the class NAMED by a `!ruby/class` / `!ruby/module` tag all have to be permitted too. Plain scalars and the Map / sequence containers never consult it, which is precisely why psych.rb can document String / Integer / Array / Hash and friends as permitted by default while Restricted holds none of their names.

Jump to

Keyboard shortcuts

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