yay

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 22, 2025 License: Apache-2.0 Imports: 6 Imported by: 0

README

yay

Working with YAML in Go can be fun. :yay:

This project provides some utilities to make it slightly more fun by wrapping yaml.v3 and YAML JSONPath.

GitHub release (latest SemVer) Go Version Apache 2.0 License
build Go Report Card

Features

  • A visitor allowing user defined handlers for standard yaml.v3
  • A ConditionalHandler allowing to define YAML JSONPath preconditions to visitor methods

Examples

Standard Handler

First, create a handler which satisfies one or more of the interfaces:

  • VisitsYaml
  • VisitsDocumentNode
  • VisitsSequenceNode
  • VisitsMappingNode
  • VisitsScalarNode
  • VisitsAliasNode
type myHandler struct{}
func (m *myHandler) VisitScalarNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error {
	fmt.Printf("found key=%s value=%s\n", key.Value, value.Value)
    return nil
}

Then, pass create a new visitor with this handler and run against the target document node. Error handling omitted from the below example:

document := &yaml.Node{}
// parse your document
visitor, _ := yay.NewVisitor(myHandler{})
_ = visitor.Visit(context.TODO(), document)
Conditional Handler

Suppose you have a complex YAML document, and you only want to parse nodes based on some condition. You could use the standard handler and apply those checks on the key/value nodes. This is fine, but may lead to unexpected bugs/behaviors. You can also apply a selector as a precondition using a conditional handler.

Consider this YAML document used in conditional_handler_test.go:

---
store:
  book:
  - author: Ernest Hemingway
    title: The Old Man and the Sea
  - author: Fyodor Mikhailovich Dostoevsky
    title: Crime and Punishment
  - author: Jane Austen
    title: Sense and Sensibility
  - author: Kurt Vonnegut Jr.
    title: Slaughterhouse-Five
  - author: J. R. R. Tolkien
    title: The Lord of the Rings

IF you only want to process titles beginning with S, you could use the selector syntax:

$.store.book[?(@.title=~/^S.*$/)]

This will select each mapping node matching that criteria. Rather than your visitor processing 5 items, you will process 2 items. For example:

document := &yaml.Node{}
_ = yaml.Unmarshal([]byte(input), document)

count := 0
handler, _ := yay.NewConditionalHandler(
    yay.OnVisitMappingNode("$.store.book[?(@.title=~/^S.*$/)]",
        func(ctx context.Context, key *yaml.Node, value *yaml.Node) error {
            fmt.Printf("processed item at index %d\n", count)
            count += 1
            return nil
        }))

visitor, _ := yay.NewVisitor(handler)
_ = visitor.Visit(context.TODO(), document)

This will output:

processed item at index 0
processed item at index 1

If you want to process the item for which your conditional applies, you can continue chaining dot-notation. For example, to process just the scalar title:

handler, _ := yay.NewConditionalHandler(
    yay.OnVisitScalarNode("$.store.book[?(@.title=~/^S.*$/)].title",
        func(ctx context.Context, key *yaml.Node, value *yaml.Node) error {
            fmt.Printf("processed item at index %d\n", count)
            count += 1
            return nil
        }))

Notice the use of the functional OnVisitScalarNode and the matcher is now $.store.book[?(@.title=~/^S.*$/)].title.

Caveats

Note that key may be nil if the node type you're processing exists within a sequence in the original document. That is, items within sequences don't have keys.

Install

go get -u github.com/jimschubert/yay

Build/Test

go test -v -race -cover ./...

License

This project is licensed under Apache 2.0.

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewMultipleToSingleMergeHandler added in v0.2.2

func NewMultipleToSingleMergeHandler(opts ...MultipleMergeKeyOpt) *multipleToSingleMergeHandler

NewMultipleToSingleMergeHandler aims to address a common situation in which the YAML specification does not allow multiple merge keys under a given node, while many parsers and libraries allow it. This is defined in yaml.v3 issue 624. When consuming such a document with yaml.v3 into a tag-based struct, parsing will fail. Passing a node through this handler will allow this scenario to succeed.

