https://github.com/yaml/go-yaml-dom
https://github.com/yaml/go-yaml-dom
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/yaml/go-yaml-dom
- Owner: yaml
- License: mit
- Created: 2026-06-18T20:45:37.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-18T22:27:11.000Z (about 2 months ago)
- Last Synced: 2026-06-21T20:20:18.261Z (about 2 months ago)
- Language: Go
- Size: 40 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: ReadMe.md
- Contributing: Contributing.md
- License: License
Awesome Lists containing this project
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`.
```sh
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:
```go
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
```go
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.
```go
err := dom.Merge(base, overlay)
```
For a non-destructive merge, clone first:
```go
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
```go
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:
```yaml
service:
image: app:v1
replicas: 3
env:
LOG_LEVEL: info
FEATURE_FLAG: enabled
```
## Example: Append Sequences
```go
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:
```yaml
pipeline:
steps:
- checkout
- test
- package
- deploy
```
## Example: Find And Update
```go
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:
```yaml
services:
api:
image: app:v2
worker:
image: worker:v2
```
## Example: Non-Destructive Merge
```go
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:
```yaml
--- original
app:
image: app:v1
--- merged
app:
image: app:v1
replicas: 2
```
## Running The Examples
The runnable versions of these examples live under `examples/`.
```sh
make -C examples build
make -C examples run
make -C examples clean
```
Run one example:
```sh
make -C examples/basic-merge run
```
Each example directory has its own `ReadMe.md` and supports:
```sh
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:
```sh
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.