dom

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jun 18, 2026 License: MIT Imports: 3 Imported by: 0

README

go-yaml-dom

go-yaml-dom is a dependency-free Go library of structural operations over go-yaml v4 representation nodes (*yaml.Node): merge, find, update in place, clone, and compare.

Its only dependency is go.yaml.in/yaml/v4.

go get github.com/yaml/go-yaml-dom

Status: prototype.

This module supports Go 1.18 and is tested with Go 1.18. The repository Makefiles install Go 1.18.10 locally through Makes, so a system Go installation is not required for development.

Purpose

Use go-yaml-dom when you want direct structural operations on go-yaml's representation graph without pulling in an expression engine or non-YAML format adapters. It is the live, in-place companion to go-yaml-yq.

Contract

dom works on live *yaml.Node graphs from go.yaml.in/yaml/v4.

  • Merge mutates the destination node in place.
  • FindNodes and FindNode return live interior pointers.
  • Update mutates those live pointers in place.
  • Clone returns a detached deep copy.
  • Equal compares node content and ignores style, comments, and source positions.

This is intentionally different from expression engines that return copies. If you pass a copied node into Merge or Update, only that copy changes.

Loading And Dumping YAML

Use go-yaml v4 directly to load and dump data:

package main

import (
	"fmt"
	"log"

	yaml "go.yaml.in/yaml/v4"
)

func load(s string) *yaml.Node {
	var n yaml.Node
	if err := yaml.Unmarshal([]byte(s), &n); err != nil {
		log.Fatal(err)
	}
	return &n
}

func dump(n *yaml.Node) {
	out, err := yaml.Marshal(n)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}

API

func Merge(dst, src *yaml.Node, opts ...MergeOption) error

func FindNodes(root *yaml.Node, pred func(*yaml.Node) bool) []*yaml.Node
func FindNode(root *yaml.Node, pred func(*yaml.Node) bool) (*yaml.Node, error)

func Update(nodes []*yaml.Node, fn func(*yaml.Node) error) error

func Clone(node *yaml.Node) *yaml.Node
func Equal(a, b *yaml.Node) bool

Merge

Merge deep-merges src into dst in place.

err := dom.Merge(base, overlay)

For a non-destructive merge, clone first:

merged := dom.Clone(base)
err := dom.Merge(merged, overlay)

By default, source nodes are deep-copied into the destination so later source mutations do not bleed into the merged tree.

Merge options:

  • WithSequenceMerge(dom.SequenceReplace) replaces sequences, the default.
  • WithAppendSequences() appends source sequence items.
  • WithSequenceMerge(dom.SequenceByIndex) merges sequence items by position.
  • WithOnlyExistingKeys() updates only keys already present in the destination.
  • WithOnlyNewKeys() adds only keys absent from the destination.
  • WithClobberTags() lets source custom tags replace destination tags.
  • WithNullMerge(dom.NullOverwrite) lets source null replace destination values, the default.
  • WithNullMerge(dom.NullIgnore) ignores source null values.
  • WithNullMerge(dom.NullDelete) removes destination mapping keys when source values are null.
  • WithSharedSource() grafts source nodes by pointer instead of deep-copying them.

Find And Update

FindNodes walks a node tree in pre-order and returns live pointers to matching nodes. It takes a Go predicate, not a path DSL. Mapping keys are visited. Document wrappers are traversed but not passed to the predicate. Alias targets are not followed.

FindNode is strict: it returns an error unless exactly one node matches.

Update applies a function to live nodes in place and stops at the first error.

Clone

Clone returns a fully detached deep copy. Anchor and alias structure is preserved: aliases in the clone point at cloned anchors, not the original graph.

Equal

Equal compares deep node content: kind, tag, value, children, and alias structure. It ignores style, comments, and source positions.

Example: Basic Merge

package main

import (
	"fmt"
	"log"

	"github.com/yaml/go-yaml-dom"
	yaml "go.yaml.in/yaml/v4"
)

const baseYAML = `
service:
  image: app:v1
  replicas: 1
  env:
    LOG_LEVEL: info
`

const overlayYAML = `
service:
  replicas: 3
  env:
    FEATURE_FLAG: enabled
`

func main() {
	base := load(baseYAML)
	overlay := load(overlayYAML)

	if err := dom.Merge(base, overlay); err != nil {
		log.Fatal(err)
	}

	dump(base)
}

