From df26b655500f09c4357169ec7e7b71c8102bb32f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bali=20Mikl=C3=B3s?= Date: Wed, 3 Nov 2021 21:52:01 +0100 Subject: [PATCH] Support custom function registration in Java API (#101) --- docs/Functions.md | 41 +++++++++++++ java-src/io/github/erdos/stencil/API.java | 15 ++++- .../io/github/erdos/stencil/APITest.java | 56 ++++++++++++++++++ test-resources/test-custom-function.docx | Bin 0 -> 4609 bytes test/stencil/api_test.clj | 16 ++++- 5 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 java-test/io/github/erdos/stencil/APITest.java create mode 100644 test-resources/test-custom-function.docx diff --git a/docs/Functions.md b/docs/Functions.md index 957acd15..065088f8 100644 --- a/docs/Functions.md +++ b/docs/Functions.md @@ -216,3 +216,44 @@ Expects two arguments: a value and a list. Checks if list contains the value. Us ### Sum Expects one number argument containing a list with numbers. Sums up the numbers and returns result. Usage: `sum(myList)` + +## Custom functions + +You can register custom implementations of `io.github.erdos.stencil.functions.Function` or the `stencil.functions/call-fn` multimethod. + +Clojure example: + +```clojure +(defmethod call-fn "first" [_ data] + (if (seq? data) (first data) data)) +``` + +Java example: + +```java +public class FirstFuncion implements Function { + + @Override + public String getName() { + return "first"; + } + + @Override + public Object call(final Object... arguments) throws IllegalArgumentException { + if (arguments.length != 1) { + throw new IllegalArgumentException("Unexpected argument count"); + } + final Object arg = arguments[0]; + if (arg instanceof Iterable) { + final Iterator iterator = ((Iterable) arg).iterator(); + return iterator.hasNext() ? iterator.next() : null; + } else { + return null; + } + } +} + +/* .... later you can register it in render stage .... */ + +API.render(preparedTemplate, fragments, data, Arrays.asList(new FirstFunction())); +``` diff --git a/java-src/io/github/erdos/stencil/API.java b/java-src/io/github/erdos/stencil/API.java index c4d28d69..2a4116f9 100644 --- a/java-src/io/github/erdos/stencil/API.java +++ b/java-src/io/github/erdos/stencil/API.java @@ -1,12 +1,15 @@ package io.github.erdos.stencil; +import io.github.erdos.stencil.functions.Function; import io.github.erdos.stencil.impl.NativeEvaluator; import io.github.erdos.stencil.impl.NativeTemplateFactory; import java.io.File; import java.io.IOException; +import java.util.Collection; import java.util.Map; +import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; public final class API { @@ -46,10 +49,18 @@ public static PreparedFragment fragment(File fragmentFile) throws IOException { } public static EvaluatedDocument render(PreparedTemplate template, TemplateData data) { - return render(template, emptyMap(), data); + return render(template, emptyMap(), data, emptyList()); } public static EvaluatedDocument render(PreparedTemplate template, Map fragments, TemplateData data) { - return new NativeEvaluator().render(template, fragments, data); + return render(template, fragments, data, emptyList()); + } + + public static EvaluatedDocument render(PreparedTemplate template, Map fragments, TemplateData data, Collection customFunctions) { + final NativeEvaluator evaluator = new NativeEvaluator(); + if (customFunctions != null) { + evaluator.getFunctionEvaluator().registerFunctions(customFunctions.toArray(new Function[0])); + } + return evaluator.render(template, fragments, data); } } diff --git a/java-test/io/github/erdos/stencil/APITest.java b/java-test/io/github/erdos/stencil/APITest.java new file mode 100644 index 00000000..c01a2820 --- /dev/null +++ b/java-test/io/github/erdos/stencil/APITest.java @@ -0,0 +1,56 @@ +package io.github.erdos.stencil; + +import io.github.erdos.stencil.exceptions.EvalException; +import io.github.erdos.stencil.functions.Function; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class APITest { + + private static class CustomFunction implements Function { + private int callCount = 0; + private Object lastResult = null; + + @Override + public Object call(final Object... arguments) throws IllegalArgumentException { + callCount++; + lastResult = arguments.length > 0 ? arguments[0] : arguments; + return lastResult; + } + + @Override + public String getName() { + return "customFunction"; + } + } + + @Test + public void testCustomFunction() throws IOException { + final CustomFunction fn = new CustomFunction(); + try (final PreparedTemplate prepared = + API.prepare(new File("test-resources/test-custom-function.docx"))) { + final Map data = new HashMap<>(); + data.put("input", "testInput"); + API.render(prepared, Collections.emptyMap(), TemplateData.fromMap(data), Collections.singletonList(fn)); + } + Assert.assertTrue("Custom function should have been called", fn.callCount > 0); + Assert.assertEquals("Custom function returned unexpected value", "testInput", fn.lastResult); + } + + @Test(expected = EvalException.class) + public void testWithoutCustomFunction() throws IOException { + try (final PreparedTemplate prepared = + API.prepare(new File("test-resources/test-custom-function.docx"))) { + final Map data = new HashMap<>(); + data.put("input", "testInput"); + API.render(prepared, Collections.emptyMap(), TemplateData.fromMap(data)); + Assert.fail("Should have thrown exception"); + } + } +} diff --git a/test-resources/test-custom-function.docx b/test-resources/test-custom-function.docx new file mode 100644 index 0000000000000000000000000000000000000000..3fec8c8a1f49c0c0078332d27990a8aee9b5e1d8 GIT binary patch literal 4609 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 Nn2#c+N|irP{4Z-{n&bcg literal 0 HcmV?d00001 diff --git a/test/stencil/api_test.clj b/test/stencil/api_test.clj index daaac118..d9a35826 100644 --- a/test/stencil/api_test.clj +++ b/test/stencil/api_test.clj @@ -1,7 +1,8 @@ (ns stencil.api-test (:import [io.github.erdos.stencil.exceptions EvalException]) (:require [clojure.test :refer [deftest testing is]] - [stencil.api :refer [prepare render! fragment cleanup!]])) + [stencil.api :refer [prepare render! fragment cleanup!]] + [stencil.functions :refer [call-fn]])) (deftest test-prepare+render+cleanup (let [template (prepare "./examples/Purchase Reminder/template.docx") @@ -145,3 +146,16 @@ "footer" footer} :output "/tmp/out-multipart.docx" :overwrite? true))) + +(deftest test-custom-function + (with-open [template (prepare "test-resources/test-custom-function.docx")] + (let [called (atom false) + f (java.io.File/createTempFile "stencil" ".docx")] + (try + (defmethod call-fn "customFunction" + [_ & args] + (reset! called true) + (first args)) + (render! template {:input "data"} :output f :overwrite? true) + (is @called) + (finally (remove-method call-fn "customFunction"))))))