Example
package main

import (
	"context"
	"os"

	"github.com/jimschubert/yay"
	"go.yaml.in/yaml/v3"
)

func main() {
	input := `---
input:
  first: &first-ref
    a: A
    msg: goodbye, world
  second: &second-ref
    b: B
    msg: hello, world

config:
  actual:
    <<: *first-ref
    <<: *second-ref
    c: C
`

	document := &yaml.Node{}
	_ = yaml.Unmarshal([]byte(input), document)

	handler := yay.NewMultipleToSingleMergeHandler()
	visitor, _ := yay.NewVisitor(handler)
	_ = visitor.Visit(context.TODO(), document)

	enc := yaml.NewEncoder(os.Stdout)
	enc.SetIndent(1)
	_ = enc.Encode(document)
}
Output:
input:
  first: &first-ref
    a: A
    msg: goodbye, world
  second: &second-ref
    b: B
    msg: hello, world
config:
  actual:
    !!merge <<: [*second-ref, *first-ref]
    c: C

func OnVisitAliasNode

func OnVisitAliasNode(path string, fn FnVisitKeyValueNode) conditionalHandlerOpt

func OnVisitDocumentNode

func OnVisitDocumentNode(fn FnVisitValueNode) conditionalHandlerOpt

func OnVisitMappingNode

func OnVisitMappingNode(path string, fn FnVisitKeyValueNode) conditionalHandlerOpt

func OnVisitScalarNode

func OnVisitScalarNode(path string, fn FnVisitKeyValueNode) conditionalHandlerOpt

func OnVisitSequenceNode

func OnVisitSequenceNode(path string, fn FnVisitKeyValueNode) conditionalHandlerOpt

func WithPathMatcher

func WithPathMatcher(ctx context.Context, matcher *PathMatcher) context.Context

WithPathMatcher derives from the parent context a new context containing the provided PathMatcher

Types

type ConditionalHandler

type ConditionalHandler struct {
	// contains filtered or unexported fields
}

ConditionalHandler allows the user to create handler functions which are conditional on a yamlpath selector syntax.

func NewConditionalHandler

func NewConditionalHandler(opts ...conditionalHandlerOpt) (*ConditionalHandler, error)

NewConditionalHandler creates a new ConditionalHandler, allowing the user to provide 1..n handler functions with yamlpath preconditions.

Example (Mapping_selectors)
package main

import (
	"context"
	"fmt"

	"github.com/jimschubert/yay"
	"go.yaml.in/yaml/v3"
)

func main() {

	input := `---
store:
  book:
  - author: Ernest Hemingway
    title: The Old Man and the Sea
  - author: Fyodor Mikhailovich Dostoevsky
    title: Crime and Punishment
  - author: Jane Austen
    title: Sense and Sensibility
  - author: Kurt Vonnegut Jr.
    title: Slaughterhouse-Five
  - author: J. R. R. Tolkien
    title: The Lord of the Rings`

	document := &yaml.Node{}
	_ = yaml.Unmarshal([]byte(input), document)

	count := 0
	handler, _ := yay.NewConditionalHandler(
		yay.OnVisitMappingNode("$.store.book[?(@.title=~/^S.*$/)]",
			func(ctx context.Context, key *yaml.Node, value *yaml.Node) error {
				// note: key is nil here because each map value is an item in a sequence
				fmt.Printf("processed item at index %d\n", count)
				count += 1
				return nil
			}))

	visitor, _ := yay.NewVisitor(handler)
	_ = visitor.Visit(context.TODO(), document)
}
Output:
processed item at index 0
processed item at index 1
Example (Scalars_selectors)
package main

import (
	"context"
	"fmt"

	"github.com/jimschubert/yay"
	"go.yaml.in/yaml/v3"
)

