Merge branch 'master' into java6

Conflicts:
	project.clj
This commit is contained in:
Gered 2014-03-04 14:33:46 -05:00
commit 5f54d25c41
6 changed files with 505 additions and 275 deletions

View file

@ -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"

View file

@ -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)

View file

@ -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))))

View file

@ -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))}})

View file

@ -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!))))

View file

@ -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}]"))))