func Example_variables() { input := "${var.test} - ${6 + 2}" tree, err := hil.Parse(input) if err != nil { log.Fatal(err) } config := &hil.EvalConfig{ GlobalScope: &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.test": ast.Variable{ Type: ast.TypeString, Value: "TEST STRING", }, }, }, } result, err := hil.Eval(tree, config) if err != nil { log.Fatal(err) } fmt.Printf("Type: %s\n", result.Type) fmt.Printf("Value: %s\n", result.Value) // Output: // Type: TypeString // Value: TEST STRING - 8 }
// Compile validates a prepared query template and returns an opaque compiled // object that can be used later to render the template. func Compile(query *structs.PreparedQuery) (*CompiledTemplate, error) { // Make sure it's a type we understand. if query.Template.Type != structs.QueryTemplateTypeNamePrefixMatch { return nil, fmt.Errorf("Bad Template.Type '%s'", query.Template.Type) } // Start compile. ct := &CompiledTemplate{ trees: make(map[string]ast.Node), } // Make a copy of the query to use as the basis for rendering later. dup, err := copystructure.Copy(query) if err != nil { return nil, err } var ok bool ct.query, ok = dup.(*structs.PreparedQuery) if !ok { return nil, fmt.Errorf("Failed to copy query") } // Walk over all the string fields in the Service sub-structure and // parse them as HIL. parse := func(path string, v reflect.Value) error { tree, err := hil.Parse(v.String()) if err != nil { return fmt.Errorf("Bad format '%s' in Service%s: %s", v.String(), path, err) } ct.trees[path] = tree return nil } if err := walk(&ct.query.Service, parse); err != nil { return nil, err } // If they supplied a regexp then compile it. if ct.query.Template.Regexp != "" { var err error ct.re, err = regexp.Compile(ct.query.Template.Regexp) if err != nil { return nil, fmt.Errorf("Bad Regexp: %s", err) } } // Finally do a test render with the supplied name prefix. This will // help catch errors before run time, and this is the most minimal // prefix it will be expected to run with. The results might not make // sense and create a valid service to lookup, but it should render // without any errors. if _, err = ct.Render(ct.query.Name); err != nil { return nil, err } return ct, nil }
func TestDetectVariables(t *testing.T) { cases := []struct { Input string Result []InterpolatedVariable }{ { "foo $${var.foo}", nil, }, { "foo ${var.foo}", []InterpolatedVariable{ &UserVariable{ Name: "foo", key: "var.foo", }, }, }, { "foo ${var.foo} ${var.bar}", []InterpolatedVariable{ &UserVariable{ Name: "foo", key: "var.foo", }, &UserVariable{ Name: "bar", key: "var.bar", }, }, }, } for _, tc := range cases { ast, err := hil.Parse(tc.Input) if err != nil { t.Fatalf("%s\n\nInput: %s", err, tc.Input) } actual, err := DetectVariables(ast) if err != nil { t.Fatalf("err: %s", err) } if !reflect.DeepEqual(actual, tc.Result) { t.Fatalf("bad: %#v\n\nInput: %s", actual, tc.Input) } } }
// execute parses and executes a template using vars. func execute(s string, vars map[string]interface{}) (string, error) { root, err := hil.Parse(s) if err != nil { return "", err } varmap := make(map[string]ast.Variable) for k, v := range vars { // As far as I can tell, v is always a string. // If it's not, tell the user gracefully. s, ok := v.(string) if !ok { return "", fmt.Errorf("unexpected type for variable %q: %T", k, v) } // Store the defaults (string and value) var val interface{} = s typ := ast.TypeString // If we can parse a float, then use that if v, err := strconv.ParseFloat(s, 64); err == nil { val = v typ = ast.TypeFloat } varmap[k] = ast.Variable{ Value: val, Type: typ, } } cfg := hil.EvalConfig{ GlobalScope: &ast.BasicScope{ VarMap: varmap, FuncMap: config.Funcs(), }, } result, err := hil.Eval(root, &cfg) if err != nil { return "", err } if result.Type != hil.TypeString { return "", fmt.Errorf("unexpected output hil.Type: %v", result.Type) } return result.Value.(string), nil }
func testFunction(t *testing.T, config testFunctionConfig) { for i, tc := range config.Cases { ast, err := hil.Parse(tc.Input) if err != nil { t.Fatalf("Case #%d: input: %#v\nerr: %s", i, tc.Input, err) } result, err := hil.Eval(ast, langEvalConfig(config.Vars)) if err != nil != tc.Error { t.Fatalf("Case #%d:\ninput: %#v\nerr: %s", i, tc.Input, err) } if !reflect.DeepEqual(result.Value, tc.Result) { t.Fatalf("%d: bad output for input: %s\n\nOutput: %#v\nExpected: %#v", i, tc.Input, result.Value, tc.Result) } } }
func Example_functions() { input := "${lower(var.test)} - ${6 + 2}" tree, err := hil.Parse(input) if err != nil { log.Fatal(err) } lowerCase := ast.Function{ ArgTypes: []ast.Type{ast.TypeString}, ReturnType: ast.TypeString, Variadic: false, Callback: func(inputs []interface{}) (interface{}, error) { input := inputs[0].(string) return strings.ToLower(input), nil }, } config := &hil.EvalConfig{ GlobalScope: &ast.BasicScope{ VarMap: map[string]ast.Variable{ "var.test": ast.Variable{ Type: ast.TypeString, Value: "TEST STRING", }, }, FuncMap: map[string]ast.Function{ "lower": lowerCase, }, }, } result, err := hil.Eval(tree, config) if err != nil { log.Fatal(err) } fmt.Printf("Type: %s\n", result.Type) fmt.Printf("Value: %s\n", result.Value) // Output: // Type: TypeString // Value: test string - 8 }
func Example_basic() { input := "${6 + 2}" tree, err := hil.Parse(input) if err != nil { log.Fatal(err) } value, valueType, err := hil.Eval(tree, &hil.EvalConfig{}) if err != nil { log.Fatal(err) } fmt.Printf("Type: %s\n", valueType) fmt.Printf("Value: %s\n", value) // Output: // Type: TypeString // Value: 8 }
func TestInterpolateFuncTimestamp(t *testing.T) { currentTime := time.Now().UTC() ast, err := hil.Parse("${timestamp()}") if err != nil { t.Fatalf("err: %s", err) } result, err := hil.Eval(ast, langEvalConfig(nil)) if err != nil { t.Fatalf("err: %s", err) } resultTime, err := time.Parse(time.RFC3339, result.Value.(string)) if err != nil { t.Fatalf("Error parsing timestamp: %s", err) } if resultTime.Sub(currentTime).Seconds() > 10.0 { t.Fatalf("Timestamp Diff too large. Expected: %s\nRecieved: %s", currentTime.Format(time.RFC3339), result.Value.(string)) } }
func TestInterpolateFuncUUID(t *testing.T) { results := make(map[string]bool) for i := 0; i < 100; i++ { ast, err := hil.Parse("${uuid()}") if err != nil { t.Fatalf("err: %s", err) } result, err := hil.Eval(ast, langEvalConfig(nil)) if err != nil { t.Fatalf("err: %s", err) } if results[result.Value.(string)] { t.Fatalf("Got unexpected duplicate uuid: %s", result.Value) } results[result.Value.(string)] = true } }
// DetectUserVariables parses the template for any ${var.foo}, ${var.bar}, // etc.. user variables. It returns a list of found variables with, example: // []string{"foo", "bar"}. The returned list only contains unique names, so any // user variable which declared multiple times is neglected, only the last // occurence is being added. func (t *Template) DetectUserVariables(prefix string) (map[string]string, error) { if !strings.HasSuffix(prefix, "_") { prefix = prefix + "_" } out, err := t.JsonOutput() if err != nil { return nil, err } // get AST first, it's capable of parsing json a, err := hil.Parse(out) if err != nil { return nil, err } // read the variables from the given AST. This is basically just iterating // over the AST node and does the heavy lifting for us vars, err := config.DetectVariables(a) if err != nil { return nil, err } // filter out duplicates userVars := make(map[string]string, 0) for _, v := range vars { // be sure we only get userVariables, as there is many ways of // declaring variables u, ok := v.(*config.UserVariable) if !ok { continue } if _, ok = userVars[u.Name]; !ok && strings.HasPrefix(u.Name, prefix) { userVars[u.Name] = "" } } return userVars, nil }
// execute parses and executes a template using vars. func execute(s string, vars map[string]interface{}) (string, error) { root, err := hil.Parse(s) if err != nil { return "", err } varmap := make(map[string]ast.Variable) for k, v := range vars { // As far as I can tell, v is always a string. // If it's not, tell the user gracefully. s, ok := v.(string) if !ok { return "", fmt.Errorf("unexpected type for variable %q: %T", k, v) } varmap[k] = ast.Variable{ Value: s, Type: ast.TypeString, } } cfg := hil.EvalConfig{ GlobalScope: &ast.BasicScope{ VarMap: varmap, FuncMap: config.Funcs(), }, } result, err := hil.Eval(root, &cfg) if err != nil { return "", err } if result.Type != hil.TypeString { return "", fmt.Errorf("unexpected output hil.Type: %v", result.Type) } return result.Value.(string), nil }
func Eval(tmpl string, variables Variables) (string, error) { tree, err := hil.Parse(tmpl) if err != nil { return "", err } varMap := make(map[string]ast.Variable) for n, v := range variables { varMap[n] = ast.Variable{Type: ast.TypeString, Value: v} } config := &hil.EvalConfig{ GlobalScope: &ast.BasicScope{VarMap: varMap}, } out, _, err := hil.Eval(tree, config) if err != nil { return "", err } if s, ok := out.(string); ok { return s, nil } else { return "", fmt.Errorf("This is a bug: expected to find string, but found %s.\nPlease report that to %s", reflect.TypeOf(out), bugTracker) } }
func (w *interpolationWalker) Primitive(v reflect.Value) error { setV := v // We only care about strings if v.Kind() == reflect.Interface { setV = v v = v.Elem() } if v.Kind() != reflect.String { return nil } astRoot, err := hil.Parse(v.String()) if err != nil { return err } // If the AST we got is just a literal string value with the same // value then we ignore it. We have to check if its the same value // because it is possible to input a string, get out a string, and // have it be different. For example: "foo-$${bar}" turns into // "foo-${bar}" if n, ok := astRoot.(*ast.LiteralNode); ok { if s, ok := n.Value.(string); ok && s == v.String() { return nil } } if w.ContextF != nil { w.ContextF(w.loc, astRoot) } if w.F == nil { return nil } replaceVal, err := w.F(astRoot) if err != nil { return fmt.Errorf( "%s in:\n\n%s", err, v.String()) } if w.Replace { // We need to determine if we need to remove this element // if the result contains any "UnknownVariableValue" which is // set if it is computed. This behavior is different if we're // splitting (in a SliceElem) or not. remove := false if w.loc == reflectwalk.SliceElem { switch typedReplaceVal := replaceVal.(type) { case string: if typedReplaceVal == UnknownVariableValue { remove = true } case []interface{}: if hasUnknownValue(typedReplaceVal) { remove = true } } } else if replaceVal == UnknownVariableValue { remove = true } if remove { w.unknownKeys = append(w.unknownKeys, strings.Join(w.key, ".")) } resultVal := reflect.ValueOf(replaceVal) switch w.loc { case reflectwalk.MapKey: m := w.cs[len(w.cs)-1] // Delete the old value var zero reflect.Value m.SetMapIndex(w.csData.(reflect.Value), zero) // Set the new key with the existing value m.SetMapIndex(resultVal, w.lastValue) // Set the key to be the new key w.csData = resultVal case reflectwalk.MapValue: // If we're in a map, then the only way to set a map value is // to set it directly. m := w.cs[len(w.cs)-1] mk := w.csData.(reflect.Value) m.SetMapIndex(mk, resultVal) default: // Otherwise, we should be addressable setV.Set(resultVal) } } return nil }