func main() {
	input := `---
store:
  book:
  - author: Ernest Hemingway
    title: The Old Man and the Sea
  - author: Fyodor Mikhailovich Dostoevsky
    title: Crime and Punishment
  - author: Jane Austen
    title: Sense and Sensibility
  - author: Kurt Vonnegut Jr.
    title: Slaughterhouse-Five
  - author: J. R. R. Tolkien
    title: The Lord of the Rings`

	document := &yaml.Node{}
	_ = yaml.Unmarshal([]byte(input), document)

	handler, _ := yay.NewConditionalHandler(
		yay.OnVisitScalarNode("$.store.book[?(@.title=~/^S.*$/)].title",
			func(ctx context.Context, key *yaml.Node, value *yaml.Node) error {
				fmt.Printf("processed item: key=%s, value=%q\n", key.Value, value.Value)
				return nil
			}))

	visitor, _ := yay.NewVisitor(handler)
	_ = visitor.Visit(context.TODO(), document)
}
Output:
processed item: key=title, value="Sense and Sensibility"
processed item: key=title, value="Slaughterhouse-Five"

func (*ConditionalHandler) VisitAliasNode

func (c *ConditionalHandler) VisitAliasNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error

VisitAliasNode satisfies VisitsAliasNode such that a visitor always invokes this method, which defers to the handler passed by the user

func (*ConditionalHandler) VisitDocumentNode

func (c *ConditionalHandler) VisitDocumentNode(ctx context.Context, key *yaml.Node) error

VisitDocumentNode satisfies VisitsDocumentNode such that a visitor always invokes this method, which defers to the handler passed by the user

func (*ConditionalHandler) VisitMappingNode

func (c *ConditionalHandler) VisitMappingNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error

VisitMappingNode satisfies VisitsMappingNode such that a visitor always invokes this method, which defers to the handler passed by the user

func (*ConditionalHandler) VisitScalarNode

func (c *ConditionalHandler) VisitScalarNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error

VisitScalarNode satisfies VisitsScalarNode such that a visitor always invokes this method, which defers to the handler passed by the user

func (*ConditionalHandler) VisitSequenceNode

func (c *ConditionalHandler) VisitSequenceNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error

VisitSequenceNode satisfies VisitsSequenceNode such that a visitor always invokes this method, which defers to the handler passed by the user

type FnConditional

type FnConditional func(path string, fn FnVisitKeyValueNode) FnVisitKeyValueNode

type FnOptions added in v0.3.0

type FnOptions func(o *opts)

FnOptions is a function chain of options to apply conditionally to a Visitor

func NewOptions added in v0.3.0

func NewOptions() FnOptions

NewOptions creates a new options functional builder with discoverable functions that don't pollute the yay package

func (FnOptions) WithSkipDocumentCheck added in v0.3.0

func (fn FnOptions) WithSkipDocumentCheck(val bool) FnOptions

WithSkipDocumentCheck allows the user to configure a Visitor to skip the requirement that a top-level node must be a yaml.DocumentNode. This allows creating and invoking visitors against manually constructed nodes.

type FnVisitKeyValueNode

type FnVisitKeyValueNode func(ctx context.Context, key *yaml.Node, value *yaml.Node) error

type FnVisitValueNode

type FnVisitValueNode func(ctx context.Context, value *yaml.Node) error

type MultipleMergeKeyOpt added in v0.3.1

type MultipleMergeKeyOpt func(handler *multipleToSingleMergeHandler)

MultipleMergeKeyOpt is an option for NewMultipleToSingleMergeHandler.

func WithRetainMergeKeyOrder added in v0.3.1

func WithRetainMergeKeyOrder() MultipleMergeKeyOpt

WithRetainMergeKeyOrder is an option for NewMultipleToSingleMergeHandler which changes the behavior of merging keys to retain the original order.

By default, NewMultipleToSingleMergeHandler will reverse the order of the merge keys, allowing for keys to be overridden according to the specification for merge keys as defined in the YAML specification. Since multiple merge keys are not defined by the specification, some parsers and libraries may not follow the same behaviors.

This option will change the behavior to retain the original order of the multiple merge keys.

Example
package main

