470 lines
14 KiB
Go
470 lines
14 KiB
Go
// Copyright 2011-2019 Canonical Ltd
|
|
// Copyright 2025 The go-yaml Project Contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// Node types and constants for YAML tree representation.
|
|
// Defines Kind, Style, and Node structure for intermediate YAML representation.
|
|
|
|
package libyaml
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
// Tag constants for YAML types
|
|
const (
|
|
nullTag = "!!null"
|
|
boolTag = "!!bool"
|
|
strTag = "!!str"
|
|
intTag = "!!int"
|
|
floatTag = "!!float"
|
|
timestampTag = "!!timestamp"
|
|
seqTag = "!!seq"
|
|
mapTag = "!!map"
|
|
binaryTag = "!!binary"
|
|
mergeTag = "!!merge"
|
|
)
|
|
|
|
// longTagPrefix is the standard YAML tag prefix for core types.
|
|
const longTagPrefix = "tag:yaml.org,2002:"
|
|
|
|
// longTags maps short tags to their long form representations.
|
|
// shortTags maps long tags to their short form representations.
|
|
var (
|
|
longTags = make(map[string]string)
|
|
shortTags = make(map[string]string)
|
|
)
|
|
|
|
// init initializes the tag conversion maps.
|
|
func init() {
|
|
for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} {
|
|
ltag := longTag(stag)
|
|
longTags[stag] = ltag
|
|
shortTags[ltag] = stag
|
|
}
|
|
}
|
|
|
|
// shortTag converts a long-form tag to its short form (e.g., "tag:yaml.org,2002:str" to "!!str").
|
|
func shortTag(tag string) string {
|
|
if strings.HasPrefix(tag, longTagPrefix) {
|
|
if stag, ok := shortTags[tag]; ok {
|
|
return stag
|
|
}
|
|
return "!!" + tag[len(longTagPrefix):]
|
|
}
|
|
return tag
|
|
}
|
|
|
|
// longTag converts a short-form tag to its long form (e.g., "!!str" to "tag:yaml.org,2002:str").
|
|
func longTag(tag string) string {
|
|
if strings.HasPrefix(tag, "!!") {
|
|
if ltag, ok := longTags[tag]; ok {
|
|
return ltag
|
|
}
|
|
return longTagPrefix + tag[2:]
|
|
}
|
|
return tag
|
|
}
|
|
|
|
// Kind represents the type of YAML node
|
|
type Kind uint32
|
|
|
|
// Kind constants define the different types of YAML nodes.
|
|
const (
|
|
DocumentNode Kind = 1 << iota
|
|
SequenceNode
|
|
MappingNode
|
|
ScalarNode
|
|
AliasNode
|
|
StreamNode
|
|
)
|
|
|
|
// Style represents the formatting style of a YAML node
|
|
type Style uint32
|
|
|
|
// Style constants define different formatting styles for YAML nodes.
|
|
const (
|
|
TaggedStyle Style = 1 << iota
|
|
DoubleQuotedStyle
|
|
SingleQuotedStyle
|
|
LiteralStyle
|
|
FoldedStyle
|
|
FlowStyle
|
|
)
|
|
|
|
// StreamVersionDirective represents a YAML %YAML version directive for stream nodes.
|
|
type StreamVersionDirective struct {
|
|
Major int
|
|
Minor int
|
|
}
|
|
|
|
// StreamTagDirective represents a YAML %TAG directive for stream nodes.
|
|
type StreamTagDirective struct {
|
|
Handle string
|
|
Prefix string
|
|
}
|
|
|
|
// Stream holds stream-level metadata for StreamNode.
|
|
// This includes encoding, version directive, and tag directives.
|
|
type Stream struct {
|
|
Encoding Encoding
|
|
Version *StreamVersionDirective
|
|
TagDirectives []StreamTagDirective
|
|
}
|
|
|
|
// Node represents an element in the YAML document hierarchy. While documents
|
|
// are typically encoded and decoded into higher level types, such as structs
|
|
// and maps, Node is an intermediate representation that allows detailed
|
|
// control over the content being decoded or encoded.
|
|
//
|
|
// It's worth noting that although Node offers access into details such as
|
|
// line numbers, columns, and comments, the content when re-encoded will not
|
|
// have its original textual representation preserved. An effort is made to
|
|
// render the data pleasantly, and to preserve comments near the data they
|
|
// describe, though.
|
|
//
|
|
// Values that make use of the Node type interact with the yaml package in the
|
|
// same way any other type would do, by encoding and decoding yaml data
|
|
// directly or indirectly into them.
|
|
//
|
|
// For example:
|
|
//
|
|
// var person struct {
|
|
// Name string
|
|
// Address yaml.Node
|
|
// }
|
|
// err := yaml.Unmarshal(data, &person)
|
|
//
|
|
// Or by itself:
|
|
//
|
|
// var person Node
|
|
// err := yaml.Unmarshal(data, &person)
|
|
type Node struct {
|
|
// Kind defines whether the node is a document, a mapping, a sequence,
|
|
// a scalar value, or an alias to another node. The specific data type of
|
|
// scalar nodes may be obtained via the ShortTag and LongTag methods.
|
|
Kind Kind
|
|
|
|
// Style allows customizing the appearance of the node in the tree.
|
|
Style Style
|
|
|
|
// Tag holds the YAML tag defining the data type for the value.
|
|
// When decoding, this field will always be set to the resolved tag,
|
|
// even when it wasn't explicitly provided in the YAML content.
|
|
// When encoding, if this field is unset the value type will be
|
|
// implied from the node properties, and if it is set, it will only
|
|
// be serialized into the representation if TaggedStyle is used or
|
|
// the implicit tag diverges from the provided one.
|
|
Tag string
|
|
|
|
// Value holds the unescaped and unquoted representation of the value.
|
|
Value string
|
|
|
|
// Anchor holds the anchor name for this node, which allows aliases to point to it.
|
|
Anchor string
|
|
|
|
// Alias holds the node that this alias points to. Only valid when Kind is AliasNode.
|
|
Alias *Node
|
|
|
|
// Content holds contained nodes for documents, mappings, and sequences.
|
|
Content []*Node
|
|
|
|
// HeadComment holds any comments in the lines preceding the node and
|
|
// not separated by an empty line.
|
|
HeadComment string
|
|
|
|
// LineComment holds any comments at the end of the line where the node is in.
|
|
LineComment string
|
|
|
|
// FootComment holds any comments following the node and before empty lines.
|
|
FootComment string
|
|
|
|
// Line and Column hold the node position in the decoded YAML text.
|
|
// These fields are not respected when encoding the node.
|
|
Line int
|
|
Column int
|
|
|
|
// Stream holds stream metadata (non-nil only when Kind == StreamNode).
|
|
Stream *Stream
|
|
}
|
|
|
|
// IsZero returns whether the node has all of its fields unset.
|
|
func (n *Node) IsZero() bool {
|
|
return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil &&
|
|
n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 &&
|
|
n.Stream == nil
|
|
}
|
|
|
|
// LongTag returns the long form of the tag that indicates the data type for
|
|
// the node. If the Tag field isn't explicitly defined, one will be computed
|
|
// based on the node properties.
|
|
func (n *Node) LongTag() string {
|
|
return longTag(n.ShortTag())
|
|
}
|
|
|
|
// ShortTag returns the short form of the YAML tag that indicates data type for
|
|
// the node. If the Tag field isn't explicitly defined, one will be computed
|
|
// based on the node properties.
|
|
func (n *Node) ShortTag() string {
|
|
if n.indicatedString() {
|
|
return strTag
|
|
}
|
|
if n.Tag == "" || n.Tag == "!" {
|
|
switch n.Kind {
|
|
case MappingNode:
|
|
return mapTag
|
|
case SequenceNode:
|
|
return seqTag
|
|
case AliasNode:
|
|
if n.Alias != nil {
|
|
return n.Alias.ShortTag()
|
|
}
|
|
case ScalarNode:
|
|
return strTag
|
|
case 0:
|
|
// Special case to make the zero value convenient.
|
|
if n.IsZero() {
|
|
return nullTag
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
return shortTag(n.Tag)
|
|
}
|
|
|
|
// indicatedString returns true if the node's style explicitly indicates a string type.
|
|
func (n *Node) indicatedString() bool {
|
|
return n.Kind == ScalarNode &&
|
|
(shortTag(n.Tag) == strTag ||
|
|
(n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0)
|
|
}
|
|
|
|
// shouldUseLiteralStyle determines if a string should use literal style.
|
|
// It returns true if the string contains newlines AND meets additional criteria:
|
|
// - is at least 2 characters long
|
|
// - contains at least one non-whitespace character
|
|
func shouldUseLiteralStyle(s string) bool {
|
|
if !strings.Contains(s, "\n") || len(s) < 2 {
|
|
return false
|
|
}
|
|
// Must contain at least one non-whitespace character
|
|
for _, r := range s {
|
|
if !unicode.IsSpace(r) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SetString is a convenience function that sets the node to a string value
|
|
// and defines its style in a pleasant way depending on its content.
|
|
func (n *Node) SetString(s string) {
|
|
n.Kind = ScalarNode
|
|
if utf8.ValidString(s) {
|
|
n.Value = s
|
|
n.Tag = strTag
|
|
} else {
|
|
n.Value = encodeBase64(s)
|
|
n.Tag = binaryTag
|
|
}
|
|
if shouldUseLiteralStyle(n.Value) {
|
|
n.Style = LiteralStyle
|
|
}
|
|
}
|
|
|
|
// Decode decodes the node and stores its data into the value pointed to by v.
|
|
//
|
|
// See the documentation for Unmarshal for details about the
|
|
// conversion of YAML into a Go value.
|
|
func (n *Node) Decode(v any) (err error) {
|
|
d := NewConstructor(DefaultOptions)
|
|
defer handleErr(&err)
|
|
out := reflect.ValueOf(v)
|
|
if out.Kind() == reflect.Pointer && !out.IsNil() {
|
|
out = out.Elem()
|
|
}
|
|
d.Construct(n, out)
|
|
if len(d.TypeErrors) > 0 {
|
|
return &LoadErrors{Errors: d.TypeErrors}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Load decodes the node and stores its data into the value pointed to by v,
|
|
// applying the given options.
|
|
//
|
|
// This method is useful when you need to preserve options like WithKnownFields()
|
|
// inside custom UnmarshalYAML implementations.
|
|
//
|
|
// Maps and pointers (to a struct, string, int, etc) are accepted as v
|
|
// values. If an internal pointer within a struct is not initialized,
|
|
// the yaml package will initialize it if necessary. The v parameter
|
|
// must not be nil.
|
|
//
|
|
// See the documentation of the package-level Load function for details
|
|
// about YAML to Go conversion and tag options.
|
|
func (n *Node) Load(v any, opts ...Option) (err error) {
|
|
defer handleErr(&err)
|
|
o, err := ApplyOptions(opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d := NewConstructor(o)
|
|
out := reflect.ValueOf(v)
|
|
if out.Kind() == reflect.Pointer && !out.IsNil() {
|
|
out = out.Elem()
|
|
}
|
|
d.Construct(n, out)
|
|
if len(d.TypeErrors) > 0 {
|
|
return &LoadErrors{Errors: d.TypeErrors}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Encode encodes value v and stores its representation in n.
|
|
//
|
|
// See the documentation for Marshal for details about the
|
|
// conversion of Go values into YAML.
|
|
func (n *Node) Encode(v any) (err error) {
|
|
defer handleErr(&err)
|
|
// Use the 3-stage dump pipeline with round-trip to preserve styles
|
|
r := NewRepresenter(DefaultOptions)
|
|
node := r.Represent("", reflect.ValueOf(v))
|
|
d := NewDesolver(DefaultOptions)
|
|
d.Desolve(node)
|
|
s := NewSerializer(nil, DefaultOptions)
|
|
var out []byte
|
|
s.Emitter.SetOutputString(&out)
|
|
s.Serialize(node)
|
|
s.Finish()
|
|
// Parse back to get styles
|
|
p := NewComposer(out, nil)
|
|
p.Textless = true
|
|
defer p.Destroy()
|
|
doc := p.Compose()
|
|
NewResolver(nil).Resolve(doc)
|
|
*n = *doc.Content[0]
|
|
return nil
|
|
}
|
|
|
|
// Dump encodes value v and stores its representation in n,
|
|
// applying the given options.
|
|
//
|
|
// This method is useful when you need to apply specific encoding options
|
|
// while building Node trees programmatically.
|
|
//
|
|
// See the documentation for Marshal for details about the
|
|
// conversion of Go values into YAML.
|
|
func (n *Node) Dump(v any, opts ...Option) (err error) {
|
|
defer handleErr(&err)
|
|
o, err := ApplyOptions(opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Use the 3-stage dump pipeline with round-trip to preserve styles
|
|
r := NewRepresenter(o)
|
|
node := r.Represent("", reflect.ValueOf(v))
|
|
d := NewDesolver(o)
|
|
d.Desolve(node)
|
|
s := NewSerializer(nil, o)
|
|
var out []byte
|
|
s.Emitter.SetOutputString(&out)
|
|
s.Serialize(node)
|
|
s.Finish()
|
|
// Parse back to get styles
|
|
p := NewComposer(out, nil)
|
|
p.Textless = true
|
|
defer p.Destroy()
|
|
doc := p.Compose()
|
|
NewResolver(nil).Resolve(doc)
|
|
*n = *doc.Content[0]
|
|
return nil
|
|
}
|
|
|
|
// Marshaler interface may be implemented by types to customize their
|
|
// behavior when being marshaled into a YAML document.
|
|
type Marshaler interface {
|
|
MarshalYAML() (any, error)
|
|
}
|
|
|
|
// Unmarshaler is the interface implemented by types that can unmarshal
|
|
// a YAML description of themselves.
|
|
type Unmarshaler interface {
|
|
UnmarshalYAML(node *Node) error
|
|
}
|
|
|
|
// IsZeroer is used to check whether an object is zero to determine whether
|
|
// it should be omitted when marshaling with the ,omitempty flag. One notable
|
|
// implementation is [time.Time].
|
|
type IsZeroer interface {
|
|
IsZero() bool
|
|
}
|
|
|
|
// FromYAMLNode is a new interface that types can implement to customize
|
|
// their unmarshaling behavior. It receives a Node directly and modifies
|
|
// the receiver in place.
|
|
// This is the preferred interface for new code.
|
|
//
|
|
// Note: This interface is reserved for the v4 API and is not yet fully
|
|
// integrated into the current implementation.
|
|
type FromYAMLNode interface {
|
|
FromYAMLNode(*Node) error
|
|
}
|
|
|
|
// ToYAMLNode is a new interface that types can implement to customize
|
|
// their marshaling behavior. It returns a Node directly.
|
|
// This is the preferred interface for new code.
|
|
//
|
|
// Note: This interface is reserved for the v4 API and is not yet fully
|
|
// integrated into the current implementation.
|
|
type ToYAMLNode interface {
|
|
ToYAMLNode() (*Node, error)
|
|
}
|
|
|
|
// isZero reports whether v represents the zero value for its type.
|
|
// If v implements the IsZeroer interface, IsZero() is called.
|
|
// Otherwise, zero is determined by checking type-specific conditions.
|
|
// This is used to determine omitempty behavior when marshaling.
|
|
func isZero(v reflect.Value) bool {
|
|
kind := v.Kind()
|
|
if z, ok := v.Interface().(IsZeroer); ok {
|
|
if (kind == reflect.Pointer || kind == reflect.Interface) && v.IsNil() {
|
|
return true
|
|
}
|
|
return z.IsZero()
|
|
}
|
|
switch kind {
|
|
case reflect.String:
|
|
return len(v.String()) == 0
|
|
case reflect.Interface, reflect.Pointer:
|
|
return v.IsNil()
|
|
case reflect.Slice:
|
|
return v.Len() == 0
|
|
case reflect.Map:
|
|
return v.Len() == 0
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
return v.Int() == 0
|
|
case reflect.Float32, reflect.Float64:
|
|
return v.Float() == 0
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
|
return v.Uint() == 0
|
|
case reflect.Bool:
|
|
return !v.Bool()
|
|
case reflect.Struct:
|
|
vt := v.Type()
|
|
for i := v.NumField() - 1; i >= 0; i-- {
|
|
if vt.Field(i).PkgPath != "" {
|
|
continue // Private field
|
|
}
|
|
if !isZero(v.Field(i)) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|