Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Function provider SPI #138

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Function provider SPI
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
commit e05ce09f2cba931797505af3b7db5d2200de0c3e
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