From b83088f5c86b52a723fac11f3f139070840d3a3c Mon Sep 17 00:00:00 2001 From: Matt Keeler Date: Wed, 7 Sep 2022 15:34:57 -0400 Subject: [PATCH] WIP: Variable value interpolation This is intended to allow the caller to supply some set of variables to be used when evaluating an expression. This could be preferable to the caller templating an expression for 2 reasons: 1. It allows the parsed expression to be reused with different values which could improve performance if the same general expression is evaluated many times. 2. Variables are processed after general parsing and thus could not mess with how things are parsed. --- bexpr.go | 3 +- evaluate.go | 51 ++++--- evaluate_test.go | 12 +- examples/expr-eval/expr-eval.go | 53 +++++--- examples/simple/simple.go | 2 +- filter.go | 4 +- grammar/ast.go | 5 +- grammar/grammar.go | 234 +++++++++++++++++++++----------- grammar/grammar.peg | 6 + options.go | 7 + 10 files changed, 246 insertions(+), 131 deletions(-) diff --git a/bexpr.go b/bexpr.go index d39822a..08be357 100644 --- a/bexpr.go +++ b/bexpr.go @@ -48,10 +48,11 @@ func CreateEvaluator(expression string, opts ...Option) (*Evaluator, error) { return eval, nil } -func (eval *Evaluator) Evaluate(datum interface{}) (bool, error) { +func (eval *Evaluator) Evaluate(datum interface{}, variables map[string]string) (bool, error) { opts := []Option{ WithTagName(eval.tagName), WithHookFn(eval.valueTransformationHook), + withVariables(variables), } if eval.unknownVal != nil { opts = append(opts, WithUnknownValue(*eval.unknownVal)) diff --git a/evaluate.go b/evaluate.go index ba3db26..a8a776f 100644 --- a/evaluate.go +++ b/evaluate.go @@ -66,7 +66,7 @@ func derefType(rtype reflect.Type) reflect.Type { return rtype } -func doMatchMatches(expression *grammar.MatchExpression, value reflect.Value) (bool, error) { +func doMatchMatches(expression *grammar.MatchExpression, value reflect.Value, variables map[string]string) (bool, error) { if !value.Type().ConvertibleTo(byteSliceTyp) { return false, fmt.Errorf("Value of type %s is not convertible to []byte", value.Type()) } @@ -88,21 +88,21 @@ func doMatchMatches(expression *grammar.MatchExpression, value reflect.Value) (b return re.Match(value.Convert(byteSliceTyp).Interface().([]byte)), nil } -func doMatchEqual(expression *grammar.MatchExpression, value reflect.Value) (bool, error) { +func doMatchEqual(expression *grammar.MatchExpression, value reflect.Value, variables map[string]string) (bool, error) { // NOTE: see preconditions in evaluategrammar.MatchExpressionRecurse eqFn := primitiveEqualityFn(value.Kind()) if eqFn == nil { return false, errors.New("unable to find suitable primitive comparison function for matching") } - matchValue, err := getMatchExprValue(expression, value.Kind()) + matchValue, err := getMatchExprValue(expression, value.Type(), variables) if err != nil { return false, fmt.Errorf("error getting match value in expression: %w", err) } return eqFn(matchValue, value), nil } -func doMatchIn(expression *grammar.MatchExpression, value reflect.Value) (bool, error) { - matchValue, err := getMatchExprValue(expression, value.Kind()) +func doMatchIn(expression *grammar.MatchExpression, value reflect.Value, variables map[string]string) (bool, error) { + matchValue, err := getMatchExprValue(expression, value.Type(), variables) if err != nil { return false, fmt.Errorf("error getting match value in expression: %w", err) } @@ -134,7 +134,7 @@ func doMatchIn(expression *grammar.MatchExpression, value reflect.Value) (bool, // syntax errors, so as a special case in this situation, don't // error on a strconv.ErrSyntax, just continue on to the next // element. - matchValue, err = getMatchExprValue(expression, kind) + matchValue, err = getMatchExprValue(expression, itemType, variables) if err != nil { if errors.Is(err, strconv.ErrSyntax) { continue @@ -156,7 +156,7 @@ func doMatchIn(expression *grammar.MatchExpression, value reflect.Value) (bool, // Otherwise it's a concrete type and we can essentially cache the // answers. First we need to re-derive the match value for equality // assertion. - matchValue, err = getMatchExprValue(expression, kind) + matchValue, err = getMatchExprValue(expression, itemType, variables) if err != nil { return false, fmt.Errorf("error getting match value in expression: %w", err) } @@ -187,29 +187,38 @@ func doMatchIsEmpty(matcher *grammar.MatchExpression, value reflect.Value) (bool return value.Len() == 0, nil } -func getMatchExprValue(expression *grammar.MatchExpression, rvalue reflect.Kind) (interface{}, error) { +func getMatchExprValue(expression *grammar.MatchExpression, rvalue reflect.Type, variables map[string]string) (interface{}, error) { if expression.Value == nil { return nil, nil } - switch rvalue { + val := expression.Value.Raw + if expression.Value.IsVariable { + val, _ = variables[expression.Value.Raw] + } + + if val == "" { + val = fmt.Sprintf("%v", reflect.Zero(rvalue).Interface()) + } + + switch rvalue.Kind() { case reflect.Bool: - return CoerceBool(expression.Value.Raw) + return CoerceBool(val) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return CoerceInt64(expression.Value.Raw) + return CoerceInt64(val) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - return CoerceUint64(expression.Value.Raw) + return CoerceUint64(val) case reflect.Float32: - return CoerceFloat32(expression.Value.Raw) + return CoerceFloat32(val) case reflect.Float64: - return CoerceFloat64(expression.Value.Raw) + return CoerceFloat64(val) default: - return expression.Value.Raw, nil + return val, nil } } @@ -247,17 +256,17 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac rvalue := reflect.Indirect(reflect.ValueOf(val)) switch expression.Operator { case grammar.MatchEqual: - return doMatchEqual(expression, rvalue) + return doMatchEqual(expression, rvalue, opts.withVariables) case grammar.MatchNotEqual: - result, err := doMatchEqual(expression, rvalue) + result, err := doMatchEqual(expression, rvalue, opts.withVariables) if err == nil { return !result, nil } return false, err case grammar.MatchIn: - return doMatchIn(expression, rvalue) + return doMatchIn(expression, rvalue, opts.withVariables) case grammar.MatchNotIn: - result, err := doMatchIn(expression, rvalue) + result, err := doMatchIn(expression, rvalue, opts.withVariables) if err == nil { return !result, nil } @@ -271,9 +280,9 @@ func evaluateMatchExpression(expression *grammar.MatchExpression, datum interfac } return false, err case grammar.MatchMatches: - return doMatchMatches(expression, rvalue) + return doMatchMatches(expression, rvalue, opts.withVariables) case grammar.MatchNotMatches: - result, err := doMatchMatches(expression, rvalue) + result, err := doMatchMatches(expression, rvalue, opts.withVariables) if err == nil { return !result, nil } diff --git a/evaluate_test.go b/evaluate_test.go index 3acaece..cc59850 100644 --- a/evaluate_test.go +++ b/evaluate_test.go @@ -331,7 +331,7 @@ func TestEvaluate(t *testing.T) { expr, err := CreateEvaluator(expTest.expression, WithHookFn(expTest.hook)) require.NoError(t, err) - match, err := expr.Evaluate(tcase.value) + match, err := expr.Evaluate(tcase.value, nil) if expTest.err != "" { require.Error(t, err) require.EqualError(t, err, expTest.err) @@ -402,7 +402,7 @@ func TestWithHookFn(t *testing.T) { expr, err := CreateEvaluator(eval.expression, WithHookFn(tc.hook)) require.NoError(t, err) - match, err := expr.Evaluate(tc.in) + match, err := expr.Evaluate(tc.in, nil) if eval.err != "" { require.Error(t, err) require.Equal(t, eval.err, err.Error()) @@ -452,7 +452,7 @@ func TestUnknownVal(t *testing.T) { match, err := expr.Evaluate(map[string]string{ "key": "foo", - }) + }, nil) if tc.err != "" { require.Error(t, err) require.EqualError(t, err, tc.err) @@ -503,7 +503,7 @@ func TestUnknownVal_struct(t *testing.T) { Key string `bexpr:"key"` }{ Key: "foo", - }) + }, nil) if tc.err != "" { require.Error(t, err) require.EqualError(t, err, tc.err) @@ -562,7 +562,7 @@ func TestCustomTag(t *testing.T) { expr, err := CreateEvaluator(tc.expression, opts...) require.NoError(t, err) - match, err := expr.Evaluate(ts) + match, err := expr.Evaluate(ts, nil) if tc.jsonTag { if tc.jnameFound { require.NoError(t, err) @@ -601,7 +601,7 @@ func BenchmarkEvaluate(b *testing.B) { b.ResetTimer() for n := 0; n < b.N; n++ { - _, err = expr.Evaluate(tcase.value) + _, err = expr.Evaluate(tcase.value, nil) if expTest.err != "" { require.Error(b, err) } else { diff --git a/examples/expr-eval/expr-eval.go b/examples/expr-eval/expr-eval.go index d13a6cf..ea143ce 100644 --- a/examples/expr-eval/expr-eval.go +++ b/examples/expr-eval/expr-eval.go @@ -45,69 +45,80 @@ var data Matchable = Matchable{ Values: []int{1, 2, 3, 4, 5}, }, SliceInternal: []Internal{ - Internal{ + { Name: "odd", Values: []int{1, 3, 5, 7, 9}, }, - Internal{ + { Name: "even", Values: []int{2, 4, 6, 8, 10}, }, - Internal{ + { Name: "fib", Values: []int{0, 1, 1, 2, 3, 5}, }, }, MapInternal: map[string]Internal{ - "odd": Internal{ + "odd": { Name: "odd", Values: []int{1, 3, 5, 7, 9}, }, - "even": Internal{ + "even": { Name: "even", Values: []int{2, 4, 6, 8, 10}, }, - "fib": Internal{ + "fib": { Name: "fib", Values: []int{0, 1, 1, 2, 3, 5}, }, }, } -var expressions []string = []string{ +type example struct { + expression string + variables map[string]string +} + +var examples []example = []example{ // should error out in creating the evaluator as Foo is not a valid selector - "Foo == 3", + {expression: "Foo == 3"}, // should error out because the field is hidden - "Internal.Hidden == 5", + {expression: "Internal.Hidden == 5"}, // should error out because the field is not exported - "Internal.unexported == 3", + {expression: "Internal.unexported == 3"}, // should evaluate to true - "Map[`abc`] == `def`", + {expression: "Map[`abc`] == `def`"}, // should evaluate to false - "X == 3", + {expression: "X == 3"}, // should evaluate to true - "Internal.fields is not empty", + {expression: "Internal.fields is not empty"}, // should evaluate to false - "MapInternal.fib.Name != fib", + {expression: "MapInternal.fib.Name != fib"}, // should evaluate to true - "odd in MapInternal", + {expression: "odd in MapInternal"}, + // variable interpolation - should evaluate to true + {expression: "X == ${value}", variables: map[string]string{"value": "5"}}, + // variable interpolation - should evaluate to false + {expression: "X == ${value}", variables: map[string]string{"value": "4"}}, + // variable interpolation default value - should evaluate to false + {expression: "X == ${value}"}, } func main() { - for _, expression := range expressions { - eval, err := bexpr.CreateEvaluator(expression) + for _, ex := range examples { + eval, err := bexpr.CreateEvaluator(ex.expression) if err != nil { - fmt.Printf("Failed to create evaluator for expression %q: %v\n", expression, err) + fmt.Printf("Failed to create evaluator for expression %q: %v\n", ex.expression, err) continue } - result, err := eval.Evaluate(data) + result, err := eval.Evaluate(data, ex.variables) if err != nil { - fmt.Printf("Failed to run evaluation of expression %q: %v\n", expression, err) + fmt.Printf("Failed to run evaluation of expression %q (variables %#v): %v\n", ex.expression, ex.variables, err) continue } - fmt.Printf("Result of expression %q evaluation: %t\n", expression, result) + fmt.Printf("Result of expression %q evaluation (variables: %#v): %t\n", ex.expression, ex.variables, result) } } diff --git a/examples/simple/simple.go b/examples/simple/simple.go index 6f01c51..6be68f6 100644 --- a/examples/simple/simple.go +++ b/examples/simple/simple.go @@ -46,7 +46,7 @@ func main() { continue } - result, err := eval.Evaluate(value) + result, err := eval.Evaluate(value, nil) if err != nil { fmt.Printf("Failed to run evaluation of expression %q: %v\n", expression, err) continue diff --git a/filter.go b/filter.go index 390d57c..cee5086 100644 --- a/filter.go +++ b/filter.go @@ -53,7 +53,7 @@ func (f *Filter) Execute(data interface{}) (interface{}, error) { if !item.CanInterface() { return nil, fmt.Errorf("Slice/Array value can not be used") } - result, err := f.evaluator.Evaluate(item.Interface()) + result, err := f.evaluator.Evaluate(item.Interface(), nil) if err != nil { return nil, err } @@ -76,7 +76,7 @@ func (f *Filter) Execute(data interface{}) (interface{}, error) { return nil, fmt.Errorf("Map value cannot be used") } - result, err := f.evaluator.Evaluate(item.Interface()) + result, err := f.evaluator.Evaluate(item.Interface(), nil) if err != nil { return nil, err } diff --git a/grammar/ast.go b/grammar/ast.go index bf6e6c1..17e8435 100644 --- a/grammar/ast.go +++ b/grammar/ast.go @@ -82,8 +82,9 @@ func (op MatchOperator) String() string { } type MatchValue struct { - Raw string - Converted interface{} + Raw string + Converted interface{} + IsVariable bool } type UnaryExpression struct { diff --git a/grammar/grammar.go b/grammar/grammar.go index 7aee78a..846e98b 100644 --- a/grammar/grammar.go +++ b/grammar/grammar.go @@ -1323,39 +1323,51 @@ var g = &grammar{ }, }, }, + &actionExpr{ + pos: position{line: 170, col: 5, offset: 4617}, + run: (*parser).callonValue11, + expr: &labeledExpr{ + pos: position{line: 170, col: 5, offset: 4617}, + label: "s", + expr: &ruleRefExpr{ + pos: position{line: 170, col: 7, offset: 4619}, + name: "VariableInterpolation", + }, + }, + }, }, }, }, { name: "NumberLiteral", displayName: "\"number\"", - pos: position{line: 172, col: 1, offset: 4616}, + pos: position{line: 174, col: 1, offset: 4708}, expr: &choiceExpr{ - pos: position{line: 172, col: 27, offset: 4642}, + pos: position{line: 174, col: 27, offset: 4734}, alternatives: []interface{}{ &actionExpr{ - pos: position{line: 172, col: 27, offset: 4642}, + pos: position{line: 174, col: 27, offset: 4734}, run: (*parser).callonNumberLiteral2, expr: &seqExpr{ - pos: position{line: 172, col: 27, offset: 4642}, + pos: position{line: 174, col: 27, offset: 4734}, exprs: []interface{}{ &zeroOrOneExpr{ - pos: position{line: 172, col: 27, offset: 4642}, + pos: position{line: 174, col: 27, offset: 4734}, expr: &litMatcher{ - pos: position{line: 172, col: 27, offset: 4642}, + pos: position{line: 174, col: 27, offset: 4734}, val: "-", ignoreCase: false, want: "\"-\"", }, }, &ruleRefExpr{ - pos: position{line: 172, col: 32, offset: 4647}, + pos: position{line: 174, col: 32, offset: 4739}, name: "IntegerOrFloat", }, &andExpr{ - pos: position{line: 172, col: 47, offset: 4662}, + pos: position{line: 174, col: 47, offset: 4754}, expr: &ruleRefExpr{ - pos: position{line: 172, col: 48, offset: 4663}, + pos: position{line: 174, col: 48, offset: 4755}, name: "AfterNumbers", }, }, @@ -1363,30 +1375,30 @@ var g = &grammar{ }, }, &seqExpr{ - pos: position{line: 174, col: 5, offset: 4712}, + pos: position{line: 176, col: 5, offset: 4804}, exprs: []interface{}{ &zeroOrOneExpr{ - pos: position{line: 174, col: 5, offset: 4712}, + pos: position{line: 176, col: 5, offset: 4804}, expr: &litMatcher{ - pos: position{line: 174, col: 5, offset: 4712}, + pos: position{line: 176, col: 5, offset: 4804}, val: "-", ignoreCase: false, want: "\"-\"", }, }, &ruleRefExpr{ - pos: position{line: 174, col: 10, offset: 4717}, + pos: position{line: 176, col: 10, offset: 4809}, name: "IntegerOrFloat", }, ¬Expr{ - pos: position{line: 174, col: 25, offset: 4732}, + pos: position{line: 176, col: 25, offset: 4824}, expr: &ruleRefExpr{ - pos: position{line: 174, col: 26, offset: 4733}, + pos: position{line: 176, col: 26, offset: 4825}, name: "AfterNumbers", }, }, &andCodeExpr{ - pos: position{line: 174, col: 39, offset: 4746}, + pos: position{line: 176, col: 39, offset: 4838}, run: (*parser).callonNumberLiteral15, }, }, @@ -1394,24 +1406,72 @@ var g = &grammar{ }, }, }, + { + name: "VariableInterpolation", + displayName: "\"variable\"", + pos: position{line: 180, col: 1, offset: 4898}, + expr: &actionExpr{ + pos: position{line: 180, col: 37, offset: 4934}, + run: (*parser).callonVariableInterpolation1, + expr: &seqExpr{ + pos: position{line: 180, col: 37, offset: 4934}, + exprs: []interface{}{ + &litMatcher{ + pos: position{line: 180, col: 37, offset: 4934}, + val: "${", + ignoreCase: false, + want: "\"${\"", + }, + &zeroOrOneExpr{ + pos: position{line: 180, col: 42, offset: 4939}, + expr: &ruleRefExpr{ + pos: position{line: 180, col: 42, offset: 4939}, + name: "_", + }, + }, + &labeledExpr{ + pos: position{line: 180, col: 45, offset: 4942}, + label: "id", + expr: &ruleRefExpr{ + pos: position{line: 180, col: 48, offset: 4945}, + name: "Identifier", + }, + }, + &zeroOrOneExpr{ + pos: position{line: 180, col: 59, offset: 4956}, + expr: &ruleRefExpr{ + pos: position{line: 180, col: 59, offset: 4956}, + name: "_", + }, + }, + &litMatcher{ + pos: position{line: 180, col: 62, offset: 4959}, + val: "}", + ignoreCase: false, + want: "\"}\"", + }, + }, + }, + }, + }, { name: "AfterNumbers", - pos: position{line: 178, col: 1, offset: 4806}, + pos: position{line: 184, col: 1, offset: 5003}, expr: &andExpr{ - pos: position{line: 178, col: 17, offset: 4822}, + pos: position{line: 184, col: 17, offset: 5019}, expr: &choiceExpr{ - pos: position{line: 178, col: 19, offset: 4824}, + pos: position{line: 184, col: 19, offset: 5021}, alternatives: []interface{}{ &ruleRefExpr{ - pos: position{line: 178, col: 19, offset: 4824}, + pos: position{line: 184, col: 19, offset: 5021}, name: "_", }, &ruleRefExpr{ - pos: position{line: 178, col: 23, offset: 4828}, + pos: position{line: 184, col: 23, offset: 5025}, name: "EOF", }, &litMatcher{ - pos: position{line: 178, col: 29, offset: 4834}, + pos: position{line: 184, col: 29, offset: 5031}, val: ")", ignoreCase: false, want: "\")\"", @@ -1422,33 +1482,33 @@ var g = &grammar{ }, { name: "IntegerOrFloat", - pos: position{line: 180, col: 1, offset: 4840}, + pos: position{line: 186, col: 1, offset: 5037}, expr: &seqExpr{ - pos: position{line: 180, col: 19, offset: 4858}, + pos: position{line: 186, col: 19, offset: 5055}, exprs: []interface{}{ &choiceExpr{ - pos: position{line: 180, col: 20, offset: 4859}, + pos: position{line: 186, col: 20, offset: 5056}, alternatives: []interface{}{ &litMatcher{ - pos: position{line: 180, col: 20, offset: 4859}, + pos: position{line: 186, col: 20, offset: 5056}, val: "0", ignoreCase: false, want: "\"0\"", }, &seqExpr{ - pos: position{line: 180, col: 26, offset: 4865}, + pos: position{line: 186, col: 26, offset: 5062}, exprs: []interface{}{ &charClassMatcher{ - pos: position{line: 180, col: 26, offset: 4865}, + pos: position{line: 186, col: 26, offset: 5062}, val: "[1-9]", ranges: []rune{'1', '9'}, ignoreCase: false, inverted: false, }, &zeroOrMoreExpr{ - pos: position{line: 180, col: 31, offset: 4870}, + pos: position{line: 186, col: 31, offset: 5067}, expr: &charClassMatcher{ - pos: position{line: 180, col: 31, offset: 4870}, + pos: position{line: 186, col: 31, offset: 5067}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, @@ -1460,20 +1520,20 @@ var g = &grammar{ }, }, &zeroOrOneExpr{ - pos: position{line: 180, col: 39, offset: 4878}, + pos: position{line: 186, col: 39, offset: 5075}, expr: &seqExpr{ - pos: position{line: 180, col: 40, offset: 4879}, + pos: position{line: 186, col: 40, offset: 5076}, exprs: []interface{}{ &litMatcher{ - pos: position{line: 180, col: 40, offset: 4879}, + pos: position{line: 186, col: 40, offset: 5076}, val: ".", ignoreCase: false, want: "\".\"", }, &oneOrMoreExpr{ - pos: position{line: 180, col: 44, offset: 4883}, + pos: position{line: 186, col: 44, offset: 5080}, expr: &charClassMatcher{ - pos: position{line: 180, col: 44, offset: 4883}, + pos: position{line: 186, col: 44, offset: 5080}, val: "[0-9]", ranges: []rune{'0', '9'}, ignoreCase: false, @@ -1489,34 +1549,34 @@ var g = &grammar{ { name: "StringLiteral", displayName: "\"string\"", - pos: position{line: 182, col: 1, offset: 4893}, + pos: position{line: 188, col: 1, offset: 5090}, expr: &choiceExpr{ - pos: position{line: 182, col: 27, offset: 4919}, + pos: position{line: 188, col: 27, offset: 5116}, alternatives: []interface{}{ &actionExpr{ - pos: position{line: 182, col: 27, offset: 4919}, + pos: position{line: 188, col: 27, offset: 5116}, run: (*parser).callonStringLiteral2, expr: &choiceExpr{ - pos: position{line: 182, col: 28, offset: 4920}, + pos: position{line: 188, col: 28, offset: 5117}, alternatives: []interface{}{ &seqExpr{ - pos: position{line: 182, col: 28, offset: 4920}, + pos: position{line: 188, col: 28, offset: 5117}, exprs: []interface{}{ &litMatcher{ - pos: position{line: 182, col: 28, offset: 4920}, + pos: position{line: 188, col: 28, offset: 5117}, val: "`", ignoreCase: false, want: "\"`\"", }, &zeroOrMoreExpr{ - pos: position{line: 182, col: 32, offset: 4924}, + pos: position{line: 188, col: 32, offset: 5121}, expr: &ruleRefExpr{ - pos: position{line: 182, col: 32, offset: 4924}, + pos: position{line: 188, col: 32, offset: 5121}, name: "RawStringChar", }, }, &litMatcher{ - pos: position{line: 182, col: 47, offset: 4939}, + pos: position{line: 188, col: 47, offset: 5136}, val: "`", ignoreCase: false, want: "\"`\"", @@ -1524,23 +1584,23 @@ var g = &grammar{ }, }, &seqExpr{ - pos: position{line: 182, col: 53, offset: 4945}, + pos: position{line: 188, col: 53, offset: 5142}, exprs: []interface{}{ &litMatcher{ - pos: position{line: 182, col: 53, offset: 4945}, + pos: position{line: 188, col: 53, offset: 5142}, val: "\"", ignoreCase: false, want: "\"\\\"\"", }, &zeroOrMoreExpr{ - pos: position{line: 182, col: 57, offset: 4949}, + pos: position{line: 188, col: 57, offset: 5146}, expr: &ruleRefExpr{ - pos: position{line: 182, col: 57, offset: 4949}, + pos: position{line: 188, col: 57, offset: 5146}, name: "DoubleStringChar", }, }, &litMatcher{ - pos: position{line: 182, col: 75, offset: 4967}, + pos: position{line: 188, col: 75, offset: 5164}, val: "\"", ignoreCase: false, want: "\"\\\"\"", @@ -1551,42 +1611,42 @@ var g = &grammar{ }, }, &seqExpr{ - pos: position{line: 184, col: 5, offset: 5019}, + pos: position{line: 190, col: 5, offset: 5216}, exprs: []interface{}{ &choiceExpr{ - pos: position{line: 184, col: 6, offset: 5020}, + pos: position{line: 190, col: 6, offset: 5217}, alternatives: []interface{}{ &seqExpr{ - pos: position{line: 184, col: 6, offset: 5020}, + pos: position{line: 190, col: 6, offset: 5217}, exprs: []interface{}{ &litMatcher{ - pos: position{line: 184, col: 6, offset: 5020}, + pos: position{line: 190, col: 6, offset: 5217}, val: "`", ignoreCase: false, want: "\"`\"", }, &zeroOrMoreExpr{ - pos: position{line: 184, col: 10, offset: 5024}, + pos: position{line: 190, col: 10, offset: 5221}, expr: &ruleRefExpr{ - pos: position{line: 184, col: 10, offset: 5024}, + pos: position{line: 190, col: 10, offset: 5221}, name: "RawStringChar", }, }, }, }, &seqExpr{ - pos: position{line: 184, col: 27, offset: 5041}, + pos: position{line: 190, col: 27, offset: 5238}, exprs: []interface{}{ &litMatcher{ - pos: position{line: 184, col: 27, offset: 5041}, + pos: position{line: 190, col: 27, offset: 5238}, val: "\"", ignoreCase: false, want: "\"\\\"\"", }, &zeroOrMoreExpr{ - pos: position{line: 184, col: 31, offset: 5045}, + pos: position{line: 190, col: 31, offset: 5242}, expr: &ruleRefExpr{ - pos: position{line: 184, col: 31, offset: 5045}, + pos: position{line: 190, col: 31, offset: 5242}, name: "DoubleStringChar", }, }, @@ -1595,11 +1655,11 @@ var g = &grammar{ }, }, &ruleRefExpr{ - pos: position{line: 184, col: 50, offset: 5064}, + pos: position{line: 190, col: 50, offset: 5261}, name: "EOF", }, &andCodeExpr{ - pos: position{line: 184, col: 54, offset: 5068}, + pos: position{line: 190, col: 54, offset: 5265}, run: (*parser).callonStringLiteral25, }, }, @@ -1609,42 +1669,42 @@ var g = &grammar{ }, { name: "RawStringChar", - pos: position{line: 188, col: 1, offset: 5132}, + pos: position{line: 194, col: 1, offset: 5329}, expr: &seqExpr{ - pos: position{line: 188, col: 18, offset: 5149}, + pos: position{line: 194, col: 18, offset: 5346}, exprs: []interface{}{ ¬Expr{ - pos: position{line: 188, col: 18, offset: 5149}, + pos: position{line: 194, col: 18, offset: 5346}, expr: &litMatcher{ - pos: position{line: 188, col: 19, offset: 5150}, + pos: position{line: 194, col: 19, offset: 5347}, val: "`", ignoreCase: false, want: "\"`\"", }, }, &anyMatcher{ - line: 188, col: 23, offset: 5154, + line: 194, col: 23, offset: 5351, }, }, }, }, { name: "DoubleStringChar", - pos: position{line: 189, col: 1, offset: 5156}, + pos: position{line: 195, col: 1, offset: 5353}, expr: &seqExpr{ - pos: position{line: 189, col: 21, offset: 5176}, + pos: position{line: 195, col: 21, offset: 5373}, exprs: []interface{}{ ¬Expr{ - pos: position{line: 189, col: 21, offset: 5176}, + pos: position{line: 195, col: 21, offset: 5373}, expr: &litMatcher{ - pos: position{line: 189, col: 22, offset: 5177}, + pos: position{line: 195, col: 22, offset: 5374}, val: "\"", ignoreCase: false, want: "\"\\\"\"", }, }, &anyMatcher{ - line: 189, col: 26, offset: 5181, + line: 195, col: 26, offset: 5378, }, }, }, @@ -1652,11 +1712,11 @@ var g = &grammar{ { name: "_", displayName: "\"whitespace\"", - pos: position{line: 191, col: 1, offset: 5184}, + pos: position{line: 197, col: 1, offset: 5381}, expr: &oneOrMoreExpr{ - pos: position{line: 191, col: 19, offset: 5202}, + pos: position{line: 197, col: 19, offset: 5399}, expr: &charClassMatcher{ - pos: position{line: 191, col: 19, offset: 5202}, + pos: position{line: 197, col: 19, offset: 5399}, val: "[ \\t\\r\\n]", chars: []rune{' ', '\t', '\r', '\n'}, ignoreCase: false, @@ -1666,11 +1726,11 @@ var g = &grammar{ }, { name: "EOF", - pos: position{line: 193, col: 1, offset: 5214}, + pos: position{line: 199, col: 1, offset: 5411}, expr: ¬Expr{ - pos: position{line: 193, col: 8, offset: 5221}, + pos: position{line: 199, col: 8, offset: 5418}, expr: &anyMatcher{ - line: 193, col: 9, offset: 5222, + line: 199, col: 9, offset: 5419, }, }, }, @@ -2100,6 +2160,16 @@ func (p *parser) callonValue8() (interface{}, error) { return p.cur.onValue8(stack["s"]) } +func (c *current) onValue11(s interface{}) (interface{}, error) { + return &MatchValue{Raw: s.(string), IsVariable: true}, nil +} + +func (p *parser) callonValue11() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onValue11(stack["s"]) +} + func (c *current) onNumberLiteral2() (interface{}, error) { return string(c.text), nil } @@ -2120,6 +2190,16 @@ func (p *parser) callonNumberLiteral15() (bool, error) { return p.cur.onNumberLiteral15() } +func (c *current) onVariableInterpolation1(id interface{}) (interface{}, error) { + return string(id.(string)), nil +} + +func (p *parser) callonVariableInterpolation1() (interface{}, error) { + stack := p.vstack[len(p.vstack)-1] + _ = stack + return p.cur.onVariableInterpolation1(stack["id"]) +} + func (c *current) onStringLiteral2() (interface{}, error) { return strconv.Unquote(string(c.text)) } diff --git a/grammar/grammar.peg b/grammar/grammar.peg index 728fc7e..d27a250 100644 --- a/grammar/grammar.peg +++ b/grammar/grammar.peg @@ -167,6 +167,8 @@ Value "value" <- selector:Selector { return &MatchValue{Raw: n.(string)}, nil } / s:StringLiteral { return &MatchValue{Raw: s.(string)}, nil +} / s:VariableInterpolation { + return &MatchValue{Raw: s.(string), IsVariable: true}, nil } NumberLiteral "number" <- "-"? IntegerOrFloat &AfterNumbers { @@ -175,6 +177,10 @@ NumberLiteral "number" <- "-"? IntegerOrFloat &AfterNumbers { return false, errors.New("Invalid number literal") } +VariableInterpolation "variable" <- "${" _? id:Identifier _? "}" { + return string(id.(string)), nil +} + AfterNumbers <- &(_ / EOF / ")") IntegerOrFloat <- ("0" / [1-9][0-9]*) ("." [0-9]+)? diff --git a/options.go b/options.go index e1a3f69..04479e8 100644 --- a/options.go +++ b/options.go @@ -20,6 +20,7 @@ type options struct { withTagName string withHookFn ValueTransformationHookFn withUnknown *interface{} + withVariables map[string]string } func WithMaxExpressions(maxExprCnt uint64) Option { @@ -35,6 +36,12 @@ func WithTagName(tagName string) Option { } } +func withVariables(vars map[string]string) Option { + return func(o *options) { + o.withVariables = vars + } +} + // WithHookFn sets a HookFn to be called on the Go data under evaluation // and all subfields, indexes, and values recursively. That makes it // easier for the JSON Pointer to not match exactly the Go value being