Skip to content

Commit 80feebd

Browse files
committed
Merge pull request #38 from filefrog/resolver
Handle recursive cases for (( grab )) and (( concat ))
2 parents deaf670 + b283972 commit 80feebd

File tree

6 files changed

+164
-49
lines changed

6 files changed

+164
-49
lines changed

assets/concat/loop.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
a: (( concat "a" b ))
2+
b: (( concat a "b" ))

assets/concat/multi.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# foo shoud be 'quux.bar.baz.foo'
2+
foo: (( concat baz ".foo" ))
3+
bar: (( concat quux ".bar" ))
4+
baz: (( concat bar ".baz" ))
5+
quux: "quux"

assets/dereference/multi.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
name1: (( grab name2 ))
2+
name2: (( grab name3 ))
3+
name3: (( grab name4 ))
4+
name4: "name"

concat.go

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,76 @@ type Token struct {
2121
// Concatenator is an implementation of PostProcessor to de-reference (( grab me.data )) calls
2222
type Concatenator struct {
2323
root map[interface{}]interface{}
24+
ttl int
2425
}
2526

2627
// Action returns the Action string for the Concatenator
2728
func (s Concatenator) Action() string {
2829
return "concatenator"
2930
}
3031

31-
// PostProcess - resolves a value by seeing if it matches (( grab me.data )) and retrieves me.data's value
32-
func (s Concatenator) PostProcess(o interface{}, node string) (interface{}, string, error) {
32+
// parseConcatOp - determine if an object is a (( concat ... )) call
33+
func parseConcatOp(o interface{}) (bool, string) {
3334
if o != nil && reflect.TypeOf(o).Kind() == reflect.String {
3435
re := regexp.MustCompile(`^\Q((\E\s*concat\s+(.+)\s*\Q))\E$`)
3536
if re.MatchString(o.(string)) {
3637
keys := re.FindStringSubmatch(o.(string))
38+
return true, keys[1]
39+
}
40+
}
41+
return false, ""
42+
}
43+
44+
// resolve - resolves a set of tokens (literals or references), co-recursively with resolveKey()
45+
func (s Concatenator) resolve(node string, tokens []Token) (string, error) {
46+
str := ""
3747

38-
tokens := parseWords(keys[1])
39-
str := ""
40-
for _, token := range tokens {
41-
if token.Type == TokenLiteral {
42-
str += token.Value
43-
} else {
44-
DEBUG("%s: resolving from %s", node, token.Value)
45-
val, err := resolveNode(token.Value, s.root)
46-
if err != nil {
47-
return nil, "error", fmt.Errorf("%s: Unable to resolve `%s`: `%s", node, token.Value, err.Error())
48-
}
49-
// error if val is not a string
50-
str += val.(string)
51-
}
48+
for _, token := range tokens {
49+
if token.Type == TokenLiteral {
50+
str += token.Value
51+
} else {
52+
DEBUG("%s: resolving from %s", node, token.Value)
53+
val, err := s.resolveKey(token.Value)
54+
if err != nil {
55+
return "", err
5256
}
57+
str += val
58+
}
59+
}
60+
return str, nil
61+
}
5362

54-
return str, "replace", nil
63+
// resolveKey - resolves a single key reference, co-recursively with resolve()
64+
func (s Concatenator) resolveKey(key string) (string, error) {
65+
val, err := resolveNode(key, s.root)
66+
if err != nil {
67+
return "", fmt.Errorf("Unable to resolve `%s`: %s", key, err)
68+
}
69+
70+
if should, args := parseConcatOp(val); should {
71+
if s.ttl -= 1; s.ttl <= 0 {
72+
return "", fmt.Errorf("possible recursion detected in call to (( concat ))")
73+
}
74+
str, err := s.resolve(key, parseWords(args))
75+
s.ttl += 1
76+
if err != nil {
77+
return "", err
78+
}
79+
return str, nil
80+
}
81+
// error if val is not a string
82+
return val.(string), err
83+
}
84+
85+
// PostProcess - resolves a value by seeing if it matches (( concat me.data )) and retrieves me.data's value
86+
func (s Concatenator) PostProcess(o interface{}, node string) (interface{}, string, error) {
87+
if should, args := parseConcatOp(o); should {
88+
s.ttl = 64
89+
str, err := s.resolve(node, parseWords(args))
90+
if err != nil {
91+
return nil, "error", fmt.Errorf("%s: %s", node, err.Error())
5592
}
93+
return str, "replace", nil
5694
}
5795

5896
return nil, "ignore", nil

dereferencer.go

Lines changed: 64 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,51 +4,83 @@ import (
44
"fmt"
55
"reflect"
66
"regexp"
7+
"strings"
78
)
89

910
// DeReferencer is an implementation of PostProcessor to de-reference (( grab me.data )) calls
1011
type DeReferencer struct {
1112
root map[interface{}]interface{}
13+
ttl int
1214
}
1315

14-
// PostProcess - resolves a value by seeing if it matches (( grab me.data )) and retrieves me.data's value
15-
func (d DeReferencer) PostProcess(o interface{}, node string) (interface{}, string, error) {
16+
// parseGrabOp - determine if an object is a (( grab ... )) call
17+
func parseGrabOp(o interface{}) (bool, string) {
1618
if o != nil && reflect.TypeOf(o).Kind() == reflect.String {
17-
re := regexp.MustCompile("^\\Q((\\E\\s*grab\\s+(.+?)\\s*\\Q))\\E$")
19+
re := regexp.MustCompile(`^\Q((\E\s*grab\s+(.+)\s*\Q))\E$`)
1820
if re.MatchString(o.(string)) {
1921
keys := re.FindStringSubmatch(o.(string))
20-
if keys[1] != "" {
21-
wsSquasher := regexp.MustCompile("\\s+")
22-
targets := wsSquasher.Split(keys[1], -1)
23-
if len(targets) <= 1 {
24-
DEBUG("%s: dereferencing value '%s'", node, targets[0])
25-
val, err := resolveNode(targets[0], d.root)
26-
if err != nil {
27-
return nil, "error", fmt.Errorf("%s: Unable to resolve `%s`: `%s", node, targets[0], err.Error())
28-
}
29-
DEBUG("%s: setting to %#v", node, val)
30-
return val, "replace", nil
31-
}
32-
val := []interface{}{}
33-
for _, target := range targets {
34-
DEBUG("%s: dereferencing value '%s'", node, target)
35-
v, err := resolveNode(target, d.root)
36-
if err != nil {
37-
return nil, "error", fmt.Errorf("%s: Unable to resolve `%s`: `%s", node, target, err.Error())
38-
}
39-
if reflect.TypeOf(v).Kind() == reflect.Slice {
40-
for i := 0; i < reflect.ValueOf(v).Len(); i++ {
41-
val = append(val, reflect.ValueOf(v).Index(i).Interface())
42-
}
43-
} else {
44-
val = append(val, v)
45-
}
46-
}
47-
DEBUG("%s: setting to %#v", node, val)
48-
return val, "replace", nil
22+
return true, keys[1]
23+
}
24+
}
25+
return false, ""
26+
}
27+
28+
// resolve - resolves a set of tokens (literals or references), co-recursively with resolveKey()
29+
func (d DeReferencer) resolve(node string, args string) (interface{}, error) {
30+
DEBUG("%s: resolving (( grab %s )))", node, args)
31+
re := regexp.MustCompile(`\s+`)
32+
targets := re.Split(strings.Trim(args, " \t\r\n"), -1)
33+
34+
if len(targets) <= 1 {
35+
val, err := d.resolveKey(targets[0])
36+
return val, err
37+
}
38+
val := []interface{}{}
39+
for _, target := range targets {
40+
v, err := d.resolveKey(target)
41+
if err != nil {
42+
return nil, err
43+
}
44+
if v != nil && reflect.TypeOf(v).Kind() == reflect.Slice {
45+
for i := 0; i < reflect.ValueOf(v).Len(); i++ {
46+
val = append(val, reflect.ValueOf(v).Index(i).Interface())
4947
}
48+
} else {
49+
val = append(val, v)
50+
}
51+
}
52+
return val, nil
53+
}
54+
55+
// resolveKey - resolves a single key reference, co-recursively with resolve()
56+
func (d DeReferencer) resolveKey(key string) (interface{}, error) {
57+
DEBUG(" -> resolving reference to `%s`", key)
58+
val, err := resolveNode(key, d.root)
59+
if err != nil {
60+
return nil, fmt.Errorf("Unable to resolve `%s`: `%s", key, err)
61+
}
62+
63+
if should, args := parseGrabOp(val); should {
64+
if d.ttl -= 1; d.ttl <= 0 {
65+
return "", fmt.Errorf("possible recursion detected in call to (( grab ))")
5066
}
67+
val, err = d.resolve(key, args)
68+
d.ttl += 1
69+
return val, err
5170
}
71+
return val, nil
72+
}
5273

74+
// PostProcess - resolves a value by seeing if it matches (( grab me.data )) and retrieves me.data's value
75+
func (d DeReferencer) PostProcess(o interface{}, node string) (interface{}, string, error) {
76+
if should, args := parseGrabOp(o); should {
77+
d.ttl = 64
78+
val, err := d.resolve(node, args)
79+
if err != nil {
80+
return nil, "error", fmt.Errorf("%s: %s", node, err.Error())
81+
}
82+
DEBUG("%s: setting to %#v", node, val)
83+
return val, "replace", nil
84+
}
5385
return nil, "ignore", nil
5486
}

main_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,19 @@ properties:
329329
So(stderr, ShouldEqual, "")
330330
So(stdout, ShouldEqual, `value: null
331331
332+
`)
333+
})
334+
Convey("can dereference nestedly", func() {
335+
os.Args = []string{"spruce", "merge", "assets/dereference/multi.yml"}
336+
stdout = ""
337+
stderr = ""
338+
main()
339+
So(stderr, ShouldEqual, "")
340+
So(stdout, ShouldEqual, `name1: name
341+
name2: name
342+
name3: name
343+
name4: name
344+
332345
`)
333346
})
334347
Convey("static_ips() failures return errors to the user", func() {
@@ -421,6 +434,27 @@ storage: 4096
421434
So(stderr, ShouldStartWith, "$.ident: Unable to resolve `local.sites.[42].uuid`:")
422435
So(stdout, ShouldEqual, "")
423436
})
437+
Convey("string concatentation handles multiple levels of reference", func() {
438+
os.Args = []string{"spruce", "merge", "assets/concat/multi.yml"}
439+
stdout = ""
440+
stderr = ""
441+
main()
442+
So(stderr, ShouldEqual, "")
443+
So(stdout, ShouldEqual, `bar: quux.bar
444+
baz: quux.bar.baz
445+
foo: quux.bar.baz.foo
446+
quux: quux
447+
448+
`)
449+
Convey("string concatenation handles infinite loop self-reference", func() {
450+
os.Args = []string{"spruce", "merge", "assets/concat/loop.yml"}
451+
stdout = ""
452+
stderr = ""
453+
main()
454+
So(stderr, ShouldContainSubstring, "possible recursion detected in call to (( concat ))")
455+
So(stdout, ShouldEqual, "")
456+
})
457+
})
424458
})
425459
}
426460

0 commit comments

Comments
 (0)