Learn how to implement a WarpScript function in Java, so you can make your own extension or contribute to the source code of Warp 10.
In this tutorial, we will learn how to implement a WarpScript function in Java. For example, this can be useful if you want to contribute to Warp 10 source code, or if you want to build a custom WarpScript extension.
Learn more about WarpScript. |
A first example
A WarpScript function is a Java class that implements the interface WarpScriptStackFunction
. That is, it must have an apply
method that takes a WarpScriptStack
object as argument (which is the execution environment of the WarpScript functions), uses it to retrieve its arguments and then returns its results.
For instance, in what follows, we write a function that generates a random ASCII String (we will just wrap RandomStringUtils.randomAscii
into a WarpScript function):
package io.exampleExtension;
import io.warp10.script.NamedWarpScriptFunction;
import io.warp10.script.WarpScriptException;
import io.warp10.script.WarpScriptStack;
import io.warp10.script.WarpScriptStackFunction;
import org.apache.commons.lang3.RandomStringUtils;
public class RANDOMASCII extends NamedWarpScriptFunction implements WarpScriptStackFunction {
public RANDOMASCII(String name) {
super(name);
}
@Override
public Object apply(WarpScriptStack stack) throws WarpScriptException {
// get the argument and check its type
Object o = stack.pop();
if (!(o instanceof Long)) {
throw new WarpScriptException(getName() + " expects a LONG on top of the stack.");
}
// convert the argument and call function
int count = ((Long) o).intValue();
String result = RandomStringUtils.randomAscii(count);
// return the result
stack.push(result);
return stack;
}
}
Then, this function needs to be added to a library of functions. For example, you can put it in the static functions
map of the WarpScriptLib
source file if this function is part of a pull request for the Warp 10 Platform or if you are working on your own fork.
Another situation is when this function is part of a WarpScript extension. A WarpScript extension is simply a class that extends WarpScriptExtension
. It has a method called getFunctions()
which returns a Map
of WarpScript functions indexed by their names. For example, you can write one like this:
package io.exampleExtension;
import io.warp10.script.NamedWarpScriptFunction;
import io.warp10.warp.sdk.WarpScriptExtension;
import java.util.HashMap;
import java.util.Map;
public class MyAwesomeWarpScriptExtension extends WarpScriptExtension {
private static Map <String, Object> functions = new HashMap<String, Object>();
static {
functions.put("RANDOMASCII", new RANDOMASCII("RANDOMASCII"));
functions.put("RANDOMSTRING", new RANDOMSTRING("RANDOMSTRING"));
}
@Override
public Map<String, Object> getFunctions() {
return functions;
}
}
For more information on packaging and loading an extension, you can refer to the documentation. To publish, manage, or automatically install an extension, there is WarpFleet.
An example of a function with multiple arguments
In the previous example, the function that is implemented has only one argument, so its interactions with the stack are simple: retrieve the argument, check its type and apply the function on the WarpScript execution environment (the stack).
If there are multiple arguments, the interactions with the stack can be more complex, extracting multiple arguments, checking their type and throwing exceptions for incorrect syntax, and handling arguments that are optional and those that are not.
Learn how to host web content in Warp 10 |
If you want to simplify some of that work, the abstract class FormattedWarpScriptFunction
can handle some of those tasks automatically, so that its subclass can focus on implementing the function's operation. In the child's class, the arguments are specified using a builder pattern: an ArgumentsBuilder
object has methods that register the arguments of the function. These arguments are used by the parent class to create a Map
of parameters. The subclass can then use this map directly, rather than interacting with the stack.
For example, the following code wraps the method RandomStringUtils.Random
into a WarpScript function:
package io.exampleExtension;
import io.warp10.script.WarpScriptException;
import io.warp10.script.WarpScriptStack;
import io.warp10.script.formatted.FormattedWarpScriptFunction;
import org.apache.commons.lang3.RandomStringUtils;
import java.util.Map;
public class RANDOMSTRING extends FormattedWarpScriptFunction {
private Arguments args;
private static final String COUNT = "count";
private static final String LETTERS = "letters";
private static final String NUMBERS = "numbers";
private static final String START = "start";
private static final String END = "end";
private static final String CHARS = "chars";
@Override
protected Arguments getArguments() {
return args;
}
public RANDOMSTRING(String name) {
super(name);
// add a description
getDocstring().append("Creates a random string based on a variety of options.");
//
// Build the arguments:
// - addArguments() adds a mandatory argument by its class, name and description,
// - addOptionalArgument() adds an optional argument (it also requires a default value),
// - build() creates and fixes the arguments of the function
//
args = new ArgumentsBuilder()
.addArgument(Long.class, COUNT, "The length of random string to create")
.addArgument(Boolean.class, LETTERS, "Only allow letters?")
.addArgument(Boolean.class, NUMBERS, "Only allow numbers?")
.addOptionalArgument(Long.class, START, "The position in set of chars to start at", 0 L)
.addOptionalArgument(Long.class, END, "The position in set of chars to end before", 0 L)
.addOptionalArgument(String.class, CHARS, "The set of chars (a String) to choose randoms from. If empty, then it will use the set of all chars.", "")
.build();
//
// Other methods for ArgumentBuilder() includes:
// - addListArgument / addOptionalListArgument: same as addArgument / addOptionalArgument but for List<class>
// - addMapArgument / addOptionalMapArgument: ditto for Map<class1,class2>
// - firstArgIsListExpandable: first argument can be passed as a list or not, in which case the result is a list or not (on head 2.1.1 since commit 0e2db1d)
//
}
@Override
protected WarpScriptStack apply(Map < String, Object > params, WarpScriptStack stack) throws WarpScriptException {
// retrieve the arguments
int count = ((Long) params.get(COUNT)).intValue();
int start = ((Long) params.get(START)).intValue();
int end = ((Long) params.get(END)).intValue();
boolean letters = Boolean.TRUE.equals(params.get(LETTERS));
boolean numbers = Boolean.TRUE.equals(params.get(NUMBERS));
char[] chars = ((String) params.get(CHARS)).toCharArray();
if (0 == chars.length) {
chars = null;
} else {
end = Math.min(chars.length, end);
}
// call the function and return the result
String result = RandomStringUtils.random(count, start, end, letters, numbers, chars);
stack.push(result);
return stack;
}
}
After adding this function to WarpScriptLib or to an extension, there are two possibilities in WarpScript to call it:
-
count:LONG letters:BOOLEAN letters:BOOLEAN RANDOMSTRING
,- for instance
34 false false RANDOMSTRING
- for instance
{...} RANDOMSTRING
where the map contains at least the mandatory arguments- for instance
{ "count" 27 "letters" false "numbers" false "chars" "warp" } RANDOMSTRING
- for instance
Additionally, it is possible to automatically generate the documentation of a FormattedWarpScriptFunction
: see the class DocumentationGenerator
and the class RunAndGenerateDocumentationWithUnitTests
.
Unit Tests
Unit testing the implementation of a WarpScript function is good practice for obvious reasons.
For example, a unit test of the previous examples can be done by the following class:
package io.exampleExtension;
import io.warp10.WarpConfig;
import io.warp10.script.MemoryWarpScriptStack;
import io.warp10.script.WarpScriptLib;
import org.junit.BeforeClass;
import org.junit.Test;
import java.io.StringReader;
public class MyAwesomeWarpScriptExtensionTest {
@BeforeClass
public static void beforeClass() throws Exception {
StringBuilder properties = new StringBuilder();
properties.append("warp.timeunits=us");
WarpConfig.safeSetProperties(new StringReader(properties.toString()));
WarpScriptLib.register(new MyAwesomeWarpScriptExtension());
}
@Test
public void RANDOMSTRING_test() throws Exception {
MemoryWarpScriptStack stack = new MemoryWarpScriptStack(null, null);
stack.maxLimits();
//
// For this example, we only check that the functions run without error, and that the sizes are correct
//
stack.exec("23 RANDOMASCII");
stack.exec("DUP SIZE 23 == ASSERT");
stack.exec("34 false false RANDOMSTRING");
stack.exec("DUP SIZE 34 == ASSERT");
// for a FormattedWarpScriptFunction that sets at least one of its optional arguments, its arguments are passed using a Map.
stack.exec("{ 'count' 27 'letters' false 'numbers' false 'chars' 'warp' } RANDOMSTRING");
stack.exec("DUP SIZE 27 == ASSERT");
// printing
System.out.println(stack.dump(stack.depth()));
}
}
Unit testing your functions can also be done directly in WarpScript code by providing a .mc2
file containing those tests.
Conclusion
Congratulations! With this tutorial, you have learned to write custom WarpScript functions in Java. So you can build your own WarpScript extension and/or contribute to the source code.
You can retrieve this example on its GitHub repository.
Read more
Archiving Time Series Data into Amazon S3
Conversions to Apache Arrow format
Server monitoring with Warp 10 and Telegraf
Machine Learning Engineer