Merge branch 'master' into java6
Conflicts: project.clj
This commit is contained in:
commit
5f54d25c41
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
76
src/clj_jtwig/functions.clj
Normal file
76
src/clj_jtwig/functions.clj
Normal 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))))
|
103
src/clj_jtwig/standard_functions.clj
Normal file
103
src/clj_jtwig/standard_functions.clj
Normal 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))}})
|
|
@ -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!))))
|
319
test/clj_jtwig/functions_test.clj
Normal file
319
test/clj_jtwig/functions_test.clj
Normal 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}]"))))
|
Reference in a new issue