import (
	"context"
	"os"

	"github.com/jimschubert/yay"
	"go.yaml.in/yaml/v3"
)

func main() {
	input := `---
input:
  first: &first-ref
    a: A
    msg: goodbye, world
  second: &second-ref
    b: B
    msg: hello, world

config:
  actual:
    <<: *first-ref
    <<: *second-ref
    c: C
`

	document := &yaml.Node{}
	_ = yaml.Unmarshal([]byte(input), document)

	handler := yay.NewMultipleToSingleMergeHandler(
		yay.WithRetainMergeKeyOrder(),
	)
	visitor, _ := yay.NewVisitor(handler)
	_ = visitor.Visit(context.TODO(), document)

	enc := yaml.NewEncoder(os.Stdout)
	enc.SetIndent(1)
	_ = enc.Encode(document)
}
Output:
input:
  first: &first-ref
    a: A
    msg: goodbye, world
  second: &second-ref
    b: B
    msg: hello, world
config:
  actual:
    !!merge <<: [*first-ref, *second-ref]
    c: C

type PathMatcher

type PathMatcher struct {
	// contains filtered or unexported fields
}

PathMatcher collects information internally to wrap yamlpath.Path for optimized key/value matching during iteration. A user-facing PathMatcher.Match can be invoked on a node's children to determine if they also match the condition.

func PathMatcherFor

func PathMatcherFor(ctx context.Context, path string) (*PathMatcher, error)

PathMatcherFor retrieves the PathMatcher for the given path on this context, or creates a new one if it differs.

func (*PathMatcher) Match

func (p *PathMatcher) Match(node *yaml.Node) (bool, error)

Match determines if a node matches the yamlpath.Path condition provided by the user

func (*PathMatcher) MustMatch

func (p *PathMatcher) MustMatch(node *yaml.Node) bool

MustMatch is exactly like Match, but panics if the expected match fails

type Visitor

type Visitor interface {
	Visit(ctx context.Context, node *yaml.Node) error
}

Visitor defines behaviors related to recursively visiting a yaml.Node

func NewVisitor

func NewVisitor(handlers ...any) (Visitor, error)

NewVisitor constructs a new Visitor which handles yaml.Node processing defined by handler. The handler must satisfy one or more of the visitor interfaces. See:

  • VisitsYaml
  • VisitsDocumentNode
  • VisitsSequenceNode
  • VisitsMappingNode
  • VisitsScalarNode
  • VisitsAliasNode

func NewVisitorWithOptions added in v0.3.0

func NewVisitorWithOptions(options FnOptions, handlers ...any) (Visitor, error)

NewVisitorWithOptions allows constructing a visitor with options addressing edge case scenarios. It constructs a new Visitor which handles yaml.Node processing defined by handler. The handler must satisfy one or more of the visitor interfaces. See:

  • VisitsYaml
  • VisitsDocumentNode
  • VisitsSequenceNode
  • VisitsMappingNode
  • VisitsScalarNode
  • VisitsAliasNode

type VisitsAliasNode

type VisitsAliasNode interface {
	VisitAliasNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error
}

VisitsAliasNode defines behaviors for visitors which want to handle alias nodes

type VisitsDocumentNode

type VisitsDocumentNode interface {
	VisitDocumentNode(ctx context.Context, key *yaml.Node) error
}

VisitsDocumentNode defines behaviors for visitors which want to handle document nodes

type VisitsMappingNode

type VisitsMappingNode interface {
	VisitMappingNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error
}

VisitsMappingNode defines behaviors for visitors which want to handle mapping nodes

type VisitsScalarNode

type VisitsScalarNode interface {
	VisitScalarNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error
}

VisitsScalarNode defines behaviors for visitors which want to handle scalar nodes

type VisitsSequenceNode

type VisitsSequenceNode interface {
	VisitSequenceNode(ctx context.Context, key *yaml.Node, value *yaml.Node) error
}

VisitsSequenceNode defines behaviors for visitors which want to handle sequence nodes

type VisitsYaml

VisitsYaml defines all behaviors for YAML visitors