A set of utility methods to help with parsing and transforming YAML using go-yaml/yaml.
go get -u github.com/jcwillox/go-yamltoolsimport (
"gopkg.in/yaml.v3"
"github.com/jcwillox/go-yamltools"
)
type ConfigList []*Config
type Config struct {
Path string `yaml:",omitempty"`
Force bool
Recursive bool
}
func (c *ConfigList) UnmarshalYAML(n *yaml.Node) error {
// will ensure the order of the keys is preserved
// by converting a map to a list of single key maps
// has no effect if the root node is not a map
// key1: val1
// key2: val2
// becomes
// - key1: val1
// - key2: val2
n = yamltools.MapToSliceMap(n)
// ensure the node is a list, i.e. if it was a string
n = yamltools.EnsureList(n)
// this type alias is unfortunately necessary otherwise when
// we call `n.Decode` it will call `UnmarshalYAML` again
type ConfigListT ConfigList
return n.Decode((*ConfigListT)(c))
}
func (c *Config) UnmarshalYAML(n *yaml.Node) error {
// will ensure the node is a map and that the value is not null
// key1: null
// > key1: {}
// string2
// > string2: {}
n = yamltools.EnsureMapMap(n)
// this a rather complex transformation used to flatten a map to
// make it easier to use with structs
// ~/projects:
// key2: val2
// becomes
// key2: val2
// path: ~/projects
n = yamltools.MapKeyIntoValueMap(n, "path")
type ConfigT Config
return n.Decode((*ConfigT)(c))
}This project adds support for YAML tags such as the !include tag to load in a YAML fragment. It also support the definition of custom tags.
Include Tag
import (
"gopkg.in/yaml.v3"
"github.com/jcwillox/go-yamltools"
)
type Config struct {
...
}
func (c *Config) UnmarshalYAML(n *yaml.Node) error {
// will recursively load in any `!include` tags replacing the
// node with the loaded fragment
err := yamltools.LoadIncludeTag(n)
if err != nil {
return err
}
// support the `!include_dir_named` tag, which will load in all
// files in a directory into a map of each filename to the fragment
err = yamltools.LoadIncludeDirNamedTag(n)
if err != nil {
return err
}
// this type alias is unfortunately necessary otherwise when
// we call `n.Decode` it will call `UnmarshalYAML` again
type ConfigT Config
return n.Decode((*ConfigT)(c))
}Custom Tags
Custom tags can be defined using the HandleCustomTag function, and use the LoadFileFragment function which reads in, parses the file and returns a YAML node.
See the LoadIncludeDirNamedTag function definition for a more advanced example.
func LoadIncludeTag(n *yaml.Node) error {
return HandleCustomTag(n, "!include", func(n *yaml.Node) error {
fragment, err := LoadFileFragment(n.Value)
if err != nil {
return err
}
*n = *fragment
return nil
})
}- func EnsureFlatList(n *yaml.Node) *yaml.Node
- func EnsureList(n *yaml.Node) *yaml.Node
- func EnsureMapMap(n *yaml.Node) *yaml.Node
- func HandleCustomTag(n *yaml.Node, tag string, fn TagProcessor) error
- func IsScalarMap(n *yaml.Node) bool
- func ListToMapVal(n *yaml.Node, key string) *yaml.Node
- func LoadFileFragment(path string) (*yaml.Node, error)
- func LoadIncludeDirNamedTag(n *yaml.Node) error
- func LoadIncludeTag(n *yaml.Node) error
- func MapKeyIntoValueMap(n *yaml.Node, keyKey string) *yaml.Node
- func MapKeys(n *yaml.Node) []string
- func MapSplitKeyVal(n *yaml.Node, keyKey, valKey string) *yaml.Node
- func MapToSliceMap(n *yaml.Node) *yaml.Node
- func ParseBoolNode(n *yaml.Node) (value bool, ok bool)
- func ScalarToList(n *yaml.Node) *yaml.Node
- func ScalarToMap(n *yaml.Node) *yaml.Node
- func ScalarToMapVal(n *yaml.Node, key string) *yaml.Node
- type Fragment
- type TagProcessor
func EnsureFlatList(n *yaml.Node) *yaml.NodeEnsureFlatList will flatten nested lists of yaml nodes expects to be passed a yaml.SequenceNode
func EnsureList(n *yaml.Node) *yaml.NodeEnsureList will ensure that the base node is a SequenceNode
key: val
=========
- key: val
func EnsureMapMap(n *yaml.Node) *yaml.NodeEnsureMapMap will ensure that the node is a map and that the value is not null
key: null
> key: {}
string
> string: {}
func HandleCustomTag(n *yaml.Node, tag string, fn TagProcessor) errorHandleCustomTag is used to define a custom YAML tags, it will recursively search YAML nodes for the tag and call the tag processor function.
func IsScalarMap(n *yaml.Node) boolIsScalarMap tests if n is a map that contains only scalar keys and values
func ListToMapVal(n *yaml.Node, key string) *yaml.NodeListToMapVal converts a sequence node to a mapping of {key: node} does nothing if n is not a sequence node.
[i1, i2]
key: [i1, i2]
func LoadFileFragment(path string) (*yaml.Node, error)LoadFileFragment reads in and parses a given file returning a YAML node.
func LoadIncludeDirNamedTag(n *yaml.Node) errorLoadIncludeDirNamedTag recursively searches for the !include_dir_named tag from the given node and will replace the tag node with map of filename to content for each file in the directory.
func LoadIncludeTag(n *yaml.Node) errorLoadIncludeTag recursively searches for the !include tag from the given node and will replace the tag node with content of the included file.
func MapKeyIntoValueMap(n *yaml.Node, keyKey string) *yaml.NodeMapKeyIntoValueMap if the value is a map moves the key into the map with the specified name
key1:
key2: val2
============
key2: val2
keyKey: key1
func MapKeys(n *yaml.Node) []stringMapKeys will return a list of the keys in a map, or an empty list if the node is not a map
func MapSplitKeyVal(n *yaml.Node, keyKey, valKey string) *yaml.NodeMapSplitKeyVal splits a maps key and val into their own maps using the specified keys
key: val
===========
keyKey: key
valKey: val
func MapToSliceMap(n *yaml.Node) *yaml.NodeMapToSliceMap converts a map to a slice of maps with one key each
key1:
key2: val1
key3: val2
==============
- key1:
key2: val1
- key3: val2
func ParseBoolNode(n *yaml.Node) (value bool, ok bool)ParseBoolNode will parse a boolean node and return its value if the node is not a boolean node `ok` will be false. This is useful if you want to apply a different transform based on a boolean flag in the YAML.
func ScalarToList(n *yaml.Node) *yaml.NodeScalarToList wraps a scalar node in a sequence node
string
========
- string
func ScalarToMap(n *yaml.Node) *yaml.NodeScalarToMap will convert a scalar node to a mapping of {scalar: nil}
string
========
string: null
func ScalarToMapVal(n *yaml.Node, key string) *yaml.NodeScalarToMapVal converts a scalar node to a mapping of {key: node} does nothing if n is not a scalar node.
string
> key: string
Fragment is used to parse YAML into a node instead of an interface.
type Fragment struct {
Content *yaml.Node
}func (f *Fragment) UnmarshalYAML(n *yaml.Node) errortype TagProcessor = func(n *yaml.Node) error