// Dependencies returns the nodes that (( join ... )) requires to be resolved // before its evaluation. Returns no dependencies on error, because who cares // about eval order if Run is going to bomb out anyway. func (JoinOperator) Dependencies(ev *Evaluator, args []*Expr, _ []*tree.Cursor) []*tree.Cursor { DEBUG("Calculating dependencies for (( join ... ))") deps := []*tree.Cursor{} if len(args) < 2 { DEBUG("Not enough arguments to (( join ... ))") return []*tree.Cursor{} } //skip the separator arg for _, arg := range args[1:] { if arg.Type == Literal { continue } if arg.Type != Reference { DEBUG("(( join ... )) argument not Literal or Reference type") return []*tree.Cursor{} } //get the real cursor finalCursor, err := arg.Resolve(ev.Tree) if err != nil { DEBUG("Could not resolve to a canonical path '%s'", arg.String()) return []*tree.Cursor{} } //get the list at this location list, err := finalCursor.Reference.Resolve(ev.Tree) if err != nil { DEBUG("Could not retrieve object at path '%s'", arg.String()) return []*tree.Cursor{} } //must be a list or a string switch list.(type) { case []interface{}: //add .* to the end of the cursor so we can glob all the elements globCursor, err := tree.ParseCursor(fmt.Sprintf("%s.*", finalCursor.Reference.String())) if err != nil { DEBUG("Could not parse cursor with '.*' appended. This is a BUG") return []*tree.Cursor{} } //have the cursor library get all the subelements for us subElements, err := globCursor.Glob(ev.Tree) if err != nil { DEBUG("Could not retrieve subelements at path '%s'. This may be a BUG.", arg.String()) return []*tree.Cursor{} } deps = append(deps, subElements...) case string: deps = append(deps, finalCursor.Reference) default: DEBUG("Unsupported type at object location") return []*tree.Cursor{} } } DEBUG("Dependencies for (( join ... )):") for i, dep := range deps { DEBUG("\t#%d %s", i, dep.String()) } return deps }
// Prune ... func (ev *Evaluator) Prune(paths []string) error { DEBUG("pruning %d paths from the final YAML structure", len(paths)) for _, path := range paths { c, err := tree.ParseCursor(path) if err != nil { return err } key := c.Component(-1) parent := c.Copy() parent.Pop() o, err := parent.Resolve(ev.Tree) if err != nil { continue } switch o.(type) { case map[interface{}]interface{}: if _, ok := o.(map[interface{}]interface{}); ok { DEBUG(" pruning %s", path) delete(o.(map[interface{}]interface{}), key) } case []interface{}: if list, ok := o.([]interface{}); ok { if idx, err := strconv.Atoi(key); err == nil { parent.Pop() if s, err := parent.Resolve(ev.Tree); err == nil { if reflect.TypeOf(s).Kind() == reflect.Map { parentName := fmt.Sprintf("%s", c.Component(-2)) DEBUG(" pruning index %d of array '%s'", idx, parentName) length := len(list) - 1 replacement := make([]interface{}, length) copy(replacement, append(list[:idx], list[idx+1:]...)) delete(s.(map[interface{}]interface{}), parentName) s.(map[interface{}]interface{})[parentName] = replacement } } } } default: DEBUG(" I don't know how to prune %s\n value=%v\n", path, o) } } DEBUG("") return nil }
return true, nil } func (m *cursorMatcher) FailureMessage(actual interface{}) string { return fmt.Sprintf("Expected\n\t%s\nto equal\n\t%s", actual.(tree.Cursor), strings.Join(m.nodes, ".")) } func (m *cursorMatcher) NegatedFailureMessage(actual interface{}) string { return fmt.Sprintf("Expected\n\t%s\nto not equal\n\t%s", actual.(tree.Cursor), strings.Join(m.nodes, ".")) } var _ = Describe("Cursors", func() { Context("Basic Cursor Parsing", func() { It("handles dotted-notation", func() { c, err := tree.ParseCursor("x.y.z") Ω(err).ShouldNot(HaveOccurred()) Ω(c).Should(BeCursor("x", "y", "z")) }) It("ignores the '$' sigil in the initial position", func() { c, err := tree.ParseCursor("$.x.y.z") Ω(err).ShouldNot(HaveOccurred()) Ω(c).Should(BeCursor("x", "y", "z")) }) It("handles the '$' sigil in any other position", func() { c, err := tree.ParseCursor("x.$.y.z") Ω(err).ShouldNot(HaveOccurred()) Ω(c).Should(BeCursor("x", "$", "y", "z")) })
// ParseOpcall ... func ParseOpcall(phase OperatorPhase, src string) (*Opcall, error) { split := func(src string) []string { list := make([]string, 0, 0) buf := "" escaped := false quoted := false for _, c := range src { if escaped { switch c { case 'n': buf += "\n" case 'r': buf += "\r" case 't': buf += "\t" default: buf += string(c) } escaped = false continue } if c == '\\' { escaped = true continue } if c == ' ' || c == '\t' || c == ',' { if quoted { buf += string(c) continue } else { if buf != "" { list = append(list, buf) buf = "" } if c == ',' { list = append(list, ",") } } continue } if c == '"' { buf += string(c) quoted = !quoted continue } buf += string(c) } if buf != "" { list = append(list, buf) } return list } argify := func(src string) (args []*Expr, err error) { qstring := regexp.MustCompile(`(?s)^"(.*)"$`) integer := regexp.MustCompile(`^[+-]?\d+(\.\d+)?$`) float := regexp.MustCompile(`^[+-]?\d*\.\d+$`) envvar := regexp.MustCompile(`^\$[a-zA-Z_][a-zA-Z0-9_]*$`) var final []*Expr var left, op *Expr pop := func() { if left != nil { final = append(final, left) left = nil } } push := func(e *Expr) { TRACE("expr: pushing data expression `%s' onto stack", e) TRACE("expr: start: left=`%s', op=`%s'", left, op) defer func() { TRACE("expr: end: left=`%s', op=`%s'\n", left, op) }() if left == nil { left = e return } if op == nil { pop() left = e return } op.Left = left op.Right = e left = op op = nil } TRACE("expr: parsing `%s'", src) for i, arg := range split(src) { switch { case arg == ",": DEBUG(" #%d: literal comma found; treating what we've seen so far as a complete expression") pop() case envvar.MatchString(arg): DEBUG(" #%d: parsed as unquoted environment variable reference '%s'", i, arg) push(&Expr{Type: EnvVar, Name: arg[1:]}) case qstring.MatchString(arg): m := qstring.FindStringSubmatch(arg) DEBUG(" #%d: parsed as quoted string literal '%s'", i, m[1]) push(&Expr{Type: Literal, Literal: m[1]}) case float.MatchString(arg): DEBUG(" #%d: parsed as unquoted floating point literal '%s'", i, arg) v, err := strconv.ParseFloat(arg, 64) if err != nil { DEBUG(" #%d: %s is not parsable as a floating point number: %s", i, arg, err) return args, err } push(&Expr{Type: Literal, Literal: v}) case integer.MatchString(arg): DEBUG(" #%d: parsed as unquoted integer literal '%s'", i, arg) v, err := strconv.ParseInt(arg, 10, 64) if err != nil { DEBUG(" #%d: %s is not parsable as an integer: %s", i, arg, err) return args, err } push(&Expr{Type: Literal, Literal: v}) case arg == "||": DEBUG(" #%d: parsed logical-or operator, '||'", i) if left == nil || op != nil { return args, fmt.Errorf(`syntax error near: %s`, src) } TRACE("expr: pushing || expr-op onto the stack") op = &Expr{Type: LogicalOr} case arg == "nil" || arg == "null" || arg == "~" || arg == "Nil" || arg == "Null" || arg == "NIL" || arg == "NULL": DEBUG(" #%d: parsed the nil value token '%s'", i, arg) push(&Expr{Type: Literal, Literal: nil}) case arg == "false" || arg == "False" || arg == "FALSE": DEBUG(" #%d: parsed the false value token '%s'", i, arg) push(&Expr{Type: Literal, Literal: false}) case arg == "true" || arg == "True" || arg == "TRUE": DEBUG(" #%d: parsed the true value token '%s'", i, arg) push(&Expr{Type: Literal, Literal: true}) default: c, err := tree.ParseCursor(arg) if err != nil { DEBUG(" #%d: %s is a malformed reference: %s", i, arg, err) return args, err } DEBUG(" #%d: parsed as a reference to $.%s", i, c) push(&Expr{Type: Reference, Reference: c}) } } pop() if left != nil || op != nil { return nil, fmt.Errorf(`syntax error near: %s`, src) } DEBUG("") for _, e := range final { TRACE("expr: pushing expression `%v' onto the operand list", e) reduced, err := e.Reduce() if err != nil { fmt.Fprintf(os.Stdout, "warning: %s\n", err) } args = append(args, reduced) } return args, nil } op := &Opcall{src: src} for _, pattern := range []string{ `^\Q((\E\s*([a-zA-Z][a-zA-Z0-9_-]*)(?:\s*\((.*)\))?\s*\Q))\E$`, // (( op(x,y,z) )) `^\Q((\E\s*([a-zA-Z][a-zA-Z0-9_-]*)(?:\s+(.*))?\s*\Q))\E$`, // (( op x y z )) } { re := regexp.MustCompile(pattern) if !re.MatchString(src) { continue } m := re.FindStringSubmatch(src) DEBUG("parsing `%s': looks like a (( %s ... )) operator\n arguments:", src, m[1]) op.op = OperatorFor(m[1]) if op.op.Phase() != phase { DEBUG(" - skipping (( %s ... )) operation; it belongs to a different phase", m[1]) return nil, nil } args, err := argify(m[2]) if err != nil { return nil, err } if len(args) == 0 { DEBUG(" (none)") } op.args = args return op, nil } return nil, nil }
func TestOperators(t *testing.T) { cursor := func(s string) *tree.Cursor { c, err := tree.ParseCursor(s) So(err, ShouldBeNil) return c } YAML := func(s string) map[interface{}]interface{} { y, err := simpleyaml.NewYaml([]byte(s)) So(err, ShouldBeNil) data, err := y.Map() So(err, ShouldBeNil) return data } ref := func(s string) *Expr { return &Expr{Type: Reference, Reference: cursor(s)} } str := func(s string) *Expr { return &Expr{Type: Literal, Literal: s} } num := func(v int64) *Expr { return &Expr{Type: Literal, Literal: v} } null := func() *Expr { return &Expr{Type: Literal, Literal: nil} } env := func(s string) *Expr { return &Expr{Type: EnvVar, Name: s} } or := func(l *Expr, r *Expr) *Expr { return &Expr{Type: LogicalOr, Left: l, Right: r} } var exprOk func(*Expr, *Expr) exprOk = func(got *Expr, want *Expr) { So(got, ShouldNotBeNil) So(want, ShouldNotBeNil) So(got.Type, ShouldEqual, want.Type) switch want.Type { case Literal: So(got.Literal, ShouldEqual, want.Literal) case Reference: So(got.Reference.String(), ShouldEqual, want.Reference.String()) case LogicalOr: exprOk(got.Left, want.Left) exprOk(got.Right, want.Right) } } Convey("Parser", t, func() { Convey("parses op calls in their entirety", func() { phase := EvalPhase opOk := func(code string, name string, args ...*Expr) { op, err := ParseOpcall(phase, code) So(err, ShouldBeNil) So(op, ShouldNotBeNil) _, ok := op.op.(NullOperator) So(ok, ShouldBeTrue) So(op.op.(NullOperator).Missing, ShouldEqual, name) So(len(op.args), ShouldEqual, len(args)) for i, expect := range args { exprOk(op.args[i], expect) } } opErr := func(code string, msg string) { _, err := ParseOpcall(phase, code) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, msg) } Convey("handles opcodes with and without arguments", func() { opOk(`(( null ))`, "null") opOk(`(( null 42 ))`, "null", num(42)) opOk(`(( null 1 2 3 4 ))`, "null", num(1), num(2), num(3), num(4)) }) Convey("ignores optional whitespace", func() { opOk(`((null))`, "null") opOk(`(( null ))`, "null") opOk(`(( null ))`, "null") args := []*Expr{num(1), num(2), num(3)} opOk(`((null 1 2 3))`, "null", args...) opOk(`((null 1 2 3))`, "null", args...) opOk(`((null 1 2 3 ))`, "null", args...) opOk(`((null 1 2 3 ))`, "null", args...) }) Convey("allows use of commas to separate arguments", func() { args := []*Expr{num(1), num(2), num(3)} opOk(`((null 1, 2, 3))`, "null", args...) opOk(`((null 1, 2, 3))`, "null", args...) opOk(`((null 1, 2, 3, ))`, "null", args...) opOk(`((null 1 , 2 , 3 , ))`, "null", args...) }) Convey("allows use of parentheses around arguments", func() { args := []*Expr{num(1), num(2), num(3)} opOk(`((null(1,2,3)))`, "null", args...) opOk(`((null(1, 2, 3) ))`, "null", args...) opOk(`((null( 1, 2, 3)))`, "null", args...) opOk(`((null (1, 2, 3) ))`, "null", args...) opOk(`((null (1 , 2 , 3) ))`, "null", args...) }) Convey("handles string literal arguments", func() { opOk(`(( null "string" ))`, "null", str("string")) opOk(`(( null "string with whitespace" ))`, "null", str("string with whitespace")) opOk(`(( null "a \"quoted\" string" ))`, "null", str(`a "quoted" string`)) opOk(`(( null "\\escaped" ))`, "null", str(`\escaped`)) }) Convey("handles reference (cursor) arguments", func() { opOk(`(( null x.y.z ))`, "null", ref("x.y.z")) opOk(`(( null x.[0].z ))`, "null", ref("x.0.z")) opOk(`(( null x[0].z ))`, "null", ref("x.0.z")) opOk(`(( null x[0]z ))`, "null", ref("x.0.z")) }) Convey("handles mixed collections of argument types", func() { opOk(`(( xyzzy "string" x.y.z 42 ))`, "xyzzy", str("string"), ref("x.y.z"), num(42)) opOk(`(( xyzzy("string" x.y.z 42) ))`, "xyzzy", str("string"), ref("x.y.z"), num(42)) }) Convey("handles expression-based operands", func() { opOk(`(( null meta.key || "default" ))`, "null", or(ref("meta.key"), str("default"))) opOk(`(( null meta.key || "default" "second" ))`, "null", or(ref("meta.key"), str("default")), str("second")) opOk(`(( null meta.key || "default", "second" ))`, "null", or(ref("meta.key"), str("default")), str("second")) opOk(`(( null meta.key || "default", meta.other || nil ))`, "null", or(ref("meta.key"), str("default")), or(ref("meta.other"), null())) opOk(`(( null meta.key || "default" meta.other || nil ))`, "null", or(ref("meta.key"), str("default")), or(ref("meta.other"), null())) }) Convey("handles environment variables as operands", func() { os.Setenv("SPRUCE_FOO", "first test") os.Setenv("_SPRUCE", "_sprucify!") os.Setenv("ENOENT", "") os.Setenv("http_proxy", "no://thank/you") opOk(`(( null $SPRUCE_FOO ))`, "null", env("SPRUCE")) opOk(`(( null $_SPRUCE ))`, "null", env("_SPRUCE")) opOk(`(( null $ENOENT || $SPRUCE_FOO ))`, "null", or(env("ENOENT"), env("SPRUCE_FOO"))) opOk(`(( null $http_proxy))`, "null", env("http_proxy")) }) Convey("throws errors for malformed expression", func() { opErr(`(( null meta.key ||, nil ))`, `syntax error near: meta.key ||, nil`) opErr(`(( null || ))`, `syntax error near: ||`) opErr(`(( null || meta.key ))`, `syntax error near: || meta.key`) opErr(`(( null meta.key || || ))`, `syntax error near: meta.key || ||`) }) }) }) Convey("Expression Engine", t, func() { var e *Expr var tree map[interface{}]interface{} evaluate := func(e *Expr, tree map[interface{}]interface{}) interface{} { v, err := e.Evaluate(tree) So(err, ShouldBeNil) return v } Convey("Literals evaluate to themselves", func() { e = &Expr{Type: Literal, Literal: "value"} So(evaluate(e, tree), ShouldEqual, "value") e = &Expr{Type: Literal, Literal: ""} So(evaluate(e, tree), ShouldEqual, "") e = &Expr{Type: Literal, Literal: nil} So(evaluate(e, tree), ShouldEqual, nil) }) Convey("References evaluate to the referenced part of the YAML tree", func() { tree = YAML(`--- meta: foo: FOO bar: BAR `) e = &Expr{Type: Reference, Reference: cursor("meta.foo")} So(evaluate(e, tree), ShouldEqual, "FOO") e = &Expr{Type: Reference, Reference: cursor("meta.bar")} So(evaluate(e, tree), ShouldEqual, "BAR") }) Convey("|| operator evaluates to the first found value", func() { tree = YAML(`--- meta: foo: FOO bar: BAR `) So(evaluate(or(str("first"), str("second")), tree), ShouldEqual, "first") So(evaluate(or(ref("meta.foo"), str("second")), tree), ShouldEqual, "FOO") So(evaluate(or(ref("meta.ENOENT"), ref("meta.foo")), tree), ShouldEqual, "FOO") }) Convey("|| operator treats nil as a found value", func() { tree = YAML(`--- meta: foo: FOO bar: BAR `) So(evaluate(or(null(), str("second")), tree), ShouldBeNil) So(evaluate(or(ref("meta.ENOENT"), null()), tree), ShouldBeNil) }) }) Convey("Expression Reduction Algorithm", t, func() { var orig, final *Expr var err error Convey("ignores singleton expression", func() { orig = str("string") final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) orig = null() final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) orig = ref("meta.key") final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) }) Convey("handles normal alternates that terminated in a literal", func() { orig = or(ref("a.b.c"), str("default")) final, err = orig.Reduce() So(err, ShouldBeNil) exprOk(final, orig) }) Convey("throws errors (warnings) for unreachable alternates", func() { orig = or(null(), str("ignored")) final, err = orig.Reduce() So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, `literal nil short-circuits expression (nil || "ignored")`) exprOk(final, null()) orig = or(ref("some.key"), or(str("default"), ref("ignored.key"))) final, err = orig.Reduce() So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, `literal "default" short-circuits expression (some.key || "default" || ignored.key)`) exprOk(final, or(ref("some.key"), str("default"))) orig = or(or(ref("some.key"), str("default")), ref("ignored.key")) final, err = orig.Reduce() So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, `literal "default" short-circuits expression (some.key || "default" || ignored.key)`) exprOk(final, or(ref("some.key"), str("default"))) }) }) Convey("File Operator", t, func() { op := FileOperator{} ev := &Evaluator{ Tree: YAML( `meta: sample_file: assets/file_operator/sample.txt `), } basedir, _ := os.Getwd() Convey("can read a direct file", func() { r, err := op.Run(ev, []*Expr{ str("assets/file_operator/test.txt"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "This is a test\n") }) Convey("can read a file from a reference", func() { r, err := op.Run(ev, []*Expr{ ref("meta.sample_file"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) content, err := ioutil.ReadFile("assets/file_operator/sample.txt") So(r.Value.(string), ShouldEqual, string(content)) }) Convey("can read a file relative to a specified base path", func() { os.Setenv("SPRUCE_FILE_BASE_PATH", filepath.Join(basedir, "assets/file_operator")) r, err := op.Run(ev, []*Expr{ str("test.txt"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "This is a test\n") }) if _, err := os.Stat("/etc/hosts"); err == nil { Convey("can read an absolute path", func() { os.Setenv("SPRUCE_FILE_BASE_PATH", filepath.Join(basedir, "assets/file_operator")) r, err := op.Run(ev, []*Expr{ str("/etc/hosts"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) content, err := ioutil.ReadFile("/etc/hosts") So(r.Value.(string), ShouldEqual, string(content)) }) } Convey("can handle a missing file", func() { r, err := op.Run(ev, []*Expr{ str("no_one_should_ever_name_a_file_that_doesnt_exist_this_name"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) }) Convey("Grab Operator", t, func() { op := GrabOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 list1: - first - second list2: - third - fourth lonely: - one `), } Convey("can grab a single value", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "found it") }) Convey("can grab a single list value", func() { r, err := op.Run(ev, []*Expr{ ref("key.lonely"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) l, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(l), ShouldEqual, 1) So(l[0], ShouldEqual, "one") }) Convey("can grab a multiple lists and flatten them", func() { r, err := op.Run(ev, []*Expr{ ref("key.list1"), ref("key.lonely"), ref("key.list2"), ref("key.lonely.0"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) l, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(l), ShouldEqual, 6) So(l[0], ShouldEqual, "first") So(l[1], ShouldEqual, "second") So(l[2], ShouldEqual, "one") So(l[3], ShouldEqual, "third") So(l[4], ShouldEqual, "fourth") So(l[5], ShouldEqual, "one") }) Convey("can grab multiple values", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), ref("key.subkey.other"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 2) So(v[0], ShouldEqual, "found it") So(v[1], ShouldEqual, "value 2") }) Convey("flattens constituent arrays", func() { r, err := op.Run(ev, []*Expr{ ref("key.list2"), ref("key.list1"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 4) So(v[0], ShouldEqual, "third") So(v[1], ShouldEqual, "fourth") So(v[2], ShouldEqual, "first") So(v[3], ShouldEqual, "second") }) Convey("throws errors for missing arguments", func() { _, err := op.Run(ev, []*Expr{}) So(err, ShouldNotBeNil) }) Convey("throws errors for dangling references", func() { _, err := op.Run(ev, []*Expr{ ref("key.that.does.not.exist"), }) So(err, ShouldNotBeNil) }) }) Convey("Environment Variable Resolution (via grab)", t, func() { op := GrabOperator{} ev := &Evaluator{} os.Setenv("GRAB_ONE", "one") os.Setenv("GRAB_TWO", "two") os.Setenv("GRAB_NOT", "") Convey("can grab a single environment value", func() { r, err := op.Run(ev, []*Expr{ env("GRAB_ONE"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "one") }) Convey("tries alternates until it finds a set environment variable", func() { r, err := op.Run(ev, []*Expr{ or(env("GRAB_THREE"), or(env("GRAB_TWO"), env("GRAB_ONE"))), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "two") }) Convey("throws errors for unset environment variables", func() { _, err := op.Run(ev, []*Expr{ env("GRAB_NOT"), }) So(err, ShouldNotBeNil) }) }) Convey("Concat Operator", t, func() { op := ConcatOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 list1: - first - second list2: - third - fourth douglas: adams: 42 math: PI: 3.14159 `), } Convey("can concat a single value", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), ref("key.list1.0"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "found itfirst") }) Convey("can concat a literal values", func() { r, err := op.Run(ev, []*Expr{ str("a literal "), str("value"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "a literal value") }) Convey("can concat multiple values", func() { r, err := op.Run(ev, []*Expr{ str("I "), ref("key.subkey.value"), str("!"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "I found it!") }) Convey("can concat integer literals", func() { r, err := op.Run(ev, []*Expr{ str("the answer = "), ref("douglas.adams"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "the answer = 42") }) Convey("can concat float literals", func() { r, err := op.Run(ev, []*Expr{ ref("math.PI"), str(" is PI"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "3.14159 is PI") }) Convey("throws errors for missing arguments", func() { _, err := op.Run(ev, []*Expr{}) So(err, ShouldNotBeNil) _, err = op.Run(ev, []*Expr{str("one")}) So(err, ShouldNotBeNil) }) Convey("throws errors for dangling references", func() { _, err := op.Run(ev, []*Expr{ ref("key.that.does.not.exist"), str("string"), }) So(err, ShouldNotBeNil) }) }) Convey("static_ips Operator", t, func() { op := StaticIPOperator{} Reset(func() { UsedIPs = map[string]string{} }) Convey("can resolve valid networks inside of job contexts", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v[0], ShouldEqual, "10.0.0.5") So(v[1], ShouldEqual, "10.0.0.6") So(v[2], ShouldEqual, "10.0.0.7") }) Convey("works with new style bosh manifests", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] instance_groups: - name: job1 instances: 2 networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 2) So(v[0], ShouldEqual, "10.0.0.5") So(v[1], ShouldEqual, "10.0.0.6") }) Convey("works with multiple subnets", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.3 ] - static: [ 10.0.1.5 - 10.0.1.10 ] instance_groups: - name: job1 instances: 4 networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2), num(3)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 4) So(v[0], ShouldEqual, "10.0.0.2") So(v[1], ShouldEqual, "10.0.0.3") So(v[2], ShouldEqual, "10.0.1.5") So(v[3], ShouldEqual, "10.0.1.6") }) Convey("works with multiple subnets with an availability zone", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.3 ] az: z2 - static: [ 10.0.1.5 - 10.0.1.10 ] az: z1 instance_groups: - name: job1 instances: 4 networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2), num(3)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 4) So(v[0], ShouldEqual, "10.0.0.2") So(v[1], ShouldEqual, "10.0.0.3") So(v[2], ShouldEqual, "10.0.1.5") So(v[3], ShouldEqual, "10.0.1.6") }) Convey("works with instance_group availability zones", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.3 ] az: z1 - static: [ 10.0.1.5 - 10.0.1.10 ] az: z2 instance_groups: - name: job1 instances: 3 azs: [z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v[0], ShouldEqual, "10.0.1.5") So(v[1], ShouldEqual, "10.0.1.6") So(v[2], ShouldEqual, "10.0.1.7") }) Convey("works with directly specified availability zones", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.4 ] az: z1 - static: [ 10.0.2.6 - 10.0.2.10 ] az: z2 instance_groups: - name: job1 instances: 6 azs: [z1,z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z2:1"), num(0), str("z1:2"), str("z2:2"), num(1), str("z2:4"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 6) So(v[0], ShouldEqual, "10.0.2.7") So(v[1], ShouldEqual, "10.0.0.2") So(v[2], ShouldEqual, "10.0.0.4") So(v[3], ShouldEqual, "10.0.2.8") So(v[4], ShouldEqual, "10.0.0.3") So(v[5], ShouldEqual, "10.0.2.10") }) Convey("throws an error if an unknown availability zone is used in operator", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.4 ] az: z1 - static: [ 10.0.2.6 - 10.0.2.10 ] az: z2 instance_groups: - name: job1 instances: 2 azs: [z1,z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z2:0"), str("z3:1"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if offset for an availability zone is out of bounds", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.1 - 10.0.0.5 ] az: z1 - static: [ 10.0.2.1 - 10.0.2.5 ] az: z2 instance_groups: - name: job1 instances: 2 azs: [z1,z2] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z1:4"), str("z1:5"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if an instance_group availability zone is not found in subnets", func() { ev := &Evaluator{ Here: cursor("instance_groups.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-net subnets: - static: [ 10.0.0.2 - 10.0.0.4 ] az: z1 - static: [ 10.0.2.6 - 10.0.2.10 ] az: z2 instance_groups: - name: job1 instances: 2 azs: [z1,z2,z3] networks: - name: test-net static_ips: <------------- HERE ------------ `), } r, err := op.Run(ev, []*Expr{ str("z1:0"), str("z2:1"), }) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("can resolve valid large networks inside of job contexts", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.0 - 10.1.0.1 ] jobs: - name: job1 instances: 7 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{ num(0), num(255), // 2^8 - 1 num(256), // 2^8 num(257), // 2^8 + 1 num(65535), // 2^16 - 1 num(65536), // 2^16 num(65537), // 2^16 + 1 // 1st octet rollover testing disabled due to improve speed. // but verified working on 11/30/2015 - gfranks // num(16777215), // 2^24 - 1 // num(16777216), // 2^24 // num(16777217), // 2^24 + 1 }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) v, ok := r.Value.([]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 7) So(v[0], ShouldEqual, "10.0.0.0") So(v[1], ShouldEqual, "10.0.0.255") So(v[2], ShouldEqual, "10.0.1.0") // 3rd octet rollover So(v[3], ShouldEqual, "10.0.1.1") So(v[4], ShouldEqual, "10.0.255.255") So(v[5], ShouldEqual, "10.1.0.0") // 2nd octet rollover So(v[6], ShouldEqual, "10.1.0.1") // 1st octet rollover testing disabled due to improve speed. // but verified working on 11/30/2015 - gfranks // So(v[7], ShouldEqual, "10.255.255.255") // So(v[8], ShouldEqual, "11.0.0.0") // 1st octet rollover // So(v[9], ShouldEqual, "11.0.0.1") }) Convey("throws an error if no job name is specified", func() { ev := &Evaluator{ Here: cursor("jobs.0.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if no job instances specified", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if job instances is not a number", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: PI networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if job has no network name", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.5 - 10.0.0.10 ] jobs: - name: job1 instances: 3 networks: - static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no subnets key", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no subnets", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: [] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has no static ranges", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - {} jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network has malformed static range array(s)", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.0.0.1, 10.0.0.254 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if network static range has malformed IP addresses", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: 10.0.0.0.0.0.0.1 - geoff jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if the static address pool is too small", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: 172.16.31.10 - 172.16.31.11 jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(r, ShouldBeNil) }) Convey("throws an error if the address pool ends before it starts", func() { ev := &Evaluator{ Here: cursor("jobs.job1.networks.0.static_ips"), Tree: YAML( `networks: - name: test-network subnets: - static: [ 10.8.0.1 - 10.0.0.255 ] jobs: - name: job1 instances: 3 networks: - name: test-network static_ips: <---------- HERE ----------------- `), } r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)}) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "ends before it starts") So(r, ShouldBeNil) }) }) Convey("inject Operator", t, func() { op := InjectOperator{} ev := &Evaluator{ Tree: YAML( `key: subkey: value: found it other: value 2 subkey2: value: overridden third: trois list1: - first - second list2: - third - fourth `), } Convey("can inject a single sub-map", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Inject) v, ok := r.Value.(map[interface{}]interface{}) So(ok, ShouldBeTrue) So(v["value"], ShouldEqual, "found it") So(v["other"], ShouldEqual, "value 2") }) Convey("can inject multiple sub-maps", func() { r, err := op.Run(ev, []*Expr{ ref("key.subkey"), ref("key.subkey2"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Inject) v, ok := r.Value.(map[interface{}]interface{}) So(ok, ShouldBeTrue) So(len(v), ShouldEqual, 3) So(v["value"], ShouldEqual, "overridden") So(v["other"], ShouldEqual, "value 2") So(v["third"], ShouldEqual, "trois") }) Convey("handles non-existent references", func() { _, err := op.Run(ev, []*Expr{ ref("key.subkey"), ref("key.subkey2"), ref("key.subkey2.ENOENT"), }) So(err, ShouldNotBeNil) }) Convey("throws an error when trying to inject a scalar", func() { _, err := op.Run(ev, []*Expr{ ref("key.subkey.value"), }) So(err, ShouldNotBeNil) }) Convey("throws an error when trying to inject a list", func() { _, err := op.Run(ev, []*Expr{ ref("key.list1"), }) So(err, ShouldNotBeNil) }) }) Convey("param Operator", t, func() { op := ParamOperator{} ev := &Evaluator{} Convey("always causes an error", func() { r, err := op.Run(ev, []*Expr{ str("this is the error"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "this is the error") So(r, ShouldBeNil) }) }) Convey("Join Operator", t, func() { op := JoinOperator{} ev := &Evaluator{ Tree: YAML( `--- meta: authorities: - password.write - clients.write - clients.read - scim.write - scim.read - uaa.admin - clients.secret secondlist: - admin.write - admin.read emptylist: [] anotherkey: - entry1 - somekey: value - entry2 somestanza: foo: bar wom: bat `), } Convey("can join a simple list", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.authorities"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret") }) Convey("can join multiple lists", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.authorities"), ref("meta.secondlist"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret,admin.write,admin.read") }) Convey("can join an empty list", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.emptylist"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "") }) Convey("can join string literals", func() { r, err := op.Run(ev, []*Expr{ str(","), str("password.write"), str("clients.write"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write") }) Convey("can join integer literals", func() { r, err := op.Run(ev, []*Expr{ str(":"), num(4), num(8), num(15), num(16), num(23), num(42), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "4:8:15:16:23:42") }) Convey("can join referenced string entry", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.somestanza.foo"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "bar") }) Convey("can join referenced string entries", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.somestanza.foo"), ref("meta.somestanza.wom"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "bar,bat") }) Convey("can join multiple referenced entries", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.authorities"), ref("meta.somestanza.foo"), ref("meta.somestanza.wom"), str("ending"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret,bar,bat,ending") }) Convey("throws an error when there are no arguments", func() { r, err := op.Run(ev, []*Expr{}) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "no arguments specified") So(r, ShouldBeNil) }) Convey("throws an error when there are too few arguments", func() { r, err := op.Run(ev, []*Expr{ str(","), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "too few arguments supplied") So(r, ShouldBeNil) }) Convey("throws an error when seperator argument is not a literal", func() { r, err := op.Run(ev, []*Expr{ ref("meta.emptylist"), ref("meta.authorities"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "join operator only accepts literal argument for the seperator") So(r, ShouldBeNil) }) Convey("throws an error when referenced entry is not a list or literal", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.somestanza"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "referenced entry is not a list or string") So(r, ShouldBeNil) }) Convey("throws an error when referenced list contains non-string entries", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.anotherkey"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "is not compatible for") So(r, ShouldBeNil) }) Convey("throws an error when there are unresolvable references", func() { r, err := op.Run(ev, []*Expr{ str(","), ref("meta.non-existent"), }) So(err, ShouldNotBeNil) So(err.Error(), ShouldContainSubstring, "Unable to resolve") So(r, ShouldBeNil) }) Convey("calculates dependencies correctly", func() { //TODO: Move this to a higher scope when more dependencies tests are added shouldHaveDeps := func(actual interface{}, expected ...interface{}) string { deps := actual.([]*tree.Cursor) paths := []string{} for _, path := range expected { normalizedPath, err := tree.ParseCursor(path.(string)) if err != nil { panic(fmt.Sprintf("improper path %s passed to test", path.(string))) } paths = append(paths, normalizedPath.String()) } actualPaths := []string{} //make an array so we can give some coherent output on error for _, dep := range deps { //Pass through tree so that tests can tolerate changes to the cursor lib actualPaths = append(actualPaths, dep.String()) } //sort and compare sort.Strings(actualPaths) sort.Strings(paths) match := reflect.DeepEqual(actualPaths, paths) //give result if !match { return fmt.Sprintf("actual: %+v\n expected: %+v", actualPaths, paths) } return "" } Convey("with a single list", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.secondlist"), }, nil) So(deps, shouldHaveDeps, "meta.secondlist.[0]", "meta.secondlist.[1]") }) Convey("with multiple lists", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.authorities"), ref("meta.secondlist"), }, nil) So(deps, shouldHaveDeps, "meta.authorities.[0]", "meta.authorities.[1]", "meta.authorities.[2]", "meta.authorities.[3]", "meta.authorities.[4]", "meta.authorities.[5]", "meta.authorities.[6]", "meta.secondlist.[0]", "meta.secondlist.[1]") }) Convey("with a reference string", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.somestanza.foo"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo") }) Convey("with multiple reference strings", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.somestanza.foo"), ref("meta.somestanza.wom"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo", "meta.somestanza.wom") }) Convey("with a reference string and a list", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), ref("meta.somestanza.foo"), ref("meta.secondlist"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo", "meta.secondlist.[0]", "meta.secondlist.[1]") }) Convey("with a literal string", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), str("literally literal"), }, nil) So(deps, shouldHaveDeps) }) Convey("with a literal string and a reference string", func() { deps := op.Dependencies(ev, []*Expr{ str(" "), str("beep"), ref("meta.somestanza.foo"), }, nil) So(deps, shouldHaveDeps, "meta.somestanza.foo") }) }) }) Convey("empty operator", t, func() { op := EmptyOperator{} ev := &Evaluator{ Tree: YAML( `--- meta: authorities: meep `), } //These three are with unquoted arguments (references) Convey("can replace with a hash", func() { r, err := op.Run(ev, []*Expr{ ref("hash"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isHash := r.Value.(map[string]interface{}) So(isHash, ShouldBeTrue) So(val, ShouldResemble, map[string]interface{}{}) }) Convey("can replace with an array", func() { r, err := op.Run(ev, []*Expr{ ref("array"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isArray := r.Value.([]interface{}) So(isArray, ShouldBeTrue) So(val, ShouldResemble, []interface{}{}) }) Convey("can replace with an empty string", func() { r, err := op.Run(ev, []*Expr{ ref("string"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isString := r.Value.(string) So(isString, ShouldBeTrue) So(val, ShouldEqual, "") }) Convey("throws an error for unrecognized types", func() { r, err := op.Run(ev, []*Expr{ ref("void"), }) So(r, ShouldBeNil) So(err, ShouldNotBeNil) }) Convey("works with string literals", func() { r, err := op.Run(ev, []*Expr{ str("hash"), }) So(err, ShouldBeNil) So(r, ShouldNotBeNil) So(r.Type, ShouldEqual, Replace) val, isHash := r.Value.(map[string]interface{}) So(isHash, ShouldBeTrue) So(val, ShouldResemble, map[string]interface{}{}) }) Convey("throws an error with no args", func() { r, err := op.Run(ev, []*Expr{}) So(r, ShouldBeNil) So(err, ShouldNotBeNil) }) Convey("throws an error with too many args", func() { r, err := op.Run(ev, []*Expr{ ref("hash"), ref("array"), }) So(r, ShouldBeNil) So(err, ShouldNotBeNil) }) }) }
// Cherry-pick ... func (ev *Evaluator) CherryPick(paths []string) error { DEBUG("cherry-picking %d paths from the final YAML structure", len(paths)) if len(paths) > 0 { // This will serve as the replacement tree ... replacement := make(map[interface{}]interface{}) for _, path := range paths { cursor, err := tree.ParseCursor(path) if err != nil { return err } // These variables will potentially be modified (depending on the structure) var cherryName string var cherryValue interface{} // Resolve the value that needs to be cherry picked cherryValue, err = cursor.Resolve(ev.Tree) if err != nil { return err } // Name of the parameter of the to-be-picked value cherryName = cursor.Nodes[len(cursor.Nodes)-1] // Since the cherry can be deep down the structure, we need to go down // (or up, depending how you read it) the structure to include the parent // names of the respective cherry. The pointer will be reassigned with // each level. pointer := cursor for pointer != nil { parent := pointer.Copy() parent.Pop() if parent.String() == "" { // Empty parent string means we reached the root, setting the pointer nil to stop processing ... pointer = nil // ... create the final cherry wrapped in its container ... tmp := make(map[interface{}]interface{}) tmp[cherryName] = cherryValue // ... and add it to the replacement map DEBUG("Merging '%s' into the replacement tree", path) merger := &Merger{AppendByDefault: true} merged := merger.mergeObj(tmp, replacement, path) if err := merger.Error(); err != nil { return err } replacement = merged.(map[interface{}]interface{}) } else { // Reassign the pointer to the parent and restructre the current cherry value to address the parent structure and name pointer = parent // Depending on the type of the parent, either a map or a list is created for the new parent of the cherry value if obj, err := parent.Resolve(ev.Tree); err == nil { switch obj.(type) { case map[interface{}]interface{}: tmp := make(map[interface{}]interface{}) tmp[cherryName] = cherryValue cherryName = parent.Nodes[len(parent.Nodes)-1] cherryValue = tmp case []interface{}: tmp := make([]interface{}, 0, 0) tmp = append(tmp, cherryValue) cherryName = parent.Nodes[len(parent.Nodes)-1] cherryValue = tmp default: return ansi.Errorf("@*{Unsupported type detected, %s is neither a map nor a list}", parent.String()) } } else { return err } } } } // replace the existing tree with a new one that contain the cherry-picks ev.Tree = replacement } DEBUG("") return nil }