Documentation
¶
Overview ¶
Package oneyaml reads, writes and edits YAML. Spec-correct YAML 1.2 by default, in pure Go with no dependencies outside the standard library.
The package has two halves. The first is a typed load and dump in the style of encoding/json: Unmarshal, Marshal, NewDecoder and NewEncoder. The second is Document, which holds a whole YAML file and edits it in place. Edits change only the value you set, in its own style; comments and formatting elsewhere are kept byte for byte.
Nesting, alias expansion, number size and patch work are bounded by default; input from outside needs the Untrusted option as well. See "Limits" below.
Install ¶
go get oneyaml.dev/go@latest
The import path is oneyaml.dev/go and the package name is oneyaml. This is a v0 release: the API may change before 1.0. The supported Go versions and what each release promises are in the Stability section of the README.
Load ¶
Unmarshal reads the first document of its input into a Go value. Struct fields are matched through the yaml struct tag; with no yaml tag a json tag is used. A document read into any gives map[string]any, []any, string, bool, int, int64, uint64, math/big.Int and float64. Integers are never rounded. Plain scalars follow the YAML 1.2 Core schema: yes, no, on and off are strings, and 0755 is the integer 755. NewDecoder reads a stream of documents and returns io.EOF at the end.
A type can read its own YAML. The hooks are tried in this order: Unmarshaler, which receives a Node; UnmarshalYAML(func(any) error) error, the older form that yaml.v2, yaml.v3 and goccy/go-yaml accept; encoding.TextUnmarshaler, for a scalar; and encoding/json.Unmarshaler, which receives the value as JSON. So types written for encoding/json, such as Kubernetes API types with json tags and UnmarshalJSON methods, load as they are. An UnmarshalYAML method of any other form, such as yaml.v3's UnmarshalYAML(*yaml.Node) error, is reported as a *TypeError rather than skipped. A Node is not a decode target; read one with Document.Get.
Edit ¶
Parse returns a Document that keeps every byte of its input. Document.Set assigns a value at an RFC 6901 pointer: it replaces an existing value, keeping its style when the style can hold the new value and moving to the nearest style that can when it cannot, and it adds a missing key at the end of its mapping in the style of its siblings. Reading the value back always gives exactly the value set. Document.Remove deletes an entry with the comments that belong to it. Document.Bytes and Document.WriteTo write the result. An edit that cannot be made, such as one that would write through an alias, returns an *EditError and leaves the document unchanged.
An edit makes every Node read before it stale (Node.Stale). A range over Node.Items or Node.Entries may edit the document, as a loop over a map may change the map: it finds its collection again and goes on with fresh nodes. Edits that add or remove entries may make such a loop skip or repeat some; for those, collect the places with Node.Pointer first and edit afterwards.
Document.Get and Document.Decode read values back through the same binder that Unmarshal uses, and Document.Root gives a read-only Node view with kinds, styles, tags, anchors, comments and positions.
Patch ¶
Document.ApplyPatch applies an RFC 6902 JSON Patch whose test operation compares values by the YAML data model, so 1 and 1.0 differ (see the Document.ApplyPatch documentation). Document.Apply takes the same operations as Go values (PatchAdd, PatchReplace and the rest), and Document.ApplyMergePatch applies an RFC 7396 merge patch. A patch is atomic: if any operation fails, the document is unchanged. The edits it makes follow the same rules as Document.Set.
Marshal ¶
Marshal and Encoder write Go values as block-style YAML. A string is written plain only when every common reader, under the Core schema and under YAML 1.1, reads it back as the same string, so "yes", "0755" and "1:30" come out quoted. Output is deterministic: map keys are sorted and struct fields keep their declaration order. A type can choose its own form by implementing Marshaler, encoding.TextMarshaler or encoding/json.Marshaler, tried in that order. A Node is written as the value it holds. The omitempty tag option leaves out zero values, empty collections and structs whose fields are all zero; omitzero leaves out only zero values. Indent sets the indentation.
Limits ¶
Every call that reads input runs under limits on nesting depth, alias expansion, the digits of a number and the work of a patch; see DefaultLimits. They are on unless you pass NoLimits. Input size, scalar size and documents per stream are bounded only with Untrusted, which you should select, and size with Limits, for input from a source you do not control. Options apply in order: Limits changes only the fields it sets, on top of the options before it, and NoBound lifts one bound. So Untrusted(), Limits(LimitSet{MaxDepth: 50}) keeps every untrusted bound and tightens depth. A limit that is hit is a *LimitError. The Document and decoder paths follow aliases under the same budget. Code in your own hooks runs outside these limits, and the compat packages always use the defaults.
Errors ¶
Errors are values to test with errors.As and errors.Is: *SyntaxError carries the line, column, offset and a stable category; *TypeError the path and position of a value that does not fit its target; *LimitError the limit and the option that raises it; and *EditError the operation, the pointer and a kind to match against ErrNotFound, ErrAliasWrite, ErrAmbiguousKey, ErrTestFailed, ErrAnchorInUse, ErrInvalidPatch, ErrInvalidPointer, ErrUndefinedAlias, ErrUnsupportedEdit, ErrStaleNode, ErrNoPointer and ErrNilDocument. Match errors by type and kind: the text of a message is written for people and may change in any release.
The library contains no unsafe code. A panic caused by input is treated as a security bug; report it as described in the security policy in the repository.
Options ¶
One Option type serves every entry point, and an option that does not apply to a call is ignored: KnownFields makes an unknown struct key an error, MergeKeys reads the "<<" merge key, Indent sets the output indentation, and Limits, Untrusted and NoLimits set the limits, in the order given. Duplicate keys are always an error.
Compatibility ¶
The compatibility adapters oneyaml.dev/go/compat/yamlv3 and oneyaml.dev/go/compat/k8syaml have the names and signatures of gopkg.in/yaml.v3 and sigs.k8s.io/yaml, so code that uses them keeps its call sites after one import change. They read YAML 1.1 forms the way their targets do, which this package never does, and each lists where it differs from its target.
More ¶
The repository README has a quick start, and its docs directory has guides on editing, patches, limits, conformance, migration and a comparison with other Go YAML libraries.
Example (BlockLiteralKeepsStyle) ¶
Case A: the new value ends in a line break, so the block literal keeps its style, its header and its place. Only the line of text changes.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte(`#hello
a: 10
#abc 123
b: |
this is a test
c: 13
`))
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/b", "this is a test2\n"); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: #hello a: 10 #abc 123 b: | this is a test2 c: 13
Example (BlockLiteralStrip) ¶
Case B: the new value has no final line break. Reading the document back must give exactly that value, so the header changes to |- and nothing else does.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte(`#hello
a: 10
#abc 123
b: |
this is a test
c: 13
`))
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/b", "this is a test2"); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
var b string
if err := doc.Get("/b", &b); err != nil {
log.Fatal(err)
}
fmt.Printf("%q\n", b)
}
Output: #hello a: 10 #abc 123 b: |- this is a test2 c: 13 "this is a test2"
Example (FromYAMLv3Forms) ¶
Three yaml.v3 habits that differ. 0755 is the decimal integer 755, a date is a string in a generic value, and the "<<" key is an ordinary key unless MergeKeys asks for the merge.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
src := []byte("mode: 0755\nwhen: 2001-02-03\nbase: &b {a: 1}\nuse:\n <<: *b\n")
var plain, merged map[string]any
if err := oneyaml.Unmarshal(src, &plain); err != nil {
log.Fatal(err)
}
if err := oneyaml.Unmarshal(src, &merged, oneyaml.MergeKeys()); err != nil {
log.Fatal(err)
}
fmt.Printf("%v %q\n", plain["mode"], plain["when"])
fmt.Println(plain["use"])
fmt.Println(merged["use"])
out, err := oneyaml.Marshal(struct{ Tags []string }{})
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}
Output: 755 "2001-02-03" map[<<:map[a:1]] map[a:1] tags: null
Example (FromYAMLv3Node) ¶
The yaml.v3 way to change a value and keep the comments is to walk a yaml.Node tree. A Document does it with a pointer.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
src := "# service settings\nname: api # public name\nreplicas: 2\n"
doc, err := oneyaml.Parse([]byte(src))
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/replicas", 3); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: # service settings name: api # public name replicas: 3
Example (QuickStart) ¶
The quick start: Set rewrites a value in its own style and leaves the comments and every other byte as they were.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
src := []byte(`# deploy settings
name: api
replicas: 2 # raised by the release job
script: |
make build
`)
doc, err := oneyaml.Parse(src)
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/replicas", 3); err != nil {
log.Fatal(err)
}
if err := doc.Set("/script", "make build\nmake test\n"); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: # deploy settings name: api replicas: 3 # raised by the release job script: | make build make test
Index ¶
- Constants
- Variables
- func Marshal(v any, opts ...Option) ([]byte, error)
- func Unmarshal(data []byte, v any, opts ...Option) error
- type Decoder
- type Document
- func (d *Document) Apply(ops ...PatchOp) error
- func (d *Document) ApplyMergePatch(rfc7396 []byte) error
- func (d *Document) ApplyPatch(rfc6902 []byte) error
- func (d *Document) Bytes() []byte
- func (d *Document) Decode(v any) error
- func (d *Document) Doc(i int) *Document
- func (d *Document) ExplicitEnd() bool
- func (d *Document) ExplicitStart() bool
- func (d *Document) Get(ptr string, v any) error
- func (d *Document) Len() int
- func (d *Document) Remove(ptr string) error
- func (d *Document) Root() Node
- func (d *Document) Set(ptr string, v any) error
- func (d *Document) TagDirectives() []TagDirective
- func (d *Document) Version() (major, minor int, ok bool)
- func (d *Document) WriteTo(w io.Writer) (int64, error)
- type EditError
- type Encoder
- type LimitError
- type LimitSet
- type Marshaler
- type Node
- func (n Node) AliasName() string
- func (n Node) Anchor() string
- func (n Node) Decode(v any) error
- func (n Node) Entries() iter.Seq2[Node, Node]
- func (n Node) Entry(i int) (key, value Node)
- func (n Node) ExplicitTag() (tag string, ok bool)
- func (n Node) HeadComment() string
- func (n Node) IsValid() bool
- func (n Node) Item(i int) Node
- func (n Node) Items() iter.Seq[Node]
- func (n Node) Kind() NodeKind
- func (n Node) Len() int
- func (n Node) LineComment() string
- func (n Node) Lookup(key string) (Node, bool)
- func (n Node) Pointer() (string, error)
- func (n Node) Position() (line, column int)
- func (n Node) Source() string
- func (n Node) Span() (start, end int)
- func (n Node) Stale() bool
- func (n Node) Style() NodeStyle
- func (n Node) Tag() string
- func (n Node) TagAsWritten() string
- func (n Node) Target() Node
- func (n Node) Value() string
- type NodeKind
- type NodeStyle
- type Option
- type PatchOp
- type SyntaxError
- type TagDirective
- type TypeError
- type Unmarshaler
Examples ¶
- Package (BlockLiteralKeepsStyle)
- Package (BlockLiteralStrip)
- Package (FromYAMLv3Forms)
- Package (FromYAMLv3Node)
- Package (QuickStart)
- Decoder
- DefaultLimits
- Document.Apply
- Document.Apply (TestByValue)
- Document.ApplyMergePatch
- Document.ApplyMergePatch (Alias)
- Document.ApplyPatch
- Document.ApplyPatch (AliasRefused)
- Document.ApplyPatch (PointerText)
- Document.ApplyPatch (Stream)
- Document.Decode
- Document.Doc
- Document.Get
- Document.Get (Node)
- Document.Remove
- Document.Remove (AnchorInUse)
- Document.Set
- Document.Set (Alias)
- Document.Set (AnchorsAndTags)
- Document.Set (NewKey)
- Document.Set (StyleFallback)
- Document.TagDirectives
- Document.WriteTo
- EditError
- Encoder
- Indent
- KnownFields
- Limits
- Limits (AddBound)
- Marshal
- Marshal (JsonTags)
- Marshal (Map)
- Marshal (Omitempty)
- Marshaler
- MergeKeys
- NoLimits
- Node
- Node.Decode
- Node.Entries (Edit)
- Node.ExplicitTag
- Node.Items
- Node.Pointer
- Parse
- SyntaxError
- Unmarshal
- Unmarshal (CollectionKey)
- Unmarshal (FuncHook)
- Unmarshal (Generic)
- Unmarshal (Json)
- Unmarshal (JsonHook)
- Unmarshal (JsonTags)
- Unmarshal (TypeError)
- Unmarshal (YesIsAString)
- Unmarshaler
- Unmarshaler (FromYAMLv3)
- Untrusted
- Untrusted (Tighten)
Constants ¶
const NoBound = -1
NoBound, used as the value of a LimitSet field, removes the bound for that field alone. Any negative value has the same effect; NoBound gives it a name.
Variables ¶
var ( // ErrNotFound means a pointer or path addressed nothing. ErrNotFound = errors.New("oneyaml: path not found") // ErrNilDocument means a method that changes or decodes a document was called on // a nil *Document, which is what a failed Parse returns. ErrNilDocument = errors.New("oneyaml: nil Document") // ErrAliasWrite means an edit would write through an alias. ErrAliasWrite = errors.New("oneyaml: write through an alias") // ErrAmbiguousKey means a key occurs more than once, so an edit cannot choose. ErrAmbiguousKey = errors.New("oneyaml: ambiguous key") // ErrTestFailed means a patch "test" operation did not match. ErrTestFailed = errors.New("oneyaml: patch test failed") // ErrAnchorInUse means an anchor cannot be removed while an alias uses it. ErrAnchorInUse = errors.New("oneyaml: anchor in use") )
Sentinel errors for the kinds of failure a document operation can report. Match them with errors.Is.
var ErrInvalidPatch = errors.New("oneyaml: invalid patch")
ErrInvalidPatch means a patch is not well formed: it is not valid JSON, it is not an array of operations, an operation is unknown or lacks a member it needs, or the operation cannot be done at all (moving a value into itself).
var ErrInvalidPointer = errors.New("oneyaml: invalid JSON pointer")
ErrInvalidPointer means a string is not an RFC 6901 JSON Pointer.
var ErrNoPointer = errors.New("oneyaml: node has no pointer")
ErrNoPointer means a Node has no RFC 6901 pointer: it is a mapping key, lies inside a complex key, or its pointer would not lead back to it.
var ErrStaleNode = errors.New("oneyaml: node is stale: the document was edited after it was read")
ErrStaleNode means a Node was used after the Document it came from was edited. Fetch the node again after the edit.
var ErrUndefinedAlias = errors.New("oneyaml: alias has no anchor in its document")
ErrUndefinedAlias means a value to be written holds an alias whose anchor is not in the document it was read from, so there is nothing to write in its place.
var ErrUnsupportedEdit = errors.New("oneyaml: unsupported edit")
ErrUnsupportedEdit means the edit is valid but the document layout is one the edit engine does not rewrite. The document is left unchanged.
Functions ¶
func Marshal ¶
Marshal returns v as a YAML document.
Output is block style with a two-space indent, ends with a newline and has no document marker. Strings are written plain only when every common reader would read them back as the same string; "yes", "0755" and "1:30" are quoted. Map keys are sorted and struct fields follow declaration order, so the same value always gives the same bytes.
Struct fields are named by the yaml tag, or by the json tag when there is no yaml tag, or by the lower-cased field name. The tag options are omitempty, omitzero, inline and flow. A nil map, slice, pointer or interface is written as null.
Types may implement Marshaler, encoding.TextMarshaler or json.Marshaler, tried in that order. The JSON a json.Marshaler returns is written as the equivalent YAML value (a JSON string as a plain string where it is safe, an object as a mapping with sorted keys). A type with a MarshalYAML method that is not MarshalYAML() (any, error) is a *TypeError. A Node is written as its value. A struct field tagged omitempty is left out when it is a struct whose fields are all zero, or whose IsZero method returns true.
time.Time is written as an RFC 3339 timestamp, time.Duration as its String form, *big.Int as an integer, and []byte as a !!binary scalar. A string that is not valid UTF-8 is also written as !!binary, because no other form keeps its bytes; some readers (goccy) return such a scalar as []byte rather than a string.
Marshal reports a *TypeError for a value with no YAML form (channels, functions, complex numbers) and a *LimitError when nesting or a cycle exceeds the depth limit.
Example ¶
Strings that another reader could take for a number, boolean or null are quoted, so the text reads back as the same string everywhere.
package main
import (
"fmt"
oneyaml "oneyaml.dev/go"
)
func main() {
type Server struct {
Name string `yaml:"name"`
Port int `yaml:"port"`
Enabled bool `yaml:"enabled"`
Tags []string `yaml:"tags,omitempty"`
Version string `yaml:"version"`
Notes string `yaml:"notes"`
}
out, err := oneyaml.Marshal(Server{
Name: "api",
Port: 8080,
Enabled: true,
Tags: []string{"web", "yes"},
Version: "1.10",
Notes: "first line\nsecond line\n",
})
if err != nil {
fmt.Println(err)
return
}
fmt.Print(string(out))
}
Output: name: api port: 8080 enabled: true tags: - web - 'yes' version: '1.10' notes: | first line second line
Example (JsonTags) ¶
Marshal honours json tags when a field has no yaml tag, and writes struct fields in declaration order, where a round trip through JSON sorts them.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
type Container struct {
Name string `json:"name"`
Image string `json:"image"`
Args []string `json:"args,omitempty"`
}
out, err := oneyaml.Marshal(Container{Name: "web", Image: "nginx:1.25", Args: []string{"--port", "8080"}})
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}
Output: name: web image: nginx:1.25 args: - --port - '8080'
Example (Map) ¶
Map keys are written in sorted order, so output is the same on every run.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
out, err := oneyaml.Marshal(map[string]any{
"zebra": 1,
"apple": []any{1.5, nil, "x: y"},
"empty": map[string]any{},
})
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}
Output: apple: - 1.5 - null - 'x: y' empty: {} zebra: 1
Example (Omitempty) ¶
omitempty leaves out zero values, empty slices, maps and strings, and a struct whose fields are all zero. omitzero leaves out only zero values, so an empty but non-nil slice is kept.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
type Resources struct {
CPU string `yaml:"cpu"`
Memory string `yaml:"memory"`
}
out, err := oneyaml.Marshal(struct {
Name string `yaml:"name"`
Resources Resources `yaml:"resources,omitempty"`
Args []string `yaml:"args,omitempty"`
Env []string `yaml:"env,omitzero"`
}{Name: "api", Args: []string{}, Env: []string{}})
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}
Output: name: api env: []
func Unmarshal ¶
Unmarshal reads the first YAML document in data into the value v points to.
v must be a non-nil pointer. Structs are filled by key (see the tag rules on Marshal); maps, slices, arrays, pointers, interfaces and the scalar kinds are filled by shape. A plain scalar is read with the YAML 1.2 Core schema, so "yes" and "0755" are strings. A quoted scalar is always a string, and an explicit tag such as !!int or !!str overrides the resolution. A null value leaves a struct field, number or string as it was and clears a pointer, map, slice or interface.
Into an interface{} a mapping becomes map[string]any, or map[any]any when a key is not a string; a sequence becomes []any; an integer becomes int, or uint64 or *big.Int when it does not fit; a float becomes float64; null becomes nil; and everything else is a string, timestamps and !!binary text included. A tag the Core schema does not resolve is ignored.
Types may read themselves. The hooks are tried in this order: Unmarshaler (UnmarshalYAML with a Node), the older form UnmarshalYAML(func(any) error) error that yaml.v2, yaml.v3 and goccy use, encoding.TextUnmarshaler for a scalar, then json.Unmarshaler. A json.Unmarshaler is given the value as JSON text, so types written for encoding/json (Kubernetes quantities, say) need no YAML code; a value JSON cannot hold, such as a mapping with a key that is not a string or .nan, is a *TypeError. A type with an UnmarshalYAML method of any other form, yaml.v3's UnmarshalYAML(*yaml.Node) error for one, is a *TypeError and is not filled by its fields. A null value clears a pointer target without calling any hook; of the other targets, only an Unmarshaler is called for a null value. time.Time reads a timestamp, time.Duration its String form, *big.Int any integer, and []byte a !!binary scalar (or the text of any other scalar).
A Node is a view into a Document and is not a target: decoding into one is a *TypeError. Read it with Document.Get(pointer, &node).
A mapping key that occurs twice is an error, judged after resolution, so 1 and 0x1 clash. Merge keys are ordinary keys unless the MergeKeys option is given. Unknown struct keys are skipped unless KnownFields is given.
Aliases are decoded by replaying the anchored node; the alias limits bound how much that may produce. If data holds more than one document only the first is read; if it holds none, v is left unchanged and the error is nil.
The error is a *SyntaxError for malformed input, a *TypeError for a value that does not fit its target (the first one found) and a *LimitError when a limit is exceeded. UTF-16 and UTF-32 input, with or without a byte order mark, is converted to UTF-8 first; the offsets of errors in such input index the UTF-8 form.
Example ¶
src := []byte(`# deploy settings
name: api
replicas: 2
ports: [80, 443]
labels:
tier: web
`)
var cfg struct {
Name string `yaml:"name"`
Replicas int `yaml:"replicas"`
Ports []int `yaml:"ports"`
Labels map[string]string `yaml:"labels"`
}
if err := oneyaml.Unmarshal(src, &cfg); err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Name, cfg.Replicas, cfg.Ports, cfg.Labels)
Output: api 2 [80 443] map[tier:web]
Example (CollectionKey) ¶
Generic values hold what Go maps can hold. A collection used as a mapping key cannot be a Go map key, so Unmarshal into any refuses it; a Document reads it.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
src := []byte("[a, b]: c\n")
var v any
fmt.Println(oneyaml.Unmarshal(src, &v) != nil)
doc, err := oneyaml.Parse(src)
if err != nil {
log.Fatal(err)
}
key, value := doc.Root().Entry(0)
fmt.Println(key.Kind(), key.Len(), value.Value())
}
Output: true sequence 2 c
Example (FuncHook) ¶
package main
import (
"fmt"
"log"
"strings"
oneyaml "oneyaml.dev/go"
)
// Tags reads either a sequence or a comma-separated string. It uses the
// UnmarshalYAML form of yaml.v2, yaml.v3 and goccy, so code written for them
// keeps working.
type Tags []string
func (t *Tags) UnmarshalYAML(unmarshal func(any) error) error {
var list []string
if err := unmarshal(&list); err == nil {
*t = list
return nil
}
var one string
if err := unmarshal(&one); err != nil {
return err
}
*t = strings.Split(one, ",")
return nil
}
func main() {
var doc struct {
A Tags `yaml:"a"`
B Tags `yaml:"b"`
}
if err := oneyaml.Unmarshal([]byte("a: [x, y]\nb: \"p,q,r\"\n"), &doc); err != nil {
log.Fatal(err)
}
fmt.Println(doc.A, doc.B)
}
Output: [x y] [p q r]
Example (Generic) ¶
var v any
err := oneyaml.Unmarshal([]byte("a: 1\nb: [x, 2.5, yes, null]\n"), &v)
fmt.Println(v, err)
Output: map[a:1 b:[x 2.5 yes <nil>]] <nil>
Example (Json) ¶
JSON is YAML, so a JSON document reads with Unmarshal as it is, and integers above 2^53 stay exact.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
var v map[string]any
if err := oneyaml.Unmarshal([]byte(`{"id": 12345678901234567890, "ok": true}`), &v); err != nil {
log.Fatal(err)
}
fmt.Println(v["id"], v["ok"])
}
Output: 12345678901234567890 true
Example (JsonHook) ¶
A type that implements json.Unmarshaler or json.Marshaler and no YAML hook is read and written through that hook: the YAML value becomes JSON text first.
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
oneyaml "oneyaml.dev/go"
)
// Level is written for encoding/json: it reads and writes the words "low" and
// "high" and nothing else.
type Level int
func (l *Level) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
switch strings.ToLower(s) {
case "low":
*l = 1
case "high":
*l = 2
default:
return fmt.Errorf("unknown level %q", s)
}
return nil
}
func (l Level) MarshalJSON() ([]byte, error) {
return json.Marshal([]string{"none", "low", "high"}[l])
}
// A type that implements json.Unmarshaler or json.Marshaler and no YAML hook is
// read and written through that hook: the YAML value becomes JSON text first.
func main() {
var cfg struct {
Level Level `yaml:"level"`
}
if err := oneyaml.Unmarshal([]byte("level: high\n"), &cfg); err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Level)
out, err := oneyaml.Marshal(cfg)
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
// The error from UnmarshalJSON comes back inside a *TypeError.
err = oneyaml.Unmarshal([]byte("level: loud\n"), &cfg)
fmt.Println(err)
}
Output: 2 level: high oneyaml: line 1, column 8: /level: cannot unmarshal "loud" into oneyaml_test.Level: unknown level "loud"
Example (JsonTags) ¶
Types that carry only json tags, as Kubernetes types do, read without a yaml tag; any yaml tag on a field takes the place of its json tag.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
var c struct {
Name string `json:"name"`
Image string `json:"image,omitempty"`
}
if err := oneyaml.Unmarshal([]byte("name: web\nimage: nginx:1.25\n"), &c); err != nil {
log.Fatal(err)
}
fmt.Println(c.Name, c.Image)
}
Output: web nginx:1.25
Example (TypeError) ¶
var cfg struct {
Port int `yaml:"port"`
}
err := oneyaml.Unmarshal([]byte("name: api\nport: eighty\n"), &cfg)
var te *oneyaml.TypeError
if errors.As(err, &te) {
fmt.Printf("%s at line %d, column %d: %s\n", te.Path, te.Line, te.Column, te.Msg)
}
Output: /port at line 2, column 7: cannot unmarshal !!str `eighty` into int
Example (YesIsAString) ¶
The primary API reads YAML 1.2: yes is a string, not a boolean, so a bool field refuses it. The compat packages read the way their targets do.
package main
import (
"errors"
"fmt"
oneyaml "oneyaml.dev/go"
)
func main() {
var cfg struct {
Debug bool `yaml:"debug"`
}
err := oneyaml.Unmarshal([]byte("debug: yes\n"), &cfg)
var te *oneyaml.TypeError
if errors.As(err, &te) {
fmt.Println(te.Path, te.Line, te.Column)
}
}
Output: /debug 1 8
Types ¶
type Decoder ¶
type Decoder struct {
// contains filtered or unexported fields
}
A Decoder reads YAML documents from an input stream, one per call to Decode.
It reads ahead only as far as the end of the current document: a line that starts with "---" or "..." at column 0 ends a document wherever it occurs, since YAML forbids those markers inside any node. The document is then parsed from the buffer, so a Decoder never holds more than one document and the bytes after it.
Example ¶
stream := "name: first\n---\nname: second\n---\nname: third\n"
dec := oneyaml.NewDecoder(strings.NewReader(stream))
for {
var doc struct {
Name string `yaml:"name"`
}
err := dec.Decode(&doc)
if err == io.EOF {
break
}
if err != nil {
fmt.Println(err)
return
}
fmt.Println(doc.Name)
}
Output: first second third
func NewDecoder ¶
NewDecoder returns a Decoder that reads from r. The options are those of Unmarshal.
func (*Decoder) Decode ¶
Decode reads the next document into the value v points to, as Unmarshal does. It returns io.EOF when there are no more documents. After a syntax error or a read error every later call returns the same error; after a *TypeError the next call reads the next document. An empty document decodes as null.
type Document ¶
type Document struct {
// contains filtered or unexported fields
}
A Document is a YAML stream held losslessly: every byte of the input, including comments, blank lines, indentation and line breaks, comes back from Bytes unless an edit changed it.
The methods address the first document of the stream; Len and Doc reach the others. A Document returned by Doc is a view that shares the stream: an edit through it changes the stream, and every view sees the change.
Edits are exact and minimal. Set writes the new value in the style of the value it replaces when that style can hold it, and falls back to the nearest style that can otherwise; text outside the edited value is not touched. An edit that fails leaves the document as it was. An edit makes every Node read before it stale; Node.Stale reports it. An edit, patch or merge patch that fails is rolled back completely and leaves the Nodes read before it valid. Read nodes again after editing, or collect their pointers first (Node.Pointer). A Document is not safe for concurrent use.
A nil *Document, which is what a failed Parse returns, is safe to call: getters return zero values (Len is 0, Bytes is nil, Root is the zero Node) and methods that decode, edit or patch return an error matching ErrNilDocument.
func Parse ¶
Parse reads data as a YAML stream and returns it as a Document. The data is copied; later changes to the slice do not affect the Document.
A stream with no documents (empty input) yields a Document of length 0. Syntax errors are reported as *SyntaxError and exceeded limits as *LimitError.
Example ¶
Parse keeps the whole file. Set rewrites one value in its own style and leaves the comments, the blank lines and every other byte alone.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
src := []byte(`# deploy settings
name: api
replicas: 2 # raised by the release job
script: |
make build
`)
doc, err := oneyaml.Parse(src)
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/replicas", 3); err != nil {
log.Fatal(err)
}
if err := doc.Set("/script", "make build\nmake test\n"); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
var replicas int
if err := doc.Get("/replicas", &replicas); err != nil {
log.Fatal(err)
}
fmt.Println(replicas)
}
Output: # deploy settings name: api replicas: 3 # raised by the release job script: | make build make test 3
func (*Document) Apply ¶
Apply applies RFC 6902 operations to the document in order, all or nothing, as ApplyPatch does.
Example ¶
Apply takes the same operations as Go values.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: 1\nb: 2\n"))
if err != nil {
log.Fatal(err)
}
err = doc.Apply(
oneyaml.PatchTest("/a", 1),
oneyaml.PatchMove("/b", "/c"),
oneyaml.PatchAdd("/d", []string{"x", "y"}),
)
if err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: a: 1 c: 2 d: - x - y
Example (TestByValue) ¶
A "test" operation compares by the YAML data model: 1 is an integer and 1.0 is a float, so they are different.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("n: 1\n"))
if err != nil {
log.Fatal(err)
}
fmt.Println(doc.Apply(oneyaml.PatchTest("/n", 1)))
err = doc.Apply(oneyaml.PatchTest("/n", 1.0))
fmt.Println(errors.Is(err, oneyaml.ErrTestFailed))
}
Output: <nil> true
func (*Document) ApplyMergePatch ¶
ApplyMergePatch applies an RFC 7396 JSON Merge Patch, given as JSON text, to the document. A member of the patch that is null deletes the member of the same name; an object member is merged into the mapping it meets; any other value replaces what is there, and a sequence replaces the whole sequence. A patch that is not an object replaces the whole document.
The patch is atomic: on any error the document is left as it was, byte for byte. Members that the patch adds go at the end of their mapping in the style of their siblings, and comments and layout outside the changed values stay. A null in the target is kept unless the patch names it. Merging an object into a mapping that is an alias is refused (ErrAliasWrite) because it would change every other use of the anchor; an alias to something else is replaced.
The result may not be longer than a MaxInputBytes limit, the patch text may not either, and the text its edits have to parse again is bounded (MaxPatchWorkRatio); past a bound the call fails with a *LimitError and changes nothing.
The patch applies to the first document, or to the document a view from Doc selects.
Example ¶
ApplyMergePatch applies an RFC 7396 merge patch: null deletes a member, an object merges into the mapping it meets, and anything else replaces.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte(`# settings
log:
level: info # default
file: app.log
features: [a, b]
`))
if err != nil {
log.Fatal(err)
}
err = doc.ApplyMergePatch([]byte(`{"log": {"level": "debug", "file": null}, "features": ["c"], "debug": true}`))
if err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: # settings log: level: debug # default features: [c] debug: true
Example (Alias) ¶
Merge patches replace arrays whole and refuse to merge into an alias.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("base: &b {x: 1}\ncopy: *b\n"))
if err != nil {
log.Fatal(err)
}
err = doc.ApplyMergePatch([]byte(`{"copy": {"x": 2}}`))
fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite))
}
Output: true
func (*Document) ApplyPatch ¶
ApplyPatch applies an RFC 6902 JSON Patch, given as JSON text, to the document. The patch is atomic: if any operation fails, including a "test" that does not match, the document is left exactly as it was, byte for byte, and the error is an *EditError that names the operation.
The operations follow the RFC, with these points where YAML needs a decision:
- Pointers address mapping keys by their scalar text, and a key that occurs twice is ambiguous (ErrAmbiguousKey).
- Reads follow aliases, including "from" of copy and the paths of "test". Writing below an alias is refused (ErrAliasWrite); replacing the alias node itself is allowed.
- "test" compares by the YAML data model under the Core schema: 1 differs from 1.0, 0x1F equals 31, a quoted "1" is a string, an explicit core tag such as !!float decides the type of its scalar, and mappings are equal whatever the order of their keys.
- "copy" writes the value without its anchor, so no anchor is defined twice. An alias inside the copied value stays an alias while its anchor is still defined once, before the destination; otherwise it is written out.
- "move" refuses an anchored value that aliases still use (ErrAnchorInUse), and a destination inside the value being moved (ErrInvalidPatch).
- What a patch may write is bounded. The text copies add up to at most the alias bytes limit (ten times the document, never less than 64 MiB by default), and with a MaxInputBytes limit no step may leave a document larger than it; a patch past either bound fails with a *LimitError and changes nothing. The text its edits have to parse again is bounded too (MaxPatchWorkRatio), and with a MaxInputBytes limit the patch text itself may not be longer than it. NoLimits removes the bounds.
- New members go at the end of their mapping, in the style of their siblings; comments outside the edited values stay.
A patch applies to the first document, or to the document a view from Doc selects.
Example ¶
ApplyPatch applies an RFC 6902 patch in one step. Comments and layout outside the changed values stay, and if any operation fails nothing changes.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte(`# service
name: api
replicas: 2 # raised by the release job
tags: [web]
`))
if err != nil {
log.Fatal(err)
}
err = doc.ApplyPatch([]byte(`[
{"op": "test", "path": "/name", "value": "api"},
{"op": "replace", "path": "/replicas", "value": 3},
{"op": "add", "path": "/tags/-", "value": "edge"},
{"op": "copy", "from": "/name", "path": "/title"}
]`))
if err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
// A failing test rejects the whole patch.
err = doc.ApplyPatch([]byte(`[
{"op": "replace", "path": "/replicas", "value": 9},
{"op": "test", "path": "/name", "value": "web"}
]`))
var ee *oneyaml.EditError
fmt.Println(errors.Is(err, oneyaml.ErrTestFailed), errors.As(err, &ee) && ee.Index == 1)
fmt.Print(string(doc.Bytes()))
}
Output: # service name: api replicas: 3 # raised by the release job tags: [web, edge] title: api true true # service name: api replicas: 3 # raised by the release job tags: [web, edge] title: api
Example (AliasRefused) ¶
A patch never writes through an alias, and a refused patch changes nothing, not even the operations before the refused one.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("base: &b\n x: 1\ncopy: *b\n"))
if err != nil {
log.Fatal(err)
}
err = doc.ApplyPatch([]byte(`[
{"op": "replace", "path": "/base/x", "value": 2},
{"op": "replace", "path": "/copy/x", "value": 3}
]`))
fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite))
fmt.Print(string(doc.Bytes()))
}
Output: true base: &b x: 1 copy: *b
Example (PointerText) ¶
A pointer token matches a key by its text, so /1 addresses the integer key 1. Complex keys cannot be addressed.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("1: one\n\"2\": two\n"))
if err != nil {
log.Fatal(err)
}
err = doc.ApplyPatch([]byte(`[
{"op": "replace", "path": "/1", "value": "uno"},
{"op": "replace", "path": "/2", "value": "dos"}
]`))
if err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: 1: uno "2": dos
Example (Stream) ¶
A patch applies to one document of a stream: the one the Document addresses.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: 1\n---\na: 1\n"))
if err != nil {
log.Fatal(err)
}
if err := doc.Doc(1).ApplyPatch([]byte(`[{"op": "replace", "path": "/a", "value": 2}]`)); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: a: 1 --- a: 2
func (*Document) Bytes ¶
Bytes returns the stream as text. The slice must not be modified. It stays valid after later edits, which build new text instead of changing it.
func (*Document) Decode ¶
Decode reads the document into the value v points to, with the rules and errors of Unmarshal: the same binder reads the stored tree that Unmarshal reads from the parser, so the two always agree. Aliases are expanded within the alias and depth limits the Document was parsed with. A Document with no documents leaves v unchanged. If the view addresses a document that does not exist, the error is an *EditError matching ErrNotFound.
Example ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("server:\n host: example.org\n ports: [80, 443]\n"))
if err != nil {
log.Fatal(err)
}
var cfg struct {
Server struct {
Host string `yaml:"host"`
Ports []int `yaml:"ports"`
} `yaml:"server"`
}
if err := doc.Decode(&cfg); err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Server.Host, cfg.Server.Ports)
}
Output: example.org [80 443]
func (*Document) Doc ¶
Doc returns a view of document i of the stream. The view shares the stream with d. If i is out of range every operation on the view reports ErrNotFound.
Example ¶
A stream of several documents is one Document; Doc addresses one of them.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: 1\n---\nb: 2\n"))
if err != nil {
log.Fatal(err)
}
if err := doc.Doc(1).Set("/c", 3); err != nil {
log.Fatal(err)
}
fmt.Println(doc.Len())
fmt.Print(string(doc.Bytes()))
}
Output: 2 a: 1 --- b: 2 c: 3
func (*Document) ExplicitEnd ¶
ExplicitEnd reports whether the document ends with a "..." marker. It is false for a view of a document that does not exist.
func (*Document) ExplicitStart ¶
ExplicitStart reports whether the document begins with a "---" marker. A document without the marker is the first of its stream, or follows one that ended with "...". It is false for a view of a document that does not exist.
func (*Document) Get ¶
Get reads the value at the RFC 6901 pointer ptr into v, as Decode does for the whole document: v is any target Unmarshal accepts. Aliases are followed, and expanding them is charged to the alias limits. A *TypeError's Path is the pointer ptr followed by the path inside the value.
If v is a *Node it receives the node itself, and the alias at the end of the pointer, if there is one, is not followed.
Example ¶
Get reads a value by pointer; aliases are followed.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("base: &b\n port: 8080\nreplica: *b\n"))
if err != nil {
log.Fatal(err)
}
var port int
if err := doc.Get("/replica/port", &port); err != nil {
log.Fatal(err)
}
fmt.Println(port)
}
Output: 8080
Example (Node) ¶
A Node is a view into a Document, so it is read with Document.Get, never decoded into. Marshal writes a Node as the value it holds.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
src := []byte("name: api\nspec: {replicas: 2, tier: web}\n")
var n oneyaml.Node
err := oneyaml.Unmarshal(src, &n)
var te *oneyaml.TypeError
fmt.Println(errors.As(err, &te))
doc, err := oneyaml.Parse(src)
if err != nil {
log.Fatal(err)
}
if err := doc.Get("/spec", &n); err != nil {
log.Fatal(err)
}
fmt.Println(n.Kind(), n.Len())
out, err := oneyaml.Marshal(struct {
Spec oneyaml.Node `yaml:"spec"`
}{n})
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}
Output: true mapping 2 spec: replicas: 2 tier: web
func (*Document) Remove ¶
Remove deletes the value at ptr, with its key or its place in the sequence. A comment on the line of the entry, a comment directly above it and a comment inside its value go with it. Removing the last entry of a nested collection leaves {} or []. Removing an anchored node that an alias still uses is refused (ErrAnchorInUse).
Example ¶
Remove deletes an entry together with the comment on its line, the comment directly above it and any comment inside its value. A comment above the next entry stays.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: 1\n# about b\nb: 2 # two\n# about c\nc: 3\n"))
if err != nil {
log.Fatal(err)
}
if err := doc.Remove("/b"); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: a: 1 # about c c: 3
Example (AnchorInUse) ¶
An edit that would leave an alias without its anchor is refused, and the document is unchanged.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("base: &b\n x: 1\ncopy: *b\n"))
if err != nil {
log.Fatal(err)
}
err = doc.Remove("/base")
fmt.Println(errors.Is(err, oneyaml.ErrAnchorInUse))
fmt.Print(string(doc.Bytes()))
}
Output: true base: &b x: 1 copy: *b
func (*Document) Root ¶
Root returns the root node of the document. The zero Node is returned if the document does not exist.
func (*Document) Set ¶
Set replaces the value at the RFC 6901 pointer ptr with v, or adds it when the pointer names a mapping key that does not exist or the end of a sequence ("-" or the length).
v is anything Marshal accepts. The value that is replaced keeps its style (plain, quoted, literal or folded block, flow or block) when that style can hold the new value; otherwise only that value falls back to the nearest style that can. A new member goes at the end of its mapping and copies the style of its siblings. Reading the document back gives exactly v.
Set refuses to write below an alias (ErrAliasWrite) and a key that occurs twice (ErrAmbiguousKey). Replacing the alias node itself is allowed. On any error the document is unchanged and the error is an *EditError.
Example ¶
A string that cannot stay plain is quoted, a value added to a mapping goes at its end in the style of its siblings, and removing the last entry of a nested mapping leaves {}.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: 'x'\nb:\n c: 1\n"))
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/a", "true"); err != nil {
log.Fatal(err)
}
if err := doc.Set("/d", "new"); err != nil {
log.Fatal(err)
}
if err := doc.Remove("/b/c"); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: a: 'true' b: {} d: 'new'
Example (Alias) ¶
Writing below an alias would change every other place that shares the anchored node, so it is refused and nothing changes.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("base: &b\n x: 1\ncopy: *b\n"))
if err != nil {
log.Fatal(err)
}
err = doc.Set("/copy/x", 2)
fmt.Println(errors.Is(err, oneyaml.ErrAliasWrite))
err = doc.Set("/base/x", 2)
fmt.Println(err)
fmt.Print(string(doc.Bytes()))
}
Output: true <nil> base: &b x: 2 copy: *b
Example (AnchorsAndTags) ¶
An anchor stays on a replaced value, so every alias reads the new value. A tag stays when the new value reads back as exactly that value under it, and replacing an alias itself writes the new value in its place.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: &x 1\nb: *x\nt: !custom foo\n"))
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/a", 2); err != nil {
log.Fatal(err)
}
if err := doc.Set("/t", "bar"); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
var b int
if err := doc.Get("/b", &b); err != nil {
log.Fatal(err)
}
fmt.Println(b)
if err := doc.Set("/b", 9); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: a: &x 2 b: *x t: !custom bar 2 a: &x 2 b: 9 t: !custom bar
Example (NewKey) ¶
A new key goes at the end of its mapping and copies the style of its siblings. A null that is already there keeps its spelling.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("# ports\nname: 'api'\nport: 80\nnote: ~\n"))
if err != nil {
log.Fatal(err)
}
if err := doc.Set("/owner", "ops"); err != nil {
log.Fatal(err)
}
if err := doc.Set("/note", nil); err != nil {
log.Fatal(err)
}
if err := doc.Set("/port", 8080); err != nil {
log.Fatal(err)
}
fmt.Print(string(doc.Bytes()))
}
Output: # ports name: 'api' port: 8080 note: ~ owner: 'ops'
Example (StyleFallback) ¶
When the existing style cannot hold the new value, only that value moves to the nearest style that can. A plain scalar stays plain when the Core schema reads it back as the same string, and becomes quoted when it would not.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: plain\nb: plain\nc: plain\nd: \"quoted\"\n"))
if err != nil {
log.Fatal(err)
}
for _, edit := range []struct {
ptr string
v any
}{
{"/a", "yes"}, // Core reads this as a string: it stays plain
{"/b", "true"}, // Core reads this as a boolean: it is quoted
{"/c", "two\nlines"}, // a line break needs an escape: double quotes
{"/d", "now: plain?"}, // a double-quoted scalar stays double-quoted
} {
if err := doc.Set(edit.ptr, edit.v); err != nil {
log.Fatal(err)
}
}
fmt.Print(string(doc.Bytes()))
}
Output: a: yes b: 'true' c: "two\nlines" d: "now: plain?"
func (*Document) TagDirectives ¶
func (d *Document) TagDirectives() []TagDirective
TagDirectives returns the %TAG directives written before the document, in order. Directives belong to one document: those of an earlier document are not included.
Example ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("%YAML 1.2\n%TAG !e! tag:example.com,2000:\n---\na: 1\n...\n---\nb: 2\n"))
if err != nil {
log.Fatal(err)
}
fmt.Println(doc.Doc(0).ExplicitStart(), doc.Doc(0).ExplicitEnd(), doc.Doc(0).TagDirectives())
fmt.Println(doc.Doc(0).Version())
fmt.Println(doc.Doc(1).ExplicitStart(), doc.Doc(1).ExplicitEnd(), doc.Doc(1).TagDirectives())
fmt.Println(doc.Doc(1).Version())
}
Output: true true [{!e! tag:example.com,2000:}] 1 2 true true false [] 0 0 false
func (*Document) Version ¶
Version returns the version of a %YAML directive the document carries. ok is false when it has none, in which case the document is read as YAML 1.2.
func (*Document) WriteTo ¶
WriteTo writes the stream to w.
Example ¶
Document writes the bytes it holds to any io.Writer.
package main
import (
"fmt"
"log"
"strings"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("# kept\na: 1\n"))
if err != nil {
log.Fatal(err)
}
var sb strings.Builder
n, err := doc.WriteTo(&sb)
if err != nil {
log.Fatal(err)
}
fmt.Print(n, " ", sb.String())
}
Output: 12 # kept a: 1
type EditError ¶
type EditError struct {
// Op is the operation, for example "replace".
Op string
// Pointer is the RFC 6901 pointer the operation addressed. For "move" and
// "copy" it is the destination.
Pointer string
// From is the source pointer of a "move" or "copy".
From string
// Patch reports that the operation was part of a patch, and Index is its
// zero-based position in that patch.
Patch bool
Index int
// Kind is one of the sentinel errors above, or another cause.
Kind error
}
EditError reports a failed document edit or patch operation.
Example ¶
An edit that fails returns an *EditError that says what was attempted.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("a: 1\n"))
if err != nil {
log.Fatal(err)
}
err = doc.Set("/a/b/c", 2)
var ee *oneyaml.EditError
if errors.As(err, &ee) {
fmt.Println(ee.Op, ee.Pointer, errors.Is(err, oneyaml.ErrNotFound))
}
}
Output: set /a/b/c true
type Encoder ¶
type Encoder struct {
// contains filtered or unexported fields
}
An Encoder writes YAML documents to an output stream.
Example ¶
An Encoder writes several documents to one stream, separated by "---".
package main
import (
"log"
"os"
oneyaml "oneyaml.dev/go"
)
func main() {
enc := oneyaml.NewEncoder(os.Stdout)
if err := enc.Encode(map[string]any{"kind": "first"}); err != nil {
log.Fatal(err)
}
if err := enc.Encode([]int{1, 2}); err != nil {
log.Fatal(err)
}
if err := enc.Close(); err != nil {
log.Fatal(err)
}
}
Output: kind: first --- - 1 - 2
func NewEncoder ¶
NewEncoder returns an Encoder that writes to w.
type LimitError ¶
type LimitError struct {
// Limit names the limit, for example "depth".
Limit string
// Max is the bound that was exceeded.
Max int
// Option names the option that raises the bound.
Option string
// Path is the RFC 6901 pointer to the value where the limit was hit, if known.
Path string
}
LimitError reports that a limit was exceeded.
func (*LimitError) Error ¶
func (e *LimitError) Error() string
Error names the limit, where it was hit and the option that raises it.
type LimitSet ¶
type LimitSet struct {
// MaxDepth is the deepest nesting of collections.
MaxDepth int
// MaxAliasNodes is the number of nodes that alias expansion may produce.
// Left at its default, the bound is raised to ten times the number of nodes
// the document itself holds (two times with Untrusted), so a large document
// that reuses its anchors is not refused. A value you set with Limits
// applies exactly as given.
MaxAliasNodes int
// MaxAliasBytes is the number of scalar bytes that alias expansion may
// produce. When left at zero it is raised with the input size in the same way
// as MaxAliasNodes.
MaxAliasBytes int
// MaxInputBytes is the largest input accepted.
MaxInputBytes int
// MaxScalarBytes is the longest single scalar accepted.
MaxScalarBytes int
// MaxDocuments is the most documents accepted in one stream.
MaxDocuments int
// MaxNumberDigits is the most digits (counting the whole text of a float)
// that an integer or float scalar may have when it is converted to a
// number, whether into a big.Int, a float, or a generic value. Turning a
// very long decimal into a big.Int takes time that grows with the square of
// its length, so a few megabytes of digits could otherwise keep a call busy
// for minutes. A longer number makes the call fail with a *LimitError.
// Reading the scalar as a string, or keeping its text in a Node or Document,
// is never limited because no conversion happens.
MaxNumberDigits int
// MaxPatchWorkRatio bounds the text that the edits of one patch (ApplyPatch,
// Apply or ApplyMergePatch) may have to read again, as a multiple of the
// document plus the patch, with 16 MiB on top. Most edits touch a small
// region, but some re-read the whole document, so a long patch against a large
// document could otherwise cost time that grows with the square of its size.
// A patch that goes past the bound fails with a *LimitError and changes
// nothing.
MaxPatchWorkRatio int
}
LimitSet bounds the work and memory a call may spend.
A field left at zero is not changed: it keeps the bound given by the options before Limits (the value DefaultLimits gives it if there are none, which is no bound for the fields that have none by default). So setting one field adds that bound without lifting the others, and Untrusted(), Limits(LimitSet{MaxDepth: 50}) tightens the depth of the untrusted preset. Set a field to NoBound, or any negative value, to remove its bound, or pass NoLimits to remove them all.
MaxDepth and MaxNumberDigits also apply when writing (the latter to numbers a MarshalJSON method returns); the other limits apply to reading.
func DefaultLimits ¶
func DefaultLimits() LimitSet
DefaultLimits returns the limits that apply when no limit option is given: generous bounds that real documents do not reach but that stop alias bombs and runaway nesting.
Example ¶
DefaultLimits applies when no limit option is given. UntrustedLimits is what the Untrusted option applies. A zero field means the bound is not applied.
package main
import (
"fmt"
oneyaml "oneyaml.dev/go"
)
func main() {
fmt.Printf("%+v\n", oneyaml.DefaultLimits())
fmt.Printf("%+v\n", oneyaml.UntrustedLimits())
}
Output: {MaxDepth:10000 MaxAliasNodes:1000000 MaxAliasBytes:67108864 MaxInputBytes:0 MaxScalarBytes:0 MaxDocuments:0 MaxNumberDigits:100000 MaxPatchWorkRatio:256} {MaxDepth:1000 MaxAliasNodes:100000 MaxAliasBytes:8388608 MaxInputBytes:67108864 MaxScalarBytes:16777216 MaxDocuments:10000 MaxNumberDigits:4300 MaxPatchWorkRatio:32}
func UntrustedLimits ¶
func UntrustedLimits() LimitSet
UntrustedLimits returns the tighter limits that the Untrusted option applies.
type Marshaler ¶
Marshaler is implemented by types that choose their own YAML form. The value MarshalYAML returns is written in place of the receiver; it may be any value Marshal accepts, including another Marshaler.
Example ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
type celsius float64
// MarshalYAML lets a type choose its own YAML form.
func (c celsius) MarshalYAML() (any, error) { return fmt.Sprintf("%.1fC", float64(c)), nil }
func main() {
out, err := oneyaml.Marshal(map[string]any{"temp": celsius(21.5)})
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}
Output: temp: 21.5C
type Node ¶
type Node struct {
// contains filtered or unexported fields
}
A Node is a read-only view of one node of a Document. It is a small value and is cheap to copy. A Node becomes stale when the Document is edited: Stale then reports true, methods that return an error report ErrStaleNode, and the others return zero values ("", 0, false, an invalid Node). Zero values are easy to write back by mistake, so to edit while reading, collect the pointers first (see Pointer) and edit afterwards, or read the nodes again after each edit.
Example ¶
A Node is a read-only view of the document: kind, style, text, anchors, tags, comments and position.
package main
import (
"fmt"
"log"
"strings"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("# the name\nname: api # service\nitems: [1, two]\n"))
if err != nil {
log.Fatal(err)
}
root := doc.Root()
name, _ := root.Lookup("name")
line, col := name.Position()
fmt.Println(name.Value(), name.Tag(), name.LineComment(), line, col)
fmt.Println(strings.TrimSpace(name.HeadComment()))
items, _ := root.Lookup("items")
fmt.Println(items.Style() == oneyaml.FlowStyle, items.Len(), items.Item(1).Value())
}
Output: api tag:yaml.org,2002:str # service 2 7 # the name true 2 two
func (Node) Decode ¶
Decode reads the node and everything below it into the value v points to, as Document.Decode does for a whole document. A *TypeError's Path is relative to the node. An alias inside the subtree is expanded even when its anchor lies outside it. A Node made stale by an edit reports an *EditError matching ErrStaleNode; the zero Node reports one matching ErrNotFound.
Example ¶
Decode on a node reads that part of the document; an alias inside it is expanded even when its anchor is defined outside.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("defaults: &d {retries: 3}\njob:\n settings: *d\n"))
if err != nil {
log.Fatal(err)
}
job, _ := doc.Root().Lookup("job")
var v map[string]map[string]int
if err := job.Decode(&v); err != nil {
log.Fatal(err)
}
fmt.Println(v)
}
Output: map[settings:map[retries:3]]
func (Node) Entries ¶
Entries returns the key and value of each entry of a mapping in order. It yields nothing for any other node.
The loop may edit the Document, as a loop over a map may change the map. After an edit the nodes already yielded are stale, but the loop goes on: it finds the mapping again by the pointer it had before the latest edit and yields the entry at the next position, as fresh nodes. An edit that inserts or removes items of a sequence on the way to this mapping moves the pointer with it. The loop ends if the mapping is replaced or removed, is no longer a mapping, or has no pointer (see Node.Pointer), and also when more than a few loops over one Document are running at once and this one is not among the first.
Entries added at the end during the loop are yielded, so a body that adds an entry on every pass never ends. Entries added or removed before the current one may make the loop skip entries or see some twice, as Go leaves open for a map: to add or remove while ranging, collect the pointers first (Node.Pointer), finish the loop, then edit.
Example (Edit) ¶
Ranging over a collection and setting each value as the loop goes.
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("images:\n web: nginx:1.25\n db: postgres:15\n"))
if err != nil {
log.Fatal(err)
}
imgs, ok := doc.Root().Lookup("images")
if !ok {
log.Fatal("no images")
}
for k, v := range imgs.Entries() {
// Each Set makes the nodes read so far stale, but the loop goes on with fresh ones.
if err := doc.Set("/images/"+k.Value(), v.Value()+"-patched"); err != nil {
log.Fatal(err)
}
}
fmt.Print(string(doc.Bytes()))
}
Output: images: web: nginx:1.25-patched db: postgres:15-patched
func (Node) Entry ¶
Entry returns the key and value of entry i of a mapping. Looping over i is linear in the length; Entries is the shorter way to write that loop.
func (Node) ExplicitTag ¶
ExplicitTag returns the tag written on the node, resolved: the handle is expanded by the %TAG directives of the node's document, "!<uri>" gives the uri, and ASCII percent escapes are decoded exactly as the parser does (non-ASCII escapes stay as written). The non-specific tag "!" is returned as "!". ok is false when no tag was written, which Tag cannot tell apart from a tag the Core schema supplies.
Example ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("%TAG !e! tag:example.com,2000:\n--- !e!point\nx: !!str 1\ny: 2\n"))
if err != nil {
log.Fatal(err)
}
root := doc.Root()
tag, ok := root.ExplicitTag()
fmt.Println(tag, ok, root.TagAsWritten())
x, _ := root.Lookup("x")
fmt.Println(x.ExplicitTag())
y, _ := root.Lookup("y")
// Tag gives the tag the Core schema assigns; ExplicitTag says none was written.
fmt.Println(y.Tag())
fmt.Println(y.ExplicitTag())
}
Output: tag:example.com,2000:point true !e!point tag:yaml.org,2002:str true tag:yaml.org,2002:int false
func (Node) HeadComment ¶
HeadComment returns the comment lines directly above the entry the node belongs to (no blank line between), each line as written, or "".
func (Node) IsValid ¶
IsValid reports whether n refers to a node of its document. It is false for the zero Node and for a stale Node.
func (Node) Item ¶
Item returns item i of a sequence, or an invalid Node. Looping over i is linear in the length; Items is the shorter way to write that loop.
func (Node) Items ¶
Items returns the items of a sequence in order. It yields nothing for any other node.
The loop may edit the Document, as a loop over a map may change the map. After an edit the nodes already yielded are stale, but the loop goes on: it finds the sequence again by the pointer it had before the latest edit and yields the item at the next position, as a fresh node. An edit that inserts or removes items of a sequence on the way to this one moves the pointer with it. The loop ends if the sequence is replaced or removed, is no longer a sequence, or has no pointer (see Node.Pointer), and also when more than a few loops over one Document are running at once and this one is not among the first.
Items appended during the loop are yielded, so a body that appends on every pass never ends. Items inserted or removed before the current one may make the loop skip items or see some twice, as Go leaves open for a map: to add or remove while ranging, collect the pointers first (Node.Pointer), finish the loop, then edit.
Example ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("- a\n- [b, c]\n- {d: e}\n"))
if err != nil {
log.Fatal(err)
}
for item := range doc.Root().Items() {
fmt.Println(item.Kind())
}
doc, err = oneyaml.Parse([]byte("x: 1\ny: [2]\n"))
if err != nil {
log.Fatal(err)
}
for k, v := range doc.Root().Entries() {
fmt.Println(k.Value(), v.Kind())
}
}
Output: scalar sequence mapping x scalar y sequence
func (Node) LineComment ¶
LineComment returns the comment that follows the node on its line, including the "#", or "".
func (Node) Lookup ¶
Lookup returns the value of the first entry of a mapping whose key has the scalar text key. Aliases are not followed.
func (Node) Pointer ¶
Pointer returns the RFC 6901 pointer of the node within its own document, ready for Document.Set, Get or Remove. It is how to edit while walking: collect the pointers of the nodes you want, finish the loop, then edit (an edit makes every Node read earlier stale).
Pointer returns an error matching ErrStaleNode when the Document has been edited since the node was read, and one matching ErrNotFound for the zero Node. It returns an error matching ErrNoPointer, and never a guess, when no pointer addresses the node: a mapping key (simple or complex), anything inside a complex key, or a node whose pointer would lead to a different node (the key occurs twice in its mapping). An alias node has the pointer of the alias itself; Target gives the node it refers to, whose pointer is the place of the anchor, not a path through the alias.
Example ¶
Collect the pointers of the images, then edit them.
package main
import (
"errors"
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
doc, err := oneyaml.Parse([]byte("images:\n web: nginx:1.25\n db: postgres:15\n"))
if err != nil {
log.Fatal(err)
}
imgs, ok := doc.Root().Lookup("images")
if !ok {
log.Fatal("no images")
}
// First collect where each value is, then edit. This works whatever the
// edits do, including adding or removing entries.
var ptrs, vals []string
for _, v := range imgs.Entries() {
p, err := v.Pointer()
if err != nil {
log.Fatal(err)
}
ptrs = append(ptrs, p)
vals = append(vals, v.Value())
}
// Then edit by pointer.
for i, p := range ptrs {
if err := doc.Set(p, vals[i]+"-patched"); err != nil {
log.Fatal(err)
}
}
fmt.Print(string(doc.Bytes()))
// Nodes read before the edits are stale now.
_, err = imgs.Pointer()
fmt.Println(imgs.Stale(), errors.Is(err, oneyaml.ErrStaleNode))
}
Output: images: web: nginx:1.25-patched db: postgres:15-patched true true
func (Node) Position ¶
Position returns the 1-based line and column of the start of the node, counted in characters.
func (Node) Span ¶
Span returns the byte offsets of the node's text in Document.Bytes, without its anchor and tag. The offsets refer to the UTF-8 text of the stream.
func (Node) Stale ¶
Stale reports whether n came from a Document that has been edited since n was read. A stale Node is not valid: its methods return zero values (or ErrStaleNode where they return an error). The zero Node is not stale, only invalid.
func (Node) Tag ¶
Tag returns the resolved tag of the node: the explicit tag with its handle expanded by the %TAG directives of the node's document, or the tag the Core schema gives the node ("tag:yaml.org,2002:str", ...). The non-specific tag "!" gives a scalar the string tag and a collection its default tag, as the specification says (section 6.9.1); the decoder reads the node the same way. An alias has no tag of its own and returns "".
func (Node) TagAsWritten ¶
TagAsWritten returns the tag exactly as it appears in the text, such as "!!str", "!e!foo" or "!<tag:x,2000:y>", or "" when the node has no tag.
type NodeKind ¶
type NodeKind uint8
NodeKind is the kind of a Node.
Node kinds. The zero value marks an invalid Node.
type Option ¶
type Option func(*config)
Option configures Marshal, Unmarshal, an Encoder, a Decoder or Parse. One option type is shared by every entry point; an option that does not apply to the call it is passed to is ignored.
func Indent ¶
Indent sets how many spaces each nesting level of block output is indented by. The default is 2. Values below 1 select the default and values above 9 are treated as 9.
Example ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
func main() {
out, err := oneyaml.Marshal(map[string]any{"a": map[string]any{"b": []any{1, 2}}}, oneyaml.Indent(4))
if err != nil {
log.Fatal(err)
}
fmt.Print(string(out))
}
Output: a: b: - 1 - 2
func KnownFields ¶
func KnownFields() Option
KnownFields makes reading into a struct fail on a mapping key that matches no field. It applies to reads and is ignored when writing.
Example ¶
var cfg struct {
Name string `yaml:"name"`
}
err := oneyaml.Unmarshal([]byte("name: api\nnmae: typo\n"), &cfg, oneyaml.KnownFields())
var te *oneyaml.TypeError
if errors.As(err, &te) {
fmt.Println(te.Path, te.Line, te.Column)
}
Output: /nmae 2 1
func Limits ¶
Limits sets the bounds that l names and leaves the others as the options before it set them: the defaults if there are none, the bounds of Untrusted after Untrusted, no bounds after NoLimits. So Untrusted(), Limits(LimitSet{MaxDepth: 50}) keeps every untrusted bound and tightens the depth, and NoLimits(), Limits(LimitSet{MaxInputBytes: 1 << 20}) bounds the input and nothing else.
A positive field sets that bound; NoBound removes it. A bound you set applies exactly as given, while an alias bound you leave alone keeps the behaviour it had: the default and Untrusted ones still grow with the size of the document. Limits(LimitSet{}) changes nothing. When writing, MaxDepth and MaxNumberDigits are used.
Example ¶
bomb := "a: &a [x, x, x, x]\nb: &b [*a, *a, *a, *a]\nc: [*b, *b, *b, *b]\n"
var v any
err := oneyaml.Unmarshal([]byte(bomb), &v, oneyaml.Limits(oneyaml.LimitSet{MaxAliasNodes: 20}))
var le *oneyaml.LimitError
if errors.As(err, &le) {
fmt.Println(le.Limit, le.Max)
}
Output: alias expansion nodes 20
Example (AddBound) ¶
A LimitSet passed to Limits changes only the fields that are set: here the input size gains a bound, and the default depth bound stays.
package main
import (
"errors"
"fmt"
"strings"
oneyaml "oneyaml.dev/go"
)
func main() {
opt := oneyaml.Limits(oneyaml.LimitSet{MaxInputBytes: 1 << 20})
large := []byte("a: " + strings.Repeat("x", 2<<20) + "\n")
deep := []byte(strings.Repeat("[", 10001) + strings.Repeat("]", 10001))
var v any
var le *oneyaml.LimitError
err := oneyaml.Unmarshal(large, &v, opt)
if errors.As(err, &le) {
fmt.Println(le.Limit, le.Max)
}
err = oneyaml.Unmarshal(deep, &v, opt)
if errors.As(err, &le) {
fmt.Println(le.Limit, le.Max)
}
}
Output: input size 1048576 depth 10000
func MergeKeys ¶
func MergeKeys() Option
MergeKeys makes reading treat the "<<" key as a YAML merge key instead of an ordinary key. It applies to reads and is ignored when writing.
Example ¶
src := []byte("base: &base {x: 1, y: 2}\nchild:\n <<: *base\n y: 3\n")
var v map[string]map[string]int
err := oneyaml.Unmarshal(src, &v, oneyaml.MergeKeys())
fmt.Println(v["child"], err)
Output: map[x:1 y:3] <nil>
func NoLimits ¶
func NoLimits() Option
NoLimits removes every limit, including the depth limit when writing. Only use it for input you trust. A Limits option after it adds back just the bounds it names.
Example ¶
NoLimits removes every bound, including the depth limit. Use it only for input you trust.
package main
import (
"errors"
"fmt"
"strings"
oneyaml "oneyaml.dev/go"
)
func main() {
deep := strings.Repeat("[", 10001) + strings.Repeat("]", 10001)
var v any
err := oneyaml.Unmarshal([]byte(deep), &v)
var le *oneyaml.LimitError
fmt.Println(errors.As(err, &le))
fmt.Println(oneyaml.Unmarshal([]byte(deep), &v, oneyaml.NoLimits()))
}
Output: true <nil>
func Untrusted ¶
func Untrusted() Option
Untrusted selects UntrustedLimits, for input from a source you do not control. It replaces any limits set before it. Options after it can tighten or lift single bounds: Untrusted(), Limits(LimitSet{MaxDepth: 50}).
Example ¶
Untrusted tightens every limit, for input from a source you do not control. A limit that is hit is a *LimitError that names the option which raises it.
package main
import (
"errors"
"fmt"
"strings"
oneyaml "oneyaml.dev/go"
)
func main() {
deep := strings.Repeat("[", 2000) + strings.Repeat("]", 2000)
var v any
// The default depth limit lets this through; Untrusted does not.
fmt.Println(oneyaml.Unmarshal([]byte(deep), &v))
err := oneyaml.Unmarshal([]byte(deep), &v, oneyaml.Untrusted())
var le *oneyaml.LimitError
if errors.As(err, &le) {
fmt.Println(le.Limit, le.Max, le.Option)
}
}
Output: <nil> depth 1000 Limits
Example (Tighten) ¶
A Limits option after Untrusted changes only the bounds it names: here depth is tightened, and the untrusted bound on the digits of a number still holds.
package main
import (
"errors"
"fmt"
"strings"
oneyaml "oneyaml.dev/go"
)
func main() {
opts := []oneyaml.Option{oneyaml.Untrusted(), oneyaml.Limits(oneyaml.LimitSet{MaxDepth: 50})}
deep := strings.Repeat("[", 100) + strings.Repeat("]", 100)
long := strings.Repeat("9", 5000)
var v any
var le *oneyaml.LimitError
for _, in := range []string{deep, long} {
err := oneyaml.Unmarshal([]byte(in), &v, opts...)
if errors.As(err, &le) {
fmt.Println(le.Limit, le.Max)
}
}
}
Output: depth 50 number digits 4300
type PatchOp ¶
type PatchOp struct {
// Op is "add", "remove", "replace", "move", "copy" or "test".
Op string
// Path is the RFC 6901 pointer the operation works on.
Path string
// From is the source pointer of "move" and "copy".
From string
// Value is the operand of "add", "replace" and "test": anything Marshal
// accepts, with nil meaning null.
Value any
}
A PatchOp is one RFC 6902 operation for Apply.
func PatchReplace ¶
PatchReplace returns a "replace" operation.
type SyntaxError ¶
type SyntaxError struct {
// Line and Column are 1-based, counted in characters; Offset is the byte offset.
Line, Column, Offset int
// Category is a short stable name for the kind of error.
Category string
// Msg describes the problem.
Msg string
}
SyntaxError reports YAML that is not well formed.
Example ¶
A *SyntaxError carries the position and a stable category.
package main
import (
"errors"
"fmt"
oneyaml "oneyaml.dev/go"
)
func main() {
err := oneyaml.Unmarshal([]byte("a: 1\na: 2\n"), new(any))
var se *oneyaml.SyntaxError
if errors.As(err, &se) {
fmt.Println(se.Category, se.Line, se.Column)
}
}
Output: duplicate-key 2 1
func (*SyntaxError) Error ¶
func (e *SyntaxError) Error() string
Error returns the message with its line and column.
type TagDirective ¶
type TagDirective struct {
// Handle is the tag handle as written, such as "!" or "!e!".
Handle string
// Prefix is the tag prefix as written.
Prefix string
}
A TagDirective is a %TAG directive: tags written with Handle in the document stand for Prefix followed by the rest of the tag.
type TypeError ¶
type TypeError struct {
// Path is the RFC 6901 pointer to the value, empty for the root.
Path string
// Line and Column locate the value in the input; zero when writing.
Line, Column int
// GoType is the Go type involved.
GoType string
// Msg describes the problem.
Msg string
// Err is the error a MarshalYAML or MarshalText method returned, if that was the cause.
Err error
}
TypeError reports a value that cannot be converted: a YAML value that does not fit its Go target when reading, or a Go value with no YAML form when writing.
type Unmarshaler ¶
Unmarshaler is implemented by types that read their own YAML. Unmarshal, Decoder.Decode, Document.Decode and Node.Decode call UnmarshalYAML with a view of the node that holds the value, instead of decoding into the type's fields.
The Node is read-only. Node.Decode on it decodes the subtree into any target, with the same rules as Unmarshal. When the value comes from Unmarshal or a Decoder there is no Document, so the Node covers only that subtree and is valid only during the call; aliases inside it still resolve. A null value leaves a pointer target nil without calling the method. An error the method returns comes back from the decode as a *TypeError whose Unwrap gives it.
Example ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
)
// Port reads itself from a node: a bare number, or a mapping with a number.
type Port int
func (p *Port) UnmarshalYAML(n oneyaml.Node) error {
if n.Kind() == oneyaml.MappingNode {
var m struct {
Number int `yaml:"number"`
}
if err := n.Decode(&m); err != nil {
return err
}
*p = Port(m.Number)
return nil
}
var v int
if err := n.Decode(&v); err != nil {
return err
}
*p = Port(v)
return nil
}
func main() {
var cfg struct {
Ports []Port `yaml:"ports"`
}
if err := oneyaml.Unmarshal([]byte("ports: [80, {number: 443}]\n"), &cfg); err != nil {
log.Fatal(err)
}
fmt.Println(cfg.Ports)
}
Output: [80 443]
Example (FromYAMLv3) ¶
package main
import (
"fmt"
"log"
oneyaml "oneyaml.dev/go"
yaml "oneyaml.dev/go/compat/yamlv3"
)
// legacySize was written for yaml.v3: its UnmarshalYAML takes a *yaml.Node.
// The oneyaml package does not call that form; it reports it, so the type is
// never filled by its fields without a word.
type legacySize struct{ Bytes int }
func (s *legacySize) UnmarshalYAML(n *yaml.Node) error { return n.Decode(&s.Bytes) }
// Size is the same type moved to the oneyaml package: the method takes a
// oneyaml.Node, which has the same Decode method.
type Size struct{ Bytes int }
func (s *Size) UnmarshalYAML(n oneyaml.Node) error { return n.Decode(&s.Bytes) }
func main() {
src := []byte("limit: 512\n")
var before struct {
Limit legacySize `yaml:"limit"`
}
err := oneyaml.Unmarshal(src, &before)
fmt.Println(err)
var after struct {
Limit Size `yaml:"limit"`
}
if err := oneyaml.Unmarshal(src, &after); err != nil {
log.Fatal(err)
}
fmt.Println(after.Limit.Bytes)
}
Output: oneyaml: line 1, column 8: /limit: oneyaml_test.legacySize has an UnmarshalYAML method that is neither UnmarshalYAML(oneyaml.Node) error nor UnmarshalYAML(func(any) error) error; change its signature, or remove it to fill the value by its fields 512
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
compat
|
|
|
k8syaml
Package yaml is a compatibility adapter for sigs.k8s.io/yaml (and its parent ghodss/yaml) built on oneyaml.
|
Package yaml is a compatibility adapter for sigs.k8s.io/yaml (and its parent ghodss/yaml) built on oneyaml. |
|
yamlv3
Package yaml is a compatibility adapter for gopkg.in/yaml.v3 (go.yaml.in/yaml/v3) built on the oneyaml parser and binder.
|
Package yaml is a compatibility adapter for gopkg.in/yaml.v3 (go.yaml.in/yaml/v3) built on the oneyaml parser and binder. |
|
internal
|
|
|
bind
Package bind decodes a stream of parse events into Go values.
|
Package bind decodes a stream of parse events into Go values. |
|
compatopt
Package compatopt carries the hidden settings between the root package and the compat packages.
|
Package compatopt carries the hidden settings between the root package and the compat packages. |
|
cst
Package cst builds a flat, lossless tree from parse events.
|
Package cst builds a flat, lossless tree from parse events. |
|
edit
Package edit plans minimal-diff edits of a lossless tree.
|
Package edit plans minimal-diff edits of a lossless tree. |
|
emit
Package emit writes Go values as YAML text.
|
Package emit writes Go values as YAML text. |
|
encoding
Package encoding detects and converts the character encodings a YAML stream may use: UTF-8, UTF-16 and UTF-32, each in both byte orders.
|
Package encoding detects and converts the character encodings a YAML stream may use: UTF-8, UTF-16 and UTF-32, each in both byte orders. |
|
kitcase
Package kitcase reads the parts of a shared conformance case directory that every test runner needs in the same way: the input bytes, which may be stored in a file or described by a recipe, and the "slow" tag.
|
Package kitcase reads the parts of a shared conformance case directory that every test runner needs in the same way: the input bytes, which may be stored in a file or described by a recipe, and the "slow" tag. |
|
parse
Package parse turns scan tokens into YAML events with a pull API.
|
Package parse turns scan tokens into YAML events with a pull API. |
|
patch
Package patch holds the parts of RFC 6902 and RFC 7396 patches that do not depend on the document: reading a JSON patch, comparing a node with a patch value under the data model, and copying a value so that it can be written elsewhere in the same document.
|
Package patch holds the parts of RFC 6902 and RFC 7396 patches that do not depend on the document: reading a JSON patch, comparing a node with a patch value under the data model, and copying a value so that it can be written elsewhere in the same document. |
|
pointer
Package pointer parses RFC 6901 JSON Pointers.
|
Package pointer parses RFC 6901 JSON Pointers. |
|
resolve
Package resolve decides what a plain scalar means.
|
Package resolve decides what a plain scalar means. |
|
scan
Package scan is a lossless YAML 1.2 scanner.
|
Package scan is a lossless YAML 1.2 scanner. |
|
typeinfo
Package typeinfo builds and caches, per Go type, what the encoder and the decoder need to know about it: struct fields with their YAML names and options, embedded and inline fields, and which hooks the type implements.
|
Package typeinfo builds and caches, per Go type, what the encoder and the decoder need to know about it: struct fields with their YAML names and options, embedded and inline fields, and which hooks the type implements. |