Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: array access operator #93

Merged
merged 4 commits into from
Aug 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/Syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ After applying Stencil with input data `{"customerName": "John Doe"}` you get th

<img src="screenshot-substitution-after.png"/>

## 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
Expand Down
20 changes: 18 additions & 2 deletions src/stencil/infix.clj
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
\^ :power
\( :open
\) :close
\[ :open-bracket
\] :close-bracket
\! :not
\= :eq
\< :lt
Expand Down Expand Up @@ -51,6 +53,7 @@
{:open -999
;;;:close -998
:comma -998
:open-bracket -999

:or -21
:and -20
Expand Down Expand Up @@ -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)}))))
Expand Down Expand Up @@ -191,13 +194,25 @@
(= :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)
(conj functions {:fn (:fn-name e0)
: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)]
Expand Down Expand Up @@ -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]
Expand Down
28 changes: 27 additions & 1 deletion test/stencil/infix_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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)))))

Expand Down Expand Up @@ -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"))))
Expand Down