Implementing a WarpScript function in Java

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.

Implementing a WarpScript™ function in Java

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
  • {...} RANDOMSTRING where the map contains at least the mandatory arguments
    • for instance { "count" 27 "letters" false "numbers" false "chars" "warp" } RANDOMSTRING

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.