This repository has been archived by the owner on Jun 15, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 5
/
yptr.go
149 lines (134 loc) · 3.99 KB
/
yptr.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: BSD-2-Clause
package yptr
import (
"fmt"
"strconv"
"strings"
"github.com/go-openapi/jsonpointer"
yaml "gopkg.in/yaml.v3"
)
var (
// ErrTooManyResults means a pointer matches too many results (usually more than one expected result).
ErrTooManyResults = fmt.Errorf("too many results")
// ErrNotFound a pointer failed to find a match.
ErrNotFound = fmt.Errorf("not found")
)
// FindAll finds all locations in the json/yaml tree pointed by root that match the extended
// JSONPointer passed in ptr.
func FindAll(root *yaml.Node, ptr string) ([]*yaml.Node, error) {
if ptr == "" {
return nil, fmt.Errorf("invalid empty pointer")
}
// TODO: remove dependency on jsonpointer since we only use it to split and unescape the pointer, which is trivial and well defined by the spec.
p, err := jsonpointer.New(ptr)
if err != nil {
return nil, err
}
toks := p.DecodedTokens()
res, err := find(root, toks)
if err != nil {
return nil, fmt.Errorf("%q: %w", ptr, err)
}
return res, nil
}
// Find is like FindAll but returns ErrTooManyResults if multiple matches are located.
func Find(root *yaml.Node, ptr string) (*yaml.Node, error) {
res, err := FindAll(root, ptr)
if err != nil {
return nil, err
}
if len(res) > 1 {
return nil, fmt.Errorf("got %d matches: %w", len(res), ErrTooManyResults)
}
if len(res) == 0 {
return nil, fmt.Errorf("bad state while finding %q: res is empty but error is: %v", ptr, err)
}
return res[0], nil
}
// find recursively matches a token against a yaml node.
func find(root *yaml.Node, toks []string) ([]*yaml.Node, error) {
next, err := match(root, toks[0])
if err != nil {
return nil, err
}
if len(toks) == 1 {
return next, nil
}
var res []*yaml.Node
for _, n := range next {
f, err := find(n, toks[1:])
if err != nil {
return nil, err
}
res = append(res, f...)
}
return res, nil
}
// match matches a JSONPointer token against a yaml Node.
//
// If root is a map, it performs a field lookup using tok as field name,
// and if found it will return a singleton slice containing the value contained
// in that field.
//
// If root is an array and tok is a number i, it will return the ith element of that array.
// If tok is ~{...}, it will parse the {...} object as a JSON object
// and use it to filter the array using a treeSubsetPred.
// If tok is ~[key=value] it will use keyValuePred to filter the array.
func match(root *yaml.Node, tok string) ([]*yaml.Node, error) {
c := root.Content
switch root.Kind {
case yaml.MappingNode:
if l := len(c); l%2 != 0 {
return nil, fmt.Errorf("yaml.Node invariant broken, found %d map content", l)
}
for i := 0; i < len(c); i += 2 {
key, value := c[i], c[i+1]
if tok == key.Value {
return []*yaml.Node{value}, nil
}
}
case yaml.SequenceNode:
switch {
case strings.HasPrefix(tok, "~{"): // subtree match: ~{"name":"app"}
var mtree yaml.Node
if err := yaml.Unmarshal([]byte(tok[1:]), &mtree); err != nil {
return nil, err
}
return filter(c, treeSubsetPred(&mtree))
default:
i, err := strconv.Atoi(tok)
if err != nil {
return nil, err
}
if i < 0 || i >= len(c) {
return nil, fmt.Errorf("out of bounds")
}
return c[i : i+1], nil
}
case yaml.DocumentNode:
// skip document nodes.
return match(c[0], tok)
default:
return nil, fmt.Errorf("unhandled node type: %v (%v)", root.Kind, root.Tag)
}
return nil, fmt.Errorf("%q: %w", tok, ErrNotFound)
}
type nodePredicate func(*yaml.Node) bool
// filter applies a nodePredicate to each input node and returns only those for which the predicate
// function returns true.
func filter(nodes []*yaml.Node, pred nodePredicate) ([]*yaml.Node, error) {
var res []*yaml.Node
for _, n := range nodes {
if pred(n) {
res = append(res, n)
}
}
return res, nil
}
// A treeSubsetPred is a node predicate that returns true if tree a is a subset of tree b.
func treeSubsetPred(a *yaml.Node) nodePredicate {
return func(b *yaml.Node) bool {
return isTreeSubset(a, b)
}
}