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