diff --git a/project.clj b/project.clj index 558a2e3..4521782 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject clj-jtwig-java6 "0.1.0-SNAPSHOT" +(defproject clj-jtwig-java6 "0.2" :description "Clojure wrapper for JTwig (Java 6 dependencies)" :url "https://github.com/gered/clj-jtwig/tree/java6" :license {:name "Apache License, Version 2.0" diff --git a/src/clj_jtwig/core.clj b/src/clj_jtwig/core.clj index 1c8893e..fe29a83 100644 --- a/src/clj_jtwig/core.clj +++ b/src/clj_jtwig/core.clj @@ -1,13 +1,10 @@ (ns clj-jtwig.core "wrapper functions for working with JTwig from clojure" - (:require [clojure.walk :refer [stringify-keys]] - [clj-jtwig.convert :refer [java->clojure clojure->java]]) (:import (com.lyncode.jtwig JtwigTemplate JtwigContext JtwigModelMap) - (com.lyncode.jtwig.functions.exceptions FunctionNotFoundException) - (com.lyncode.jtwig.functions.repository DefaultFunctionRepository) - (com.lyncode.jtwig.functions JtwigFunction) (com.lyncode.jtwig.tree.api Content) - (java.io File FileNotFoundException ByteArrayOutputStream))) + (java.io File FileNotFoundException ByteArrayOutputStream)) + (:require [clojure.walk :refer [stringify-keys]]) + (:use [clj-jtwig.functions])) ; global options (defonce options (atom {; true/false to enable/disable compiled template caching when using templates from @@ -131,47 +128,6 @@ [] (reset! compiled-templates {})) -(defn- create-function-repository [] - (new DefaultFunctionRepository (make-array JtwigFunction 0))) - -; we'll be reusing the same function repository object for all contexts created when rendering templates. -; any custom functions added will be added to this instance -(defonce functions (atom (create-function-repository))) - -(defn reset-functions! - "removes any added custom template function handlers" - [] - (reset! functions (create-function-repository))) - -(defn function-exists? [name] - (try - (.retrieve @functions name) - true - (catch FunctionNotFoundException ex - false))) - -(defn add-function! - "adds a new template function using the name specified. templates can call the function by the - name specified and passing in the same number of arguments accepted by f. the return value of - f is returned to the template. - prefer to use the 'deftwigfn' macro when possible." - [name f] - (if (function-exists? name) - (throw (new Exception (str "JTwig template function \"" name "\" already defined."))) - (let [handler (reify JtwigFunction - (execute [_ arguments] - (clojure->java (apply f (map java->clojure arguments)))))] - (.add @functions handler name (make-array String 0)) - (.retrieve @functions name)))) - -(defmacro deftwigfn - "adds a new template function. templates can call it by by the name specified and passing in the - same number of arguments as in args. the return value of the last form in body is returned to the - template." - [fn-name args & body] - `(do - (add-function! ~fn-name (fn ~args ~@body)))) - (defn- get-resource-path [filename] (-> (Thread/currentThread) (.getContextClassLoader) diff --git a/src/clj_jtwig/functions.clj b/src/clj_jtwig/functions.clj new file mode 100644 index 0000000..56562eb --- /dev/null +++ b/src/clj_jtwig/functions.clj @@ -0,0 +1,76 @@ +(ns clj-jtwig.functions + "standard functions added to jtwig contexts by default. these are in addition to the + functions added by default in all jtwig function repository objects" + (:import (com.lyncode.jtwig.functions JtwigFunction) + (com.lyncode.jtwig.functions.repository DefaultFunctionRepository) + (com.lyncode.jtwig.functions.exceptions FunctionNotFoundException FunctionException)) + (:require [clj-jtwig.convert :refer [java->clojure clojure->java]]) + (:use [clj-jtwig.standard-functions])) + +(defn- make-function-handler [f] + (reify JtwigFunction + (execute [_ arguments] + (try + (clojure->java (apply f (map java->clojure arguments))) + (catch Exception ex + (throw (new FunctionException ex))))))) + +(defn- make-aliased-array [aliases] + (let [n (count aliases) + array (make-array String n)] + (doseq [index (range n)] + (aset array index (nth aliases index))) + array)) + +(defn- create-function-repository [] + (let [repository (new DefaultFunctionRepository (make-array JtwigFunction 0))] + ; always add our standard functions to new repository objects + (doseq [[name {:keys [aliases fn]}] standard-functions] + (.add repository + (make-function-handler fn) + name + (make-aliased-array aliases))) + repository)) + +; we'll be reusing the same function repository object for all contexts created when rendering templates. +; any custom functions added will be added to this instance +(defonce functions (atom (create-function-repository))) + +(defn reset-functions! + "removes any added custom template function handlers. use this with care!" + [] + (reset! functions (create-function-repository))) + +(defn function-exists? [name] + (try + (.retrieve @functions name) + true + (catch FunctionNotFoundException ex + false))) + +(defn add-function! + "adds a new template function under the name specified. templates can call the function by the + name specified (or one of the aliases specified) and passing in the same number of arguments + accepted by f. the return value of f is returned to the template." + ([name f] + (add-function! name nil f)) + ([name aliases f] + (let [handler (make-function-handler f)] + (.add @functions handler name (make-aliased-array aliases)) + (.retrieve @functions name)))) + +(defmacro deftwigfn + "defines a new template function. templates can call it by by the name specified and passing in the + same number of arguments as in args. the return value of the last form in body is returned to the + template. functions defined this way have no aliases and can only be called by the name given." + [fn-name args & body] + `(do + (add-function! ~fn-name (fn ~args ~@body)))) + +(defmacro defaliasedtwigfn + "defines a new template function. templates can call it by by the name specified (or one of the + aliases specified) and passing in the same number of arguments as in args. the return value of + the last form in body is returned to the template." + [fn-name args aliases & body] + `(do + (add-function! ~fn-name ~aliases (fn ~args ~@body)))) diff --git a/src/clj_jtwig/standard_functions.clj b/src/clj_jtwig/standard_functions.clj new file mode 100644 index 0000000..1d591d9 --- /dev/null +++ b/src/clj_jtwig/standard_functions.clj @@ -0,0 +1,103 @@ +(ns clj-jtwig.standard-functions + "standard function definitions. these are functions that are not yet included in JTwig's standard function + library and are just here to fill in the gaps for now." + (:use [clojure.pprint])) + +; we are using a separate map to hold the standard functions instead of using deftwigfn, etc. because doing it this +; way makes it easy to re-add all these functions when/if the JTwig function repository object needs to be +; recreated (e.g. during unit tests). + +; the keys are function names. each value is a map containing :fn which is the actual function, and an optional +; :aliases, which should be a vector of strings containing one or more possible aliases for this function. + +(defonce standard-functions + {"blankIfNull" + {:fn (fn [x] + (if (nil? x) "" x))} + + "butlast" + {:fn (fn [sequence] + ; matching behaviour of jtwig's first/last implementation + (if (map? sequence) + (-> sequence vals butlast) + (butlast sequence)))} + + "dump" + {:fn (fn [x] + (with-out-str + (clojure.pprint/pprint x)))} + + "nth" + {:fn (fn [sequence index & optional-not-found] + (let [values (if (map? sequence) ; map instance check to match behaviour of jtwig's first/last implementation + (-> sequence vals) + sequence)] + (if optional-not-found + (nth values index (first optional-not-found)) + (nth values index))))} + + "max" + {:fn (fn [& numbers] + (if (coll? (first numbers)) + (apply max (first numbers)) + (apply max numbers)))} + + "min" + {:fn (fn [& numbers] + (if (coll? (first numbers)) + (apply min (first numbers)) + (apply min numbers)))} + + "random" + {:fn (fn [& values] + (let [first-value (first values)] + (cond + (and (= (count values) 1) + (coll? first-value)) + (rand-nth first-value) + + (> (count values) 1) + (rand-nth values) + + (string? first-value) + (rand-nth (seq first-value)) + + (number? first-value) + (rand-int first-value) + + :else + (rand))))} + + "range" + {:fn (fn [low high & [step]] + (range low high (or step 1)))} + + "rest" + {:fn (fn [sequence] + ; matching behaviour of jtwig's first/last implementation + (if (map? sequence) + (-> sequence vals rest) + (rest sequence)))} + + "second" + {:fn (fn [sequence] + ; matching behaviour of jtwig's first/last implementation + (if (map? sequence) + (-> sequence vals second) + (second sequence)))} + + "sort" + {:fn (fn [sequence] + (sort < sequence))} + + "sortDescending" + {:fn (fn [sequence] + (sort > sequence))} + + "sortBy" + {:fn (fn [coll k] + (sort-by #(get % k) coll))} + + "sortDescendingBy" + {:fn (fn [coll k] + (sort-by #(get % k) #(compare %2 %1) coll))}}) diff --git a/test/clj_jtwig/core_test.clj b/test/clj_jtwig/core_test.clj index 92c45c2..3fe7afb 100644 --- a/test/clj_jtwig/core_test.clj +++ b/test/clj_jtwig/core_test.clj @@ -1,8 +1,8 @@ (ns clj-jtwig.core-test - (:import (java.io FileNotFoundException) - (clojure.lang ArityException)) + (:import (java.io FileNotFoundException)) (:require [clojure.test :refer :all] - [clj-jtwig.core :refer :all])) + [clj-jtwig.core :refer :all] + [clj-jtwig.functions :refer :all])) ; The purpose of these tests is to establish that our wrapper around JTwig works. That is, ; we will be focusing on stuff like making sure that passing Clojure data structures @@ -14,14 +14,6 @@ ; Some of the variable passing and return / iteration verification tests might be a bit ; overkill, but better safe than sorry. :) -; TODO: is there a better way to test that something is an instance of some object generated by reify? -(defn valid-function-handler? [x] - (and (not (nil? x)) - (-> x - (class) - (.getName) - (.startsWith "clj_jtwig.core$add_function")))) - (deftest string-template (testing "Evaluating templates in string vars" (is (= (render "Hello {{ name }}!" @@ -125,219 +117,3 @@ {:name "Bob"} {:skip-model-map-stringify? true})) "passing a model-map where the keys are keywords and try skipping auto stringifying the keys")))) - -(deftest template-functions - (testing "Adding custom template functions" - (do - (reset-functions!) - - (is (valid-function-handler? - (deftwigfn "add" [a b] - (+ a b)))) - - (is (true? (function-exists? "add"))) - (is (false? (function-exists? "foobar"))) - - (is (thrown? - Exception - (deftwigfn "add" [a b] - (+ a b)))) - - (is (= (render "{{add(1, 2)}}" nil) - "3") - "calling a custom function") - (is (= (render "{{add(a, b)}}" {:a 1 :b 2}) - "3") - "calling a custom function, passing in variables from the model-map as arguments") - (is (= (render "{{x|add(1)}}" {:x 1}) - "2") - "calling a custom function using the 'filter' syntax") - - (reset-functions!))) - - (testing "Fixed and variable number of template function arguments" - (do - (reset-functions!) - - (is (valid-function-handler? - (deftwigfn "add2" [a b] - (+ a b)))) - (is (true? (function-exists? "add2"))) - (is (valid-function-handler? - (deftwigfn "addAll" [& numbers] - (apply + numbers)))) - (is (true? (function-exists? "addAll"))) - - (is (= (render "{{add2(1, 2)}}" nil) - "3") - "fixed number of arguments (correct amount)") - (is (thrown? - ArityException - (render "{{add2(1)}}" nil))) - (is (= (render "{{addAll(1, 2, 3, 4, 5)}}" nil) - "15") - "variable number of arguments (non-zero)") - (is (= (render "{{addAll}}" nil) - "null") - "variable number of arguments (zero)") - - (reset-functions!))) - - (testing "Passing different data structures to template functions" - (do - (reset-functions!) - - (is (valid-function-handler? - (deftwigfn "identity" [x] - x))) - (is (true? (function-exists? "identity"))) - (is (valid-function-handler? - (deftwigfn "typename" [x] - (.getName (type x))))) - (is (true? (function-exists? "typename"))) - - ; verify that the clojure function recognizes the correct types when the variable is passed via the model-map - (is (= (render "{{typename(x)}}" {:x 42}) - "java.lang.Long") - "integer typename via model-map") - (is (= (render "{{typename(x)}}" {:x 3.14}) - "java.lang.Double") - "float typename via model-map") - (is (= (render "{{typename(x)}}" {:x "foobar"}) - "java.lang.String") - "string typename via model-map") - (is (= (render "{{typename(x)}}" {:x \a}) - "java.lang.Character") - "char typename via model-map") - (is (= (render "{{typename(x)}}" {:x true}) - "java.lang.Boolean") - "boolean typename via model-map") - (is (= (render "{{typename(x)}}" {:x '(1 2 3 4 5)}) - "clojure.lang.LazySeq") - "list typename via model-map") - (is (= (render "{{typename(x)}}" {:x [1 2 3 4 5]}) - "clojure.lang.LazySeq") - "vector typename via model-map") - (is (= (render "{{typename(x)}}" {:x {:a 1 :b "foo" :c nil}}) - "clojure.lang.PersistentArrayMap") - "map typename via model-map") - (is (= (render "{{typename(x)}}" {:x #{1 2 3 4 5}}) - "clojure.lang.LazySeq") - "set typename via model-map") - - ; verify that the clojure function recognizes the correct types when the variable is passed via a constant - ; value embedded in the template - (is (= (render "{{typename(42)}}" nil) - "java.lang.Integer") - "integer typename via constant value embedded in the template") - (is (= (render "{{typename(3.14)}}" nil) - "java.lang.Double") - "float typename via constant value embedded in the template") - (is (= (render "{{typename('foobar')}}" nil) - "java.lang.String") - "string typename via constant value embedded in the template") - (is (= (render "{{typename('a')}}" nil) - "java.lang.Character") - "char typename via constant value embedded in the template") - (is (= (render "{{typename(true)}}" nil) - "java.lang.Boolean") - "boolean typename via constant value embedded in the template") - (is (= (render "{{typename([1, 2, 3, 4, 5])}}" nil) - "clojure.lang.LazySeq") - "list typename via constant value embedded in the template") - (is (= (render "{{typename(1..5)}}" nil) - "clojure.lang.LazySeq") - "vector typename via constant value embedded in the template") - (is (= (render "{{typename({a: 1, b: 'foo', c: null})}}" nil) - "clojure.lang.PersistentArrayMap") - "map typename via constant value embedded in the template") - - ; simple passing / returning... not doing anything exciting with the arguments - ; using a constant value embedded inside the template - (is (= (render "{{identity(x)}}" {:x 42}) - "42") - "integer via model-map") - (is (= (render "{{identity(x)}}" {:x 3.14}) - "3.14") - "float via model-map") - (is (= (render "{{identity(x)}}" {:x "foobar"}) - "foobar") - "string via model-map") - (is (= (render "{{identity(x)}}" {:x \a}) - "a") - "char via model-map") - (is (= (render "{{identity(x)}}" {:x true}) - "true") - "boolean via model-map") - (is (= (render "{{identity(x)}}" {:x '(1 2 3 4 5)}) - "[1, 2, 3, 4, 5]") - "list via model-map") - (is (= (render "{{identity(x)}}" {:x [1 2 3 4 5]}) - "[1, 2, 3, 4, 5]") - "vector via model-map") - ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) - (is (= (render "{{identity(x)}}" {:x {:a 1 :b "foo" :c nil}}) - "{b=foo, c=null, a=1}") - "map via model-map") - (is (= (render "{{identity(x)}}" {:x #{1 2 3 4 5}}) - "[1, 2, 3, 4, 5]") - "set via model-map") - - ; simple passing / returning... not doing anything exciting with the arguments - ; using a constant value embedded inside the template - (is (= (render "{{identity(42)}}" nil) - "42") - "integer via constant value embedded in the template") - (is (= (render "{{identity(3.14)}}" nil) - "3.14") - "float via constant value embedded in the template") - (is (= (render "{{identity('foobar')}}" nil) - "foobar") - "string via constant value embedded in the template") - (is (= (render "{{identity('a')}}" nil) - "a") - "char via constant value embedded in the template") - (is (= (render "{{identity(true)}}" nil) - "true") - "boolean via constant value embedded in the template") - (is (= (render "{{identity([1, 2, 3, 4, 5])}}" nil) - "[1, 2, 3, 4, 5]") - "enumerated list via constant value embedded in the template") - (is (= (render "{{identity(1..5)}}" nil) - "[1, 2, 3, 4, 5]") - "list by comprehension via constant value embedded in the template") - ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) - (is (= (render "{{identity({a: 1, b: 'foo', c: null})}}" nil) - "{b=foo, c=null, a=1}") - "map via constant value embedded in the template") - - ; iterating over passed sequence/collection type arguments passed to a custom function from a variable - ; inside the model-map and being returned - (is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x '(1 2 3 4 5)}) - "1 2 3 4 5 ") - "list (iterating over a model-map var passed to a function and returned from it)") - (is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x [1 2 3 4 5]}) - "1 2 3 4 5 ") - "vector (iterating over a model-map var passed to a function and returned from it)") - ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) - (is (= (render "{% for k, v in identity(x) %}{{k}}: {{v}} {% endfor %}" {:x {:a 1 :b "foo" :c nil}}) - "b: foo c: null a: 1 ") - "map (iterating over a model-map var passed to a function and returned from it)") - (is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x #{1 2 3 4 5}}) - "1 2 3 4 5 ") - "set (iterating over a model-map var passed to a function and returned from it)") - - ; iterating over passed sequence/collection type arguments passed to a custom function from a constant - ; value embedded in the template and being returned - (is (= (render "{% for i in identity([1, 2, 3, 4, 5]) %}{{i}} {% endfor %}" nil) - "1 2 3 4 5 ") - "enumerated list (iterating over a model-map var passed to a function and returned from it)") - (is (= (render "{% for i in identity(1..5) %}{{i}} {% endfor %}" nil) - "1 2 3 4 5 ") - "list by comprehension (iterating over a model-map var passed to a function and returned from it)") - ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) - (is (= (render "{% for k, v in identity({a: 1, b: 'foo', c: null}) %}{{k}}: {{v}} {% endfor %}" nil) - "b: foo c: null a: 1 ") - "map (iterating over a model-map var passed to a function and returned from it)") - - (reset-functions!)))) \ No newline at end of file diff --git a/test/clj_jtwig/functions_test.clj b/test/clj_jtwig/functions_test.clj new file mode 100644 index 0000000..1aa1db8 --- /dev/null +++ b/test/clj_jtwig/functions_test.clj @@ -0,0 +1,319 @@ +(ns clj-jtwig.functions-test + (:require [clojure.test :refer :all] + [clj-jtwig.core :refer :all] + [clj-jtwig.functions :refer :all])) + +; TODO: is there a better way to test that something is an instance of some object generated by reify? +(defn valid-function-handler? [x] + (and (not (nil? x)) + (-> x + (class) + (.getName) + (.startsWith "clj_jtwig.functions$make_function_handler")))) + +(deftest template-functions + (testing "Adding custom template functions" + (do + (reset-functions!) + + (is (valid-function-handler? + (deftwigfn "add" [a b] + (+ a b)))) + + (is (true? (function-exists? "add"))) + (is (false? (function-exists? "foobar"))) + + (is (valid-function-handler? + (deftwigfn "add" [a b] + (+ a b)))) + + (is (= (render "{{add(1, 2)}}" nil) + "3") + "calling a custom function") + (is (= (render "{{add(a, b)}}" {:a 1 :b 2}) + "3") + "calling a custom function, passing in variables from the model-map as arguments") + (is (= (render "{{x|add(1)}}" {:x 1}) + "2") + "calling a custom function using the 'filter' syntax") + + (reset-functions!))) + + (testing "Fixed and variable number of template function arguments" + (do + (reset-functions!) + + (is (valid-function-handler? + (deftwigfn "add2" [a b] + (+ a b)))) + (is (true? (function-exists? "add2"))) + (is (valid-function-handler? + (deftwigfn "addAll" [& numbers] + (apply + numbers)))) + (is (true? (function-exists? "addAll"))) + + (is (= (render "{{add2(1, 2)}}" nil) + "3") + "fixed number of arguments (correct amount)") + (is (thrown-with-msg? + Exception + #"clojure\.lang\.ArityException: Wrong number of args" + (render "{{add2(1)}}" nil))) + (is (= (render "{{addAll(1, 2, 3, 4, 5)}}" nil) + "15") + "variable number of arguments (non-zero)") + (is (= (render "{{addAll}}" nil) + "null") + "variable number of arguments (zero)") + + (reset-functions!))) + + (testing "Passing different data structures to template functions" + (do + (reset-functions!) + + (is (valid-function-handler? + (deftwigfn "identity" [x] + x))) + (is (true? (function-exists? "identity"))) + (is (valid-function-handler? + (deftwigfn "typename" [x] + (.getName (type x))))) + (is (true? (function-exists? "typename"))) + + ; verify that the clojure function recognizes the correct types when the variable is passed via the model-map + (is (= (render "{{typename(x)}}" {:x 42}) + "java.lang.Long") + "integer typename via model-map") + (is (= (render "{{typename(x)}}" {:x 3.14}) + "java.lang.Double") + "float typename via model-map") + (is (= (render "{{typename(x)}}" {:x "foobar"}) + "java.lang.String") + "string typename via model-map") + (is (= (render "{{typename(x)}}" {:x \a}) + "java.lang.Character") + "char typename via model-map") + (is (= (render "{{typename(x)}}" {:x true}) + "java.lang.Boolean") + "boolean typename via model-map") + (is (= (render "{{typename(x)}}" {:x '(1 2 3 4 5)}) + "clojure.lang.LazySeq") + "list typename via model-map") + (is (= (render "{{typename(x)}}" {:x [1 2 3 4 5]}) + "clojure.lang.LazySeq") + "vector typename via model-map") + (is (= (render "{{typename(x)}}" {:x {:a 1 :b "foo" :c nil}}) + "clojure.lang.PersistentArrayMap") + "map typename via model-map") + (is (= (render "{{typename(x)}}" {:x #{1 2 3 4 5}}) + "clojure.lang.LazySeq") + "set typename via model-map") + + ; verify that the clojure function recognizes the correct types when the variable is passed via a constant + ; value embedded in the template + (is (= (render "{{typename(42)}}" nil) + "java.lang.Integer") + "integer typename via constant value embedded in the template") + (is (= (render "{{typename(3.14)}}" nil) + "java.lang.Double") + "float typename via constant value embedded in the template") + (is (= (render "{{typename('foobar')}}" nil) + "java.lang.String") + "string typename via constant value embedded in the template") + (is (= (render "{{typename('a')}}" nil) + "java.lang.Character") + "char typename via constant value embedded in the template") + (is (= (render "{{typename(true)}}" nil) + "java.lang.Boolean") + "boolean typename via constant value embedded in the template") + (is (= (render "{{typename([1, 2, 3, 4, 5])}}" nil) + "clojure.lang.LazySeq") + "list typename via constant value embedded in the template") + (is (= (render "{{typename(1..5)}}" nil) + "clojure.lang.LazySeq") + "vector typename via constant value embedded in the template") + (is (= (render "{{typename({a: 1, b: 'foo', c: null})}}" nil) + "clojure.lang.PersistentArrayMap") + "map typename via constant value embedded in the template") + + ; simple passing / returning... not doing anything exciting with the arguments + ; using a constant value embedded inside the template + (is (= (render "{{identity(x)}}" {:x 42}) + "42") + "integer via model-map") + (is (= (render "{{identity(x)}}" {:x 3.14}) + "3.14") + "float via model-map") + (is (= (render "{{identity(x)}}" {:x "foobar"}) + "foobar") + "string via model-map") + (is (= (render "{{identity(x)}}" {:x \a}) + "a") + "char via model-map") + (is (= (render "{{identity(x)}}" {:x true}) + "true") + "boolean via model-map") + (is (= (render "{{identity(x)}}" {:x '(1 2 3 4 5)}) + "[1, 2, 3, 4, 5]") + "list via model-map") + (is (= (render "{{identity(x)}}" {:x [1 2 3 4 5]}) + "[1, 2, 3, 4, 5]") + "vector via model-map") + ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) + (is (= (render "{{identity(x)}}" {:x {:a 1 :b "foo" :c nil}}) + "{b=foo, c=null, a=1}") + "map via model-map") + (is (= (render "{{identity(x)}}" {:x #{1 2 3 4 5}}) + "[1, 2, 3, 4, 5]") + "set via model-map") + + ; simple passing / returning... not doing anything exciting with the arguments + ; using a constant value embedded inside the template + (is (= (render "{{identity(42)}}" nil) + "42") + "integer via constant value embedded in the template") + (is (= (render "{{identity(3.14)}}" nil) + "3.14") + "float via constant value embedded in the template") + (is (= (render "{{identity('foobar')}}" nil) + "foobar") + "string via constant value embedded in the template") + (is (= (render "{{identity('a')}}" nil) + "a") + "char via constant value embedded in the template") + (is (= (render "{{identity(true)}}" nil) + "true") + "boolean via constant value embedded in the template") + (is (= (render "{{identity([1, 2, 3, 4, 5])}}" nil) + "[1, 2, 3, 4, 5]") + "enumerated list via constant value embedded in the template") + (is (= (render "{{identity(1..5)}}" nil) + "[1, 2, 3, 4, 5]") + "list by comprehension via constant value embedded in the template") + ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) + (is (= (render "{{identity({a: 1, b: 'foo', c: null})}}" nil) + "{b=foo, c=null, a=1}") + "map via constant value embedded in the template") + + ; iterating over passed sequence/collection type arguments passed to a custom function from a variable + ; inside the model-map and being returned + (is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x '(1 2 3 4 5)}) + "1 2 3 4 5 ") + "list (iterating over a model-map var passed to a function and returned from it)") + (is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x [1 2 3 4 5]}) + "1 2 3 4 5 ") + "vector (iterating over a model-map var passed to a function and returned from it)") + ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) + (is (= (render "{% for k, v in identity(x) %}{{k}}: {{v}} {% endfor %}" {:x {:a 1 :b "foo" :c nil}}) + "b: foo c: null a: 1 ") + "map (iterating over a model-map var passed to a function and returned from it)") + (is (= (render "{% for i in identity(x) %}{{i}} {% endfor %}" {:x #{1 2 3 4 5}}) + "1 2 3 4 5 ") + "set (iterating over a model-map var passed to a function and returned from it)") + + ; iterating over passed sequence/collection type arguments passed to a custom function from a constant + ; value embedded in the template and being returned + (is (= (render "{% for i in identity([1, 2, 3, 4, 5]) %}{{i}} {% endfor %}" nil) + "1 2 3 4 5 ") + "enumerated list (iterating over a model-map var passed to a function and returned from it)") + (is (= (render "{% for i in identity(1..5) %}{{i}} {% endfor %}" nil) + "1 2 3 4 5 ") + "list by comprehension (iterating over a model-map var passed to a function and returned from it)") + ; TODO: order of iteration through a map is undefined, the string being tested may not always be the same (wrt. order) + (is (= (render "{% for k, v in identity({a: 1, b: 'foo', c: null}) %}{{k}}: {{v}} {% endfor %}" nil) + "b: foo c: null a: 1 ") + "map (iterating over a model-map var passed to a function and returned from it)") + + (reset-functions!)))) + +(deftest standard-functions + (testing "Standard functions were added properly" + (is (true? (function-exists? "blankIfNull"))) + (is (true? (function-exists? "butlast"))) + (is (true? (function-exists? "dump"))) + (is (true? (function-exists? "nth"))) + (is (true? (function-exists? "max"))) + (is (true? (function-exists? "min"))) + (is (true? (function-exists? "random"))) + (is (true? (function-exists? "range"))) + (is (true? (function-exists? "rest"))) + (is (true? (function-exists? "second"))) + (is (true? (function-exists? "sort"))) + (is (true? (function-exists? "sortDescending"))) + (is (true? (function-exists? "sortBy"))) + (is (true? (function-exists? "sortDescendingBy")))) + + (testing "blankIfNull" + (is (= (render "{{ a|blankIfNull }}" nil) + "")) + (is (= (render "{{ a|blankIfNull }}" {:a nil}) + "")) + (is (= (render "{{ a|blankIfNull }}" {:a "foo"}) + "foo"))) + + (testing "butlast" + (is (= (render "{{ [1, 2, 3, 4, 5]|butlast }}" nil) + "[1, 2, 3, 4]"))) + + (testing "dump" + (is (= (render "{{ a|dump }}" {:a [{:foo "bar"} [1, 2, 3] "hello"]}) + "({\"foo\" \"bar\"} (1 2 3) \"hello\")\n"))) + + (testing "nth" + (is (= (render "{{ [1, 2, 3, 4, 5]|nth(2) }}" nil) + "3")) + (is (thrown-with-msg? + Exception + #"java.lang.IndexOutOfBoundsException" + (render "{{ [1, 2, 3, 4, 5]|nth(6) }}" nil))) + (is (= (render "{{ [1, 2, 3, 4, 5]|nth(6, \"not found\") }}" nil) + "not found"))) + + (testing "max" + (is (= (render "{{ [2, 1, 5, 3, 4]|max }}" nil) + "5")) + (is (= (render "{{ max(2, 1, 5, 3, 4) }}" nil) + "5"))) + + (testing "min" + (is (= (render "{{ [2, 1, 5, 3, 4]|min }}" nil) + "1")) + (is (= (render "{{ min(2, 1, 5, 3, 4) }}" nil) + "1"))) + + (testing "random" + (is (some #{(render "{{ ['apple', 'orange', 'citrus']|random }}" nil)} + ["apple" "orange" "citrus"])) + (is (some #{(render "{{ \"ABC\"|random }}" nil)} + ["A" "B" "C"]))) + + (testing "range" + (is (= (render "{{ range(1, 5) }}" nil) + "[1, 2, 3, 4]")) + (is (= (render "{{ range(1, 5, 2) }}" nil) + "[1, 3]"))) + + (testing "rest" + (is (= (render "{{ [1, 2, 3, 4, 5]|rest }}" nil) + "[2, 3, 4, 5]"))) + + (testing "second" + (is (= (render "{{ [1, 2, 3, 4, 5]|second }}" nil) + "2"))) + + (testing "sort" + (is (= (render "{{ [2, 1, 5, 3, 4]|sort }}" nil) + "[1, 2, 3, 4, 5]"))) + + (testing "sortDescending" + (is (= (render "{{ [2, 1, 5, 3, 4]|sortDescending }}" nil) + "[5, 4, 3, 2, 1]"))) + + (testing "sortBy" + (is (= (render "{{ [{a: 2}, {a: 1}, {a: 5}, {a: 3}, {a: 4}]|sortBy(\"a\") }}" nil) + "[{a=1}, {a=2}, {a=3}, {a=4}, {a=5}]"))) + + (testing "sortDescendingBy" + (is (= (render "{{ [{a: 2}, {a: 1}, {a: 5}, {a: 3}, {a: 4}]|sortDescendingBy(\"a\") }}" nil) + "[{a=5}, {a=4}, {a=3}, {a=2}, {a=1}]"))))