func load(s string) *yaml.Node {
	var n yaml.Node
	if err := yaml.Unmarshal([]byte(s), &n); err != nil {
		log.Fatal(err)
	}
	return &n
}

func dump(n *yaml.Node) {
	out, err := yaml.Marshal(n)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(string(out))
}

Output:

service:
    image: app:v1
    replicas: 3
    env:
        LOG_LEVEL: info
        FEATURE_FLAG: enabled

Example: Append Sequences

package main

import (
	"fmt"
	"log"

	"github.com/yaml/go-yaml-dom"
	yaml "go.yaml.in/yaml/v4"
)

const baseYAML = `
pipeline:
  steps:
    - checkout
    - test
`

const overlayYAML = `
pipeline:
  steps:
    - package
    - deploy
`

func main() {
	base := load(baseYAML)
	overlay := load(overlayYAML)

	if err := dom.Merge(base, overlay, dom.WithAppendSequences()); err != nil {
		log.Fatal(err)
	}

	dump(base)
}

Output:

pipeline:
    steps:
        - checkout
        - test
        - package
        - deploy

Example: Find And Update

package main

import (
	"fmt"
	"log"
	"strings"

	"github.com/yaml/go-yaml-dom"
	yaml "go.yaml.in/yaml/v4"
)

const inputYAML = `
services:
  api:
    image: app:v1
  worker:
    image: worker:v1
`

func main() {
	doc := load(inputYAML)

	images := dom.FindNodes(doc, func(n *yaml.Node) bool {
		return n.Kind == yaml.ScalarNode && strings.HasSuffix(n.Value, ":v1")
	})

	if err := dom.Update(images, func(n *yaml.Node) error {
		n.Value = strings.TrimSuffix(n.Value, ":v1") + ":v2"
		return nil
	}); err != nil {
		log.Fatal(err)
	}

	dump(doc)
}

Output:

services:
    api:
        image: app:v2
    worker:
        image: worker:v2

Example: Non-Destructive Merge

package main

import (
	"fmt"
	"log"

	"github.com/yaml/go-yaml-dom"
	yaml "go.yaml.in/yaml/v4"
)

const baseYAML = `
app:
  image: app:v1
`

const overlayYAML = `
app:
  replicas: 2
`

func main() {
	base := load(baseYAML)
	overlay := load(overlayYAML)

	merged := dom.Clone(base)
	if err := dom.Merge(merged, overlay); err != nil {
		log.Fatal(err)
	}

	fmt.Println("--- original")
	dump(base)
	fmt.Println("--- merged")
	dump(merged)
}

Output:

--- original
app:
    image: app:v1
--- merged
app:
    image: app:v1
    replicas: 2

Running The Examples

The runnable versions of these examples live under examples/.

make -C examples build
make -C examples run
make -C examples clean

Run one example:

make -C examples/basic-merge run

Each example directory has its own ReadMe.md and supports:

make build
make run
make clean

Development

The root Makefile bootstraps Makes into .cache/makes and installs the pinned Go toolchain into .cache/local.

Common targets:

make test        # go test ./...
make vet         # go vet ./...
make verify      # fmt, tidy, vet, test
make examples    # build all example programs
make test-all    # tests plus example smoke runs
make clean       # remove example binaries
make deps        # print the module graph

CI

GitHub Actions runs tests, hygiene checks, example smoke tests, Staticcheck, and CodeQL. Hygiene includes formatting, go.mod/go.sum, file lint, spelling, and dependency graph checks. The dependency graph check verifies that the module graph contains only this module and go.yaml.in/yaml/v4 v4.0.0-rc.5.

Dependabot is configured for Go modules and GitHub Actions.

Works With go-yaml-yq

go-yaml-dom and go-yaml-yq compose only through *yaml.Node; neither imports the other. Use yq expressions when you want path/query/expression power, then use dom.Merge or dom.Update for live in-place structural operations.

Keep both modules on the same go.yaml.in/yaml/v4 version while yaml/v4 is pre-1.0, otherwise *yaml.Node can resolve to distinct types.

Documentation

Overview

Package dom provides live structural operations over go-yaml v4 representation nodes.

