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.
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,0xhex,0ooctal,0bbinary,_separators, and big integers), floats (.inf/-.inf/.nan, exponents), and ISO-8601 timestamps. - Symbols — the bare
:nameform and the!ruby/symboltag. - Anchors & aliases — a value shared (or cyclic) in the graph is emitted once
behind
&Nand referenced thereafter with*N; the loader resolves them. !ruby/object:Classmappings of instance variables (deterministic key order — host-suppliedOrder, else lexicographic), plus!ruby/range,!ruby/class/!ruby/module, and!ruby/regexp.- Complex / nil mapping keys via the explicit
? key/: valueform. - Document markers (
---,...), comments, and#codingheaders.
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).
go get github.com/go-ruby-yaml/yamlpackage 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
}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.
// 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).
func Load(s string) (any, error)
// SafeLoad parses like Load but honours a permitted-class allow-list; this loader
// never evaluates code, so it is safe by construction.
func SafeLoad(s string, opts ...Option) (any, error)
func WithPermittedClasses(names ...string) Option // Psych permitted_classes:
func WithAliases(allowed bool) Option // Psych aliases:
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() intThe 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
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%BSD-3-Clause — see LICENSE. Copyright the go-ruby-yaml/yaml authors.
