Skip to content

tonysparks/jslt2

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

jslt2

VM based implementation of JSLT - which is a JSON query and transformation language.

Example Translations

Given input:

{
	"name" : "Brett Favre",
	"team" : "Green Bay Packers"
}

and given the JSLT script:

{
	"player" : .name + " played for " + .team
}

outputs:

{
	"player" : "Brett Favre played for Green Bay Packers"
}

Implemented

  • All language features
  • The standard library
  • Macro support
  • The JSLT test suite - 99% of tests pass with the exception of the mentioned differences

How to use

Include in your Maven/Gradle project:

<!-- https://mvnrepository.com/artifact/com.github.tonysparks.jslt2/jslt2 -->
<dependency>
    <groupId>com.github.tonysparks.jslt2</groupId>
    <artifactId>jslt2</artifactId>
    <version>0.2.8</version>
</dependency>

How to initialize and evaluate a template expression:

// use out of the box defaults
Jslt2 runtime = new Jslt2(); 

// or, customize the runtime...

Jslt2 runtime = Jslt2.builder()
            .resourceResolver(ResourceResolvers.newFilePathResolver(new File("./examples")))
            .enableDebugMode(true)
            .objectMapper(new ObjectMapper())
            .maxStackSize(1024 * 5)
            .build();
            
            
// Execute a template:
JsonNode input = ...; // some input Json
JsonNode result = runtime.eval("{ \"input\" : . }", input);
            
            
// or, compile a template for repeated use:
Template template = runtime.compile("{ \"input\" : . }");

JsonNode result1 = template.eval(input);
JsonNode input2 = ...; // different json input
JsonNode result2 = template.eval(input2);

Differences between JSLT and JSLT2

  • Allow for including null or empty nodes - which gives a nice performance boost (~5%):
Jslt2 runtime = Jslt2.builder().includeNulls(true).build();
  • Currently no function parameter checks - this is considered a bug in JSLT2
  • Allows block comments via the /* and */ syntax
  • Verbatim strings:
let x = """this
   is a "verbatim"
   string"""
  • Automatic index variable with for expressions. The variable is injected with the value of the current iteration index (zero based) and stored in the variable index__. This variable exists for both Object and Array for expressions:
{for ([1,2,3])
    ("key_" + $index__) : .
}

// outputs:
{
    "key_0":1,
    "key_1":2,
    "key_2":3
}

If you need to embed for expressions, you can reference the parent index by:

{for ([1,2,3])
    let parentIndex = $index__
    ("key_" + $index__) : [for (["a", "b", "c"]) $parentIndex * $index__ ]
}

// outputs:

{
    "key_0":[0,0,0],
    "key_1":[0,1,2],
    "key_2":[0,2,4]
}

  • Performance has been interesting. I've tested on AMD Phenom II and Intel i5; on Intel, JSLT2 can be roughly 5% to 10% faster; and on AMD, JSLT2 is consistently 5%-10% slower. To date, depending on the template the original JSLT code will be generally slightly faster than JSLT2 code.
  • async blocks allow for running expressions in separate threads. This can improve performance for long running expressions. NOTE: This is currently an experimental feature.

As an example:

// runs the makeSlowDatabaseQuery functions in background threads, which allows them to be computed
// in parallel 
async {
  let a = makeSlowDatabaseQuery(.someParam)     
  let b = makeSlowDatabaseQuery(.someOtherParam)
} // evaluation will block here until all let expressions have been computed

{
	"a": $a, // we can now reference the computed value of $a in our template expression
	"b": $b,
}

There are several limitations or "gotchas" with async blocks:

  • Any variables defined in the async block can not reference other variables in the async block:
async {
  let a = "foo"     
  let b = "bar" + $a // INVALID, can't reference $a as this value will be computed in parallel with $b
}

  • Any variables or functions defined outside of the async block must be declared before the async block:
let y = "bar"
async {
  let a = $x // INVALID because x is defined AFTER the async block
  let b = $y // valid, because y is defined BEFORE the async block  
}

let x = "foo"

  • After the async block definition, the variables defined in the async block can be used:

async {
  let a = "hi"  
}

let x = $a // can reference $a in a new variable
def y() $a // can reference $a in a function

{
	"result": $a // can reference $a in the template expression
}

You can customize the ExecutorService provided to the Jslt2 runtime. The default ExecutorService uses daemon threads and Executors.newCachedThreadPool.

// Customize (or use an already created instance) of ExecutorService
ExecutorService service = ...
Jslt2 runtime = Jslt2.builder()
    .executorService(service)    
    .build();