232 lines
6.6 KiB
Go
232 lines
6.6 KiB
Go
// Copyright 2025 The go-yaml Project Contributors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
// This file contains the Loader API for reading YAML documents.
|
|
//
|
|
// Primary functions:
|
|
// - Load: Decode YAML document(s) into a value (use WithAll for multi-doc)
|
|
// - NewLoader: Create a streaming loader from io.Reader
|
|
|
|
package yaml
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"reflect"
|
|
|
|
"go.yaml.in/yaml/v4/internal/libyaml"
|
|
)
|
|
|
|
// Load decodes YAML document(s) with the given options.
|
|
//
|
|
// By default, Load requires exactly one document in the input.
|
|
// If zero documents are found, it returns an error.
|
|
// If multiple documents are found, it returns an error.
|
|
//
|
|
// Use WithAllDocuments() to load all documents into a slice:
|
|
//
|
|
// var configs []Config
|
|
// yaml.Load(multiDocYAML, &configs, yaml.WithAllDocuments())
|
|
//
|
|
// When WithAllDocuments is used, out must be a pointer to a slice.
|
|
// Each document is decoded into the slice element type.
|
|
// Zero documents results in an empty slice (no error).
|
|
//
|
|
// Maps and pointers (to a struct, string, int, etc) are accepted as out
|
|
// values. If an internal pointer within a struct is not initialized,
|
|
// the yaml package will initialize it if necessary. The out parameter
|
|
// must not be nil.
|
|
//
|
|
// The type of the decoded values should be compatible with the respective
|
|
// values in out. If one or more values cannot be decoded due to type
|
|
// mismatches, decoding continues partially until the end of the YAML
|
|
// content, and a *yaml.LoadErrors is returned with details for all
|
|
// missed values.
|
|
//
|
|
// Struct fields are only loaded if they are exported (have an upper case
|
|
// first letter), and are loaded using the field name lowercased as the
|
|
// default key. Custom keys may be defined via the "yaml" name in the field
|
|
// tag: the content preceding the first comma is used as the key, and the
|
|
// following comma-separated options control the loading and dumping behavior.
|
|
//
|
|
// For example:
|
|
//
|
|
// type T struct {
|
|
// F int `yaml:"a,omitempty"`
|
|
// B int
|
|
// }
|
|
// var t T
|
|
// yaml.Load([]byte("a: 1\nb: 2"), &t)
|
|
//
|
|
// See the documentation of Dump for the format of tags and a list of
|
|
// supported tag options.
|
|
func Load(in []byte, out any, opts ...Option) error {
|
|
o, err := libyaml.ApplyOptions(opts...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if o.AllDocuments {
|
|
// Multi-document mode: out must be pointer to slice
|
|
return loadAll(in, out, o)
|
|
}
|
|
|
|
// Single-document mode: exactly one document required
|
|
return loadSingle(in, out, o)
|
|
}
|
|
|
|
// loadAll loads all documents into a slice
|
|
func loadAll(in []byte, out any, opts *libyaml.Options) error {
|
|
outVal := reflect.ValueOf(out)
|
|
if outVal.Kind() != reflect.Pointer || outVal.IsNil() {
|
|
return &LoadErrors{Errors: []*libyaml.ConstructError{{
|
|
Err: errors.New("yaml: WithAllDocuments requires a non-nil pointer to a slice"),
|
|
}}}
|
|
}
|
|
|
|
sliceVal := outVal.Elem()
|
|
if sliceVal.Kind() != reflect.Slice {
|
|
return &LoadErrors{Errors: []*libyaml.ConstructError{{
|
|
Err: errors.New("yaml: WithAllDocuments requires a pointer to a slice"),
|
|
}}}
|
|
}
|
|
|
|
// Create a new slice (clear existing content)
|
|
sliceVal.Set(reflect.MakeSlice(sliceVal.Type(), 0, 0))
|
|
|
|
l, err := NewLoader(bytes.NewReader(in), func(o *libyaml.Options) error {
|
|
*o = *opts // Copy options
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
elemType := sliceVal.Type().Elem()
|
|
for {
|
|
// Create new element of slice's element type
|
|
elemPtr := reflect.New(elemType)
|
|
err := l.Load(elemPtr.Interface())
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Append decoded element to slice
|
|
sliceVal.Set(reflect.Append(sliceVal, elemPtr.Elem()))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadSingle loads exactly one document (strict)
|
|
func loadSingle(in []byte, out any, opts *libyaml.Options) error {
|
|
l, err := NewLoader(bytes.NewReader(in), func(o *libyaml.Options) error {
|
|
*o = *opts // Copy options
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load first document
|
|
err = l.Load(out)
|
|
if err == io.EOF {
|
|
return &LoadErrors{Errors: []*libyaml.ConstructError{{
|
|
Err: errors.New("yaml: no documents in stream"),
|
|
}}}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Check for additional documents
|
|
var dummy any
|
|
err = l.Load(&dummy)
|
|
if err != io.EOF {
|
|
if err != nil {
|
|
// Some other error occurred
|
|
return err
|
|
}
|
|
// Successfully loaded a second document - this is an error in strict mode
|
|
return &LoadErrors{Errors: []*libyaml.ConstructError{{
|
|
Err: errors.New("yaml: expected single document, found multiple"),
|
|
}}}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// A Loader reads and decodes YAML values from an input stream with configurable
|
|
// options.
|
|
type Loader struct {
|
|
composer *libyaml.Composer
|
|
decoder *libyaml.Constructor
|
|
opts *libyaml.Options
|
|
docCount int
|
|
}
|
|
|
|
// NewLoader returns a new Loader that reads from r with the given options.
|
|
//
|
|
// The Loader introduces its own buffering and may read data from r beyond the
|
|
// YAML values requested.
|
|
func NewLoader(r io.Reader, opts ...Option) (*Loader, error) {
|
|
o, err := libyaml.ApplyOptions(opts...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := libyaml.NewComposerFromReader(r)
|
|
c.SetStreamNodes(o.StreamNodes)
|
|
return &Loader{
|
|
composer: c,
|
|
decoder: libyaml.NewConstructor(o),
|
|
opts: o,
|
|
}, nil
|
|
}
|
|
|
|
// Load reads the next YAML-encoded document from its input and stores it
|
|
// in the value pointed to by v.
|
|
//
|
|
// Returns io.EOF when there are no more documents to read.
|
|
// If WithSingleDocument option was set and a document was already read,
|
|
// subsequent calls return io.EOF.
|
|
//
|
|
// 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.
|
|
//
|
|
// Struct fields are only loaded if they are exported (have an upper case
|
|
// first letter), and are loaded using the field name lowercased as the
|
|
// default key. Custom keys may be defined via the "yaml" name in the field
|
|
// tag: the content preceding the first comma is used as the key, and the
|
|
// following comma-separated options control the loading and dumping behavior.
|
|
//
|
|
// See the documentation of the package-level Load function for more details
|
|
// about YAML to Go conversion and tag options.
|
|
func (l *Loader) Load(v any) (err error) {
|
|
defer handleErr(&err)
|
|
if l.opts.SingleDocument && l.docCount > 0 {
|
|
return io.EOF
|
|
}
|
|
node := l.composer.Parse() // *libyaml.Node
|
|
if node == nil {
|
|
return io.EOF
|
|
}
|
|
l.docCount++
|
|
|
|
out := reflect.ValueOf(v)
|
|
if out.Kind() == reflect.Pointer && !out.IsNil() {
|
|
out = out.Elem()
|
|
}
|
|
l.decoder.Construct(node, out) // Pass libyaml.Node directly
|
|
if len(l.decoder.TypeErrors) > 0 {
|
|
typeErrors := l.decoder.TypeErrors
|
|
l.decoder.TypeErrors = nil
|
|
return &LoadErrors{Errors: typeErrors}
|
|
}
|
|
return nil
|
|
}
|