Skip to content

Commit

Permalink
Function provider SPI
Browse files Browse the repository at this point in the history
The new `FunctionProvider` Service Provider Interface simplifies the extension of the function set provided by Stencil.

Implementing and registering these new implementations in `META-INF/services/io.github.erdos.stencil.functions.FunctionProvider` enable other libraries to register their functions.
  • Loading branch information
mbali committed Oct 18, 2022
1 parent 82c07c8 commit e05ce09
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 14 deletions.
9 changes: 8 additions & 1 deletion docs/Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ Expects one number argument containing a list with numbers. Sums up the numbers

## Custom functions

You can register custom implementations of `io.github.erdos.stencil.functions.Function` or the `stencil.functions/call-fn` multimethod.
You can register custom implementations of `io.github.erdos.stencil.functions.Function` or the `stencil.functions/call-fn` multimethod.
If you implement the `call-fn` multimethod, the namespace containing these implementations should be loaded before rendering a document.
(Keep in mind, that `call-fn` implementations always have priority over `io.github.erdos.stencil.functions.Function` implementations)

Clojure example:

Expand Down Expand Up @@ -271,3 +273,8 @@ public class FirstFuncion implements Function {

API.render(preparedTemplate, fragments, data, Arrays.asList(new FirstFunction()));
```

### Automatic registration of custom functions

Stencil uses the JVM's [ServiceLoader](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) facility to load function provider implementations. If you want to register your custom functions automatically, implement the `io.github.erdos.stencil.functions.FunctionProvider` interface, and add these implementations to your extension library's `META-INF/services/io.github.erdos.stencil.functions.FunctionProvider` file.
The `call-fn` implementations which are defined in your namespaces can also be loaded using this facility, if you override `io.github.erdos.stencil.functions.ClojureCallFnProvider` abstract class, and register it as a `FunctionProvider`. See `io.github.erdos.stencil.functions.DefaultCallFnProvider` as an example.
12 changes: 12 additions & 0 deletions java-src/io/github/erdos/stencil/functions/BasicFunctions.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.github.erdos.stencil.functions;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
* Common general purpose functions.
Expand Down Expand Up @@ -72,4 +74,14 @@ public Object call(Object... arguments) {
public String getName() {
return name().toLowerCase();
}

public static class Provider implements FunctionProvider {

private static final List<Function> FUNCTIONS = Arrays.asList(values());

@Override
public Collection<Function> functions() {
return FUNCTIONS;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.github.erdos.stencil.functions;

import io.github.erdos.stencil.impl.ClojureHelper;

import java.util.Collection;
import java.util.Collections;

/**
* This provider requires a Clojure NS which contains implementations of the call-fn multimethod.
* The constructor defines the
*/
public abstract class ClojureCallFnProvider implements FunctionProvider {

/**
* The constructor loads the Clojure namespace which contains the call-fn multimethod implementations.
* This should be called from the no-args constructor of the concrete class.
* @param ns the clojuse namespace which contains the function implementations.
*/
protected ClojureCallFnProvider(final String ns) {
ClojureHelper.requireNs(ns);
}

@Override
/**
* This provider does not provide any functions directly.
*
* @return an empty collection
*/
public Collection<Function> functions() {
return Collections.emptyList();
}

@Override
/*
* call-fn implementations have priority over Java Function implementations during evaluation.
*/
public int priority() {
return Integer.MIN_VALUE;
}
}
15 changes: 11 additions & 4 deletions java-src/io/github/erdos/stencil/functions/DateFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.*;

import static java.util.Arrays.asList;
import static java.util.Locale.forLanguageTag;
Expand Down Expand Up @@ -136,4 +133,14 @@ private static Optional<LocalDateTime> maybeLocalDateTime(Object obj) {
public String getName() {
return name().toLowerCase();
}

public static class Provider implements FunctionProvider {

private static final List<Function> FUNCTIONS = Arrays.asList(values());

@Override
public Collection<Function> functions() {
return FUNCTIONS;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.github.erdos.stencil.functions;

public class DefaultCallFnProvider extends ClojureCallFnProvider {
/**
* The built-in call-fn functions are defined in the stencil.functions namespace.
* This constructor loads it.
*/
public DefaultCallFnProvider() {
super("stencil.functions");
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package io.github.erdos.stencil.functions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public final class FunctionEvaluator {

private final Map<String, Function> functions = new HashMap<>();

{
registerFunctions(BasicFunctions.values());
registerFunctions(StringFunctions.values());
registerFunctions(NumberFunctions.values());
registerFunctions(DateFunctions.values());
registerFunctions(LocaleFunctions.values());
public FunctionEvaluator() {
this(FunctionLoader.getFunctions());
}

public FunctionEvaluator(Collection<? extends Function> functions) {
for (Function f : functions) {
registerFunction(f);
}
}

private void registerFunction(Function function) {
Expand Down
20 changes: 20 additions & 0 deletions java-src/io/github/erdos/stencil/functions/FunctionLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.github.erdos.stencil.functions;

import java.util.Comparator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

final class FunctionLoader {
private FunctionLoader() {}

private static final ServiceLoader<FunctionProvider> REGISTRARS = ServiceLoader.load(FunctionProvider.class);

static List<Function> getFunctions() {
return StreamSupport.stream(REGISTRARS.spliterator(), false)
.sorted(Comparator.comparingInt(FunctionProvider::priority))
.flatMap(p -> p.functions().stream())
.collect(Collectors.toList());
}
}
32 changes: 32 additions & 0 deletions java-src/io/github/erdos/stencil/functions/FunctionProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.github.erdos.stencil.functions;

import java.util.Collection;

public interface FunctionProvider {


int DEFAULT_PRIORITY = 10;

/**
* Return the functions instances for the current render.
* The provider may choose to return new instances of a function for each call, if the function is not pure:
* e.g.: a counter, which returns an increasing sequence of numbers for each call.
*
* @return the functions provided by this provider
*/
Collection<Function> functions();

/**
* Priority of the provider.
* <p>
* Providers are called in ascending order of priority.
* <p>
* Default priority is 10.
* NB: Multimethod functions defined in Clojure namespaces have priority over Java defined functions.
*
* @return priority of the provider
*/
default int priority() {
return DEFAULT_PRIORITY;
}
}
13 changes: 13 additions & 0 deletions java-src/io/github/erdos/stencil/functions/LocaleFunctions.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.github.erdos.stencil.functions;

import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;

import static java.util.Locale.forLanguageTag;
Expand Down Expand Up @@ -53,4 +56,14 @@ private static String formatting(Function function, java.util.function.Function<
public String getName() {
return name().toLowerCase();
}

public static class Provider implements FunctionProvider {

private static final List<Function> FUNCTIONS = Arrays.asList(values());

@Override
public Collection<Function> functions() {
return FUNCTIONS;
}
}
}
14 changes: 14 additions & 0 deletions java-src/io/github/erdos/stencil/functions/NumberFunctions.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package io.github.erdos.stencil.functions;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

/**
* Common numeric functions.
*/
Expand Down Expand Up @@ -73,4 +77,14 @@ private static Number maybeNumber(Object... arguments) {
return (Number) arguments[0];
}
}

public static class Provider implements FunctionProvider {

private static final List<Function> FUNCTIONS = Arrays.asList(values());

@Override
public Collection<Function> functions() {
return FUNCTIONS;
}
}
}
12 changes: 11 additions & 1 deletion java-src/io/github/erdos/stencil/functions/StringFunctions.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.util.Arrays;
import java.util.Collection;
import java.util.IllegalFormatException;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -113,4 +113,14 @@ public Object call(Object... arguments) throws IllegalArgumentException {
public String getName() {
return name().toLowerCase();
}

public static class Provider implements FunctionProvider {

private static final List<Function> FUNCTIONS = Arrays.asList(values());

@Override
public Collection<Function> functions() {
return FUNCTIONS;
}
}
}
21 changes: 19 additions & 2 deletions java-src/io/github/erdos/stencil/impl/ClojureHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import clojure.lang.RT;
import clojure.lang.Symbol;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
* Clojure utilities.
Expand All @@ -29,10 +31,14 @@ public final <V> V getOrThrow(Map<?, V> m) {
}
}

//Do not require namespace which is already loaded.
private static final Set<Symbol> ALREADY_REQUIRED_NAMESPACES;


// requires stencil.process namespace so stencil is loaded.
static {
final IFn req = RT.var("clojure.core", "require");
req.invoke(Symbol.intern("stencil.process"));
ALREADY_REQUIRED_NAMESPACES = new HashSet<>();
requireNs("stencil.process");
}

/**
Expand All @@ -44,4 +50,15 @@ public final <V> V getOrThrow(Map<?, V> m) {
public static IFn findFunction(String functionName) {
return RT.var("stencil.process", functionName);
}


public static void requireNs(String ns) {
final Symbol nsSym = Symbol.intern(ns);
if (ALREADY_REQUIRED_NAMESPACES.contains(nsSym)) {
return;
}
final IFn req = RT.var("clojure.core", "require");
req.invoke(nsSym);
ALREADY_REQUIRED_NAMESPACES.add(nsSym);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
io.github.erdos.stencil.functions.DefaultCallFnProvider
io.github.erdos.stencil.functions.BasicFunctions$Provider
io.github.erdos.stencil.functions.StringFunctions$Provider
io.github.erdos.stencil.functions.NumberFunctions$Provider
io.github.erdos.stencil.functions.DateFunctions$Provider
io.github.erdos.stencil.functions.LocaleFunctions$Provider

0 comments on commit e05ce09

Please sign in to comment.