/
breadcrumbs.go
195 lines (171 loc) · 4.66 KB
/
breadcrumbs.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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
package query
import (
"fmt"
"regexp"
"strconv"
"strings"
"github.com/hashicorp/hcl/hcl/ast"
)
// Breadcrumbs represents the dot.style.query that the user passes in.
type Breadcrumbs struct {
Parts []Crumb
Length int
}
// Crumb represents an individual portion of a user's query. For example, given
// the query 'data.*.bar.id', each of data, *, bar, and id are each crumbs.
// Crumbs implement IsMatch to determine whether or not it can be used to match
// a given HCL key.
type Crumb interface {
IsMatch(key string, val ast.Node) (bool, error)
Key() string
}
// IndexedCrumb is a type of Crumb used for indexable elements such as arrays
// or lists. Its index is optional and will be nil if no index was specified.
// For example, in the query 'foo.bar[1]', the bar[1] portion will be represented
// by an IndexedCrumb with an index of 1. If an index is not present, such as
// with 'bar[]', the index will be nil.
type IndexedCrumb interface {
IsMatch(key string, val ast.Node) (bool, error)
Key() string
Index() *int
}
// Key is a literal breadcrumb that matches based on its exact value.
type Key struct {
value string
}
// List is a breadcrumb representing a list or array, either the entire list or
// just an individual item.
type List struct {
value string
index *int
key string
}
// Regex is a breadcrumb that matches based on a regex. It can take an optioanl indexer.
type Regex struct {
pattern *regexp.Regexp
index *int
}
// Wildcard is the literal breadcrumb '*' that matches anything.
type Wildcard struct {
}
func (w *Wildcard) IsMatch(key string, val ast.Node) (bool, error) {
return true, nil
}
func (w *Wildcard) Key() string {
return "*"
}
func (r *Regex) IsMatch(key string, val ast.Node) (bool, error) {
return r.pattern.MatchString(key), nil
}
func (r *Regex) Key() string {
return ""
}
func (r *Regex) Index() *int {
return r.index
}
func (k *Key) IsMatch(key string, val ast.Node) (bool, error) {
if key == k.value {
_, isList := val.(*ast.ListType)
if isList {
return true, fmt.Errorf("key '%s' found but is of wrong type, query requested key/literal, found list", key)
}
return true, nil
}
return false, nil
}
func (k *Key) Key() string {
return k.value
}
func (l *List) IsMatch(key string, val ast.Node) (bool, error) {
if key == l.Key() {
_, ok := val.(*ast.ListType)
if !ok {
return false, fmt.Errorf("key '%s' found but is of wrong type, query requested list", key)
}
return true, nil
}
return false, nil
}
func (l *List) Key() string {
return l.key
}
func (l *List) Index() *int {
return l.index
}
// Matches by key literal `abc`
var keyRegex, _ = regexp.Compile(`^([\w|-]+)`)
// Matches a list `abc[]` or `abc[123]` or `a[-1]`
var listRegex, _ = regexp.Compile(`^([\w|-]+)\[(-?\d*)]`)
// Matches by regex `/someRegex/` with optional indexer, e.g. `/someRegex/[]`
var regexRegex, _ = regexp.Compile(`/((?:[^\\/]|\\.)*)/(\[(\d*)\])?`)
// ParseBreadcrumbs reads in a query string specified by the user and breaks it
// down into Crumb instances that can be matched against HCL keys with IsMatch.
func ParseBreadcrumbs(queryString string) (*Breadcrumbs, error) {
queryString = strings.Trim(queryString, `"'`)
query := &Breadcrumbs{Parts: []Crumb{}}
err := parseBreadcrumbs(queryString, 0, &query.Parts)
query.Length = len(query.Parts)
return query, err
}
func parseBreadcrumbs(query string, i int, queue *[]Crumb) error {
if i >= len(query) {
return nil
}
char := query[i : i+1]
if char == "." {
return parseBreadcrumbs(query, i+1, queue)
}
if char == "*" {
newCrumb := &Wildcard{}
*queue = append(*queue, newCrumb)
return parseBreadcrumbs(query, i+1, queue)
}
regexMatches := regexRegex.FindStringSubmatch(query[i:])
if len(regexMatches) > 1 {
pattern, err := regexp.Compile(regexMatches[1])
if err != nil {
return err
}
newCrumb := &Regex{
pattern: pattern,
}
index, err := strconv.Atoi(regexMatches[1])
if err == nil {
newCrumb.index = &index
}
*queue = append(*queue, newCrumb)
i += len(regexMatches[0])
return parseBreadcrumbs(query, i, queue)
}
listMatches := listRegex.FindStringSubmatch(query[i:])
if listMatches != nil {
list := listMatches[0]
i += len(list)
newCrumb := &List{
value: list,
key: listMatches[1],
}
index, err := strconv.Atoi(listMatches[2])
if err == nil {
newCrumb.index = &index
}
*queue = append(*queue, newCrumb)
if i >= len(query) {
return nil
}
return parseBreadcrumbs(query, i, queue)
}
key := keyRegex.FindString(query[i:])
if key != "" {
i += len(key)
newCrumb := &Key{
value: key,
}
*queue = append(*queue, newCrumb)
if i >= len(query) {
return nil
}
return parseBreadcrumbs(query, i, queue)
}
return nil
}