diff --git a/docs/Syntax.md b/docs/Syntax.md index c0af26cc..db19ca7c 100644 --- a/docs/Syntax.md +++ b/docs/Syntax.md @@ -25,6 +25,12 @@ After applying Stencil with input data `{"customerName": "John Doe"}` you get th +## Nested field access + +You can reference values from nested fields by writing a dot separator between the field names. For example: `customer.name` can be used to reference the value `"John"` in `{"customer": {"name": "John"}}` + +When the field name contains special characters, it may not be possible to reference it in a syntactically correct way directly. In these cases, the special curly braces operator can be used for property access. Syntax is `field1[field2]`. The expression `field2` will also be evaluated, so you need to quote the expression if it is not a dynamic vale. For example: `customer['name']`. + ## Control structures You can embed control structures in your templates to implement advanced diff --git a/src/stencil/infix.clj b/src/stencil/infix.clj index d228a411..41e6ca45 100644 --- a/src/stencil/infix.clj +++ b/src/stencil/infix.clj @@ -21,6 +21,8 @@ \^ :power \( :open \) :close + \[ :open-bracket + \] :close-bracket \! :not \= :eq \< :lt @@ -51,6 +53,7 @@ {:open -999 ;;;:close -998 :comma -998 + :open-bracket -999 :or -21 :and -20 @@ -88,8 +91,8 @@ (case (first characters) \" (read-until \") ;; programmer quotes \' (read-until \') ;; programmer quotes - \“ (read-until \”) ;; english double quotes - \‘ (read-until \’) ;; english single quotes + \“ (read-until \”) ;; english double quotes + \‘ (read-until \’) ;; english single quotes \’ (read-until \’) ;; hungarian single quotes (felidezojel) \„ (read-until \”) ;; hungarian double quotes (macskakorom) (fail "No string literal" {:c (first characters)})))) @@ -191,6 +194,9 @@ (= :open e0) (recur next-expr (conj opstack :open) result (inc parentheses) (conj functions nil)) + (= :open-bracket e0) + (recur next-expr (conj opstack :open-bracket) result (inc parentheses) functions) + (instance? FnCall e0) (recur next-expr (conj opstack :open) result (inc parentheses) @@ -198,6 +204,15 @@ :args (if (= :close (first next-expr)) 0 1)})) ;; (recur next-expr (conj opstack :fncall) result (conj functions {:fn e0})) + (= :close-bracket e0) + (let [[popped-ops [_ & keep-ops]] + (split-with (partial not= :open-bracket) opstack)] + (recur next-expr + keep-ops + (into result (concat popped-ops [:get])) + (dec parentheses) + functions)) + (= :close e0) (let [[popped-ops [_ & keep-ops]] (split-with (partial not= :open) opstack)] @@ -292,6 +307,7 @@ (def-reduce-step :gt [s0 s1] (> s1 s0)) (def-reduce-step :gte [s0 s1] (>= s1 s0)) (def-reduce-step :power [s0 s1] (Math/pow s1 s0)) +(def-reduce-step :get [a b] (get b (if (vector? b) a (str a)))) (defn eval-rpn ([bindings default-function tokens] diff --git a/test/stencil/infix_test.clj b/test/stencil/infix_test.clj index 805eb917..6ec0d6b6 100644 --- a/test/stencil/infix_test.clj +++ b/test/stencil/infix_test.clj @@ -67,7 +67,7 @@ (into (vals infix/ops)) (into (vals infix/ops2)) (into (keys infix/operation-tokens)) - (disj :open :close :comma)) + (disj :open :close :comma :open-bracket :close-bracket)) known-ops (set (filter keyword? (keys (methods @#'infix/reduce-step))))] (is (every? known-ops ops))))) @@ -113,6 +113,32 @@ (is (= "abcd" (run "'ab' + 'c' + 'd'"))) (is (= "abc123" (run "'abc' + 1 + 23"))))) +(deftest nested-field-access + (is (= 1 (run "a.b" {"a" {"b" 1}}))) + (is (= 2 (run "a['b-c']" {"a" {"b-c" 2}}))) + (is (= 3 (run "a[1]" {"a" {"1" 3}}))) + (is (= 4 (run "a['1']" {"a" {"1" 4}}))) + (is (= 5 (run "a[(1)]" {"a" {"1" 5}}))) + (is (= 6 (run "(a)[1]" {"a" {"1" 6}}))) + + (testing "multiple expressions" + (is (= 7 (run "a[a[1]+4]" {"a" {"1" 2 "6" 7}}))) + (is (= 8 (run "a[1][2]" {"a" {"1" {"2" 8}}})))) + + (testing "syntax error" + (is (thrown? ExceptionInfo (run "[3]" {}))) + (is (thrown? ExceptionInfo (run "a[[3]]" {}))) + (is (thrown? ExceptionInfo (run "a[1,2]" {})))) + + (testing "key is missing from input" + (is (= nil (run "a[1]" {"a" {"2" 2}}))) + (is (= nil (run "a[1]" {})))) + + (testing "array access" + (is (= nil (run "a[1]" {"a" []}))) + (is (= nil (run "a['1']" {"a" ["x" "y" "z"]}))) + (is (= "y" (run "a[1]" {"a" ["x" "y" "z"]}))))) + (deftest logical-operators (testing "Mixed" (is (true? (run "3 = 3 && 4 == 4"))))