The package works directly with yaml.Node values from go.yaml.in/yaml/v4. Merge and Update mutate live node graphs in place. FindNodes and FindNode return live interior pointers. Clone returns detached deep copies, and Equal compares node content while ignoring presentation details such as comments and style.

Merge mutates the destination:

var base, overlay yaml.Node
_ = yaml.Unmarshal([]byte("a: 1\n"), &base)
_ = yaml.Unmarshal([]byte("b: 2\n"), &overlay)
err := dom.Merge(&base, &overlay)

Clone first for non-destructive merge:

merged := dom.Clone(&base)
err := dom.Merge(merged, &overlay)

FindNodes and Update compose for in-place edits:

nodes := dom.FindNodes(doc, func(n *yaml.Node) bool {
    return n.Kind == yaml.ScalarNode && n.Value == "old"
})
err := dom.Update(nodes, func(n *yaml.Node) error {
    n.Value = "new"
    return nil
})

Use github.com/yaml/go-yaml-yq when you want yq's expression language. The two packages compose only through *yaml.Node; neither imports the other.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Clone

func Clone(node *yaml.Node) *yaml.Node

Clone returns an independent deep copy. Aliases in the clone point at cloned anchor nodes, not the original graph.

Example
var base, overlay yaml.Node
_ = yaml.Unmarshal([]byte("a: 1\n"), &base)
_ = yaml.Unmarshal([]byte("b: 2\n"), &overlay)

merged := dom.Clone(&base)
_ = dom.Merge(merged, &overlay)

fmt.Println(dom.Equal(&base, merged))
Output:
false

func Equal

func Equal(a, b *yaml.Node) bool

Equal reports deep content equality. It compares kind, tag, value, children, and alias targets, while ignoring style, comments, and source positions.

func FindNode

func FindNode(root *yaml.Node, pred func(*yaml.Node) bool) (*yaml.Node, error)

FindNode returns exactly one live node matching pred.

func FindNodes

func FindNodes(root *yaml.Node, pred func(*yaml.Node) bool) []*yaml.Node

FindNodes walks root in pre-order and returns live pointers to nodes for which pred returns true. Document nodes are traversed but not passed to pred. Alias targets are not followed.

func Merge

func Merge(dst, src *yaml.Node, opts ...MergeOption) error

Merge deep-merges src into dst in place. dst is mutated; src is not unless WithSharedSource is set, in which case src subtrees may be grafted directly.

Example
var base, overlay yaml.Node
_ = yaml.Unmarshal([]byte("items: [a]\nmeta:\n  env: dev\n"), &base)
_ = yaml.Unmarshal([]byte("items: [b]\nmeta:\n  version: v1\n"), &overlay)

_ = dom.Merge(&base, &overlay, dom.WithAppendSequences())

out, _ := yaml.Marshal(&base)
fmt.Print(string(out))
Output:
items: [a, b]
meta:
    env: dev
    version: v1

func Update

func Update(nodes []*yaml.Node, fn func(*yaml.Node) error) error

Update applies fn to each live node in place, stopping at the first error.

Types

type KeyFilter

type KeyFilter int
const (
	KeyAll KeyFilter = iota
	KeyOnlyExisting
	KeyOnlyNew
)

type MergeOption

type MergeOption func(*mergeConfig)

func WithAppendSequences

func WithAppendSequences() MergeOption

func WithClobberTags

func WithClobberTags(enable ...bool) MergeOption

func WithKeyFilter

func WithKeyFilter(f KeyFilter) MergeOption

func WithNullMerge

func WithNullMerge(s NullStrategy) MergeOption

func WithOnlyExistingKeys

func WithOnlyExistingKeys() MergeOption

func WithOnlyNewKeys

func WithOnlyNewKeys() MergeOption

func WithSequenceMerge

func WithSequenceMerge(s SequenceStrategy) MergeOption

func WithSharedSource

func WithSharedSource(enable ...bool) MergeOption

type NullStrategy

type NullStrategy int
const (
	NullOverwrite NullStrategy = iota
	NullIgnore
	NullDelete
)

type SequenceStrategy

type SequenceStrategy int
const (
	SequenceReplace SequenceStrategy = iota
	SequenceAppend
	SequenceByIndex
)

Directories

Path Synopsis
examples
basic-merge command
clone-merge command
find-update command

Jump to

Keyboard shortcuts

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