JEL is a self-expanding expression language designed to be embedded within existing JSON and JSON-derivative languages.
It is designed to be used inside of existing data formats, rather than on top of one existing format. This enables JEL scripts to be queried and manipulated by any number of existing frameworks, including Jackson, GSON, and various other ecosystems.
JEL is primarily focused on providing simple inlining and tempating features to existing JSON configurations.
For example:
- Variable substitution and inlining
- Arithmetic expressions
- Data templates
- Compile-time schema validations (WIP)
JEL is capable of higher-level language concepts, including loops, generators, IO, and eventually JVM bytecode generation, but another language such as Jsonnet or JSLT might be better for highly-complex configurations and scripting.
JEL is ideal for eliminating redundancy in large configurations with relational data.
For example, assume the following configuration in XJS:
animal: {
type: cat
color: orange
name: Garfield
}
A JSON path expression may be used to query this data for reuse in mulitple locations.
person: {
name: John
favorite pet: $animal.name
}
Note that path components for keys are limited to word
characters (regex: \w
). To reference fields with
special characters, either quote the key or use the
expanded inline syntax (${}
).
values: {
key with spaces: value
}
ref1: $values.'key with spaces'
ref2: ${values.key with spaces}
ref3: ${values.'key with spaces'}
The JEL processor will automatically evaluate any arithmetic expressions embedded within a configuration.
For example,
world: {
height: 128
}
structure: {
height: $world.height - 10
}
The following operators are supported by the processor:
Operator | Description | Example |
---|---|---|
+ | Addition | 2 + 1 |
- | Subtraction | 3 - 2 |
* | Multiplication | 4 * 3 |
/ | Division | 5 / 4 |
^ | Power | 6 ^ 5 |
% | Modulus | 7 % 6 |
| | Bitwise OR | 8 | 7 |
& | Bitwise AND | 9 & 8 |
>> | Bitwise right shift | 10 >> 9 |
<< | Bitwise left shift | 11 << 10 |
() | Parentheses (multiplication) | 12(11) |
() | Parentheses (order) | 13 * (12 + 2) |
JEL supports a number of field flags which may be used as instructions to perform various operations.
To set up a field for use as a statement, begin the key
with the >>
or Logical Alias operator.
For example, to import values from another configuration:
>> import: config.xjs
If this configuration is an object, its keys will be copied into the current file.
For example, assume the following configuration:
config.xjs:
world: {
height: 128
}
tutorial.xjs:
>> import: config.xjs
generator: {
height: $world.height
}
To import this configuration into an object, give it a name.
This name is said to be the value's logical alias because
its technical key is everything to the left of the :
.
The logical alias may be used as the reference for this value.
config >> import: config.xjs
generator: {
height: $config.world.height
}
JEL can be used to define data templates. These may be used like functions to repeatedly generate values.
For example, to define a template which generates an animal object:
animal >> (type, color): {
type: $type
color: $color
}
To use this template, call it by passing any parameters
inside of parentheses (()
).
animals: [
$animal(cat, orange)
$animal(dog, brown)
]
Note that any tokens passed inside of ,
will be inlined
directly as xjs tokens, meaning raw tokens will be treated
as strings. To pass these values explicitly as strings, use
any other type of valid xjs string: (""
, ''
, """"""
).
animals: [
$animal('bird', 'blue')
]
Data templates can be used to return any type of value.
For example, to write a function which adds 2 to any number:
add2 >> (number): $number + 2
four: $add2(2)
Jel provides a variety of field flags out of the box. These flags can be used either as instructions or modifiers.
For example, to define a variable (different from a regular
value), attach the var
flag. This excludes the field from
the output.
first >> var: Bob
last >> var: Smith
name: $first $last
Exluding a field from the output does not prevent it from being exported, meaning the value is still visible to other configurations.
To prevent this, flag the field as private
.
utitlities.xjs:
secret >> private: 3.14
circumference >> (radius):
2 * $radius * $secret
tutorial.xjs:
utils >> import: utilities.xjs
radius: 1.5
circumference: $utils.circumference($radius)
The following flags are also supported out of the box:
Flag | Description |
---|---|
var |
Excludes the field from the output. |
private |
Hides the field from other files. |
add |
Adds values into another container. |
set |
Updates a value by its qualified path. |
def |
Defines a new value dynamically. |
import |
Reads data from another file. |
export |
Exports data into another file. (TBD) |
noinline |
Skips evaluation for one field. |
meta |
Attaches a config to another expression. |
from |
Destructures a container into multiple values. |
log |
Prints data to the standard output. |
jel |
Configures the JEL processor for one file. (TBD) |
JEL places no restrictions on the nubmer of flags. Expressions will be evaluated in the order in which they are written, but any flags generally do not benefit from order.
For example, to import values from another file and prevent them from being re-exported:
>> import var: config.xjs
JEL supports conditional expressions and statements via the
if
operator.
For example, to conditionally execute a statement with side effects:
config >> import var: config.xjs
>> if ($config.height > 100): {
>> log: Using an extremely tall structure!
}
Or, to use a compressed syntax by combining flags:
config >> import var: config.xjs
>> if ($config.height > 100) log:
Using an extremely tall structure!
For a more advanced conditional tree, place the if
token
at the end of the key. The value of this expression is an
object where each key is the condition.
height: 150
size >> if: {
$height < 50: small
$height < 100: large
_: giant
}
Finally, to compare values by equality, use a match
expression:
fruit: banana
preference >> match $fruit: {
apple: My favorite
banana: Second favorite
orange: Third favorite
_: Not interested
}
match
expressions can be combined with single conditions as guards:
fruit: banana
preference >> match $fruit: {
apple >> if ($rand() < 0.50):
Apple and lucky!
_:
Not an apple or not lucky.
}
match
statements can be used to check objects for specific fields
and arrays for any number of elements.
fruit: {
type: banana
color: yellow
}
preference >> match $fruit: {
{type: banana}: I like it!
_: I don't like it or type mismatch
}
Note that presently, conditional expressions are only possible in objects. There is currently no syntax for conditional expressions inside of arrays.
JEL supports a handful of advanced scripting concepts including loops and generators.
Array generators (and regular loops) are written by placing an array after the logical alias operator.
For example, to loop over a set of pre-defined values:
>> [1, 2, 3]: {
>> log: [
Current value: $v
Current index: $i
]
}
Or, combine operators to get the highest number:
max: null
>> [1, 2, 3] if ($v > $max): {
max >> set: $v
}
To generate an array, simply provide an alias:
squares >> [1, 2, 3]: $v ^ 2
Copy values from another array:
input: [1, 2, 3]
cubes >> [$input..]: $v ^ 3
Combine operators to filter values:
odds >> [1, 2, 3] if ($v %2 != 0): $v
To iterate over the keys, values, and indices of an object:
>> {type: baloon, color: red}: {
>> log: [
key: $k
value: $v
index: $i
]
}
To generate an object, provide an alias:
numbers >> {one: 1, two: 2, three: 3}: {
number_$k >> def: $v
}
Copy values from another object:
input: {one: 1, two: 2, three: 3}
numbers: {$input..}: {
number_$k >> def: $v
}
Combine operators to filter keys and values:
odds >> {one: 1, two: 2, three: 3}
if ($v % 2 != 0): {
number_$k >> def: $v
}
Note that in xjs, a key does not end until the first
exposed :
, so these definitions may run onto more
than one line:
numbers >> {
one: 1
two: 2
three: 3
}: {
number_$k >> def: $v
}
In JEL, objects and arrays may be destructured in order to extract values by pattern.
For example, to copy values from another object:
config: {
world: { height: 128 }
structure: { type: nbt }
metadata: { author: PersonTheCat }
}
{world, structure} >> from: $config
Combine operators to get a handful of specific keys
from an import
expression:
{world, structure} >> import from: config.xjs
Use a very explicit syntax to declare these values as variables:
{world} >> import var from: config.xjs
To copy values from an array:
[first, second, _, fourth] >> from: [ 1, 2, 3, 4 ]
While JEL does provide a syntax for declaring templates, these are not true functions.
Extension functions are supported by the ecosystem, but these must be defined in-code.
JEL provides a handful of static methods for interacting with, querying, and generating data.
The language specification does provide support for overloaded functions, so several of these functions may have the same identifier.
Signature | Description |
---|---|
max() |
Returns the highest possible value. |
max(a: number, b: number) |
Returns the highest of 2 values. |
max(a: array) |
Returns the highest value from an array |
min() |
Returns the lowest possible value. |
min(a: number, b: number) |
Returns the lowest of 2 values./td> |
min(a: array) |
Returns the lowest value from an array. |
rand() |
Returns a random decimal from 0 to 1. |
rand(minIn: number, maxEx: number) |
Returns a random integer with inclusive lower and exclusive higher bounds. |
rand(a: array) |
Returns a random element from an array. |
JEL also provides a handful of instance functions.
Signature | Description |
---|---|
hash() |
Generates an integer hash from the value. |
startsWith(value: any) |
Returns true if a number, string, array, or object starts with the other. |
endsWith(value: any) |
Returns true if a number, string, array, or object starts with the other. |
contains(value: any) |
Returns true if a number, string, array, or object contains the other. |
size() |
Returns the number of elements or length of a value. |
coerce(values: any...) |
Returns the first nonnull parameter. |
matches(regex: string) |
Returns true if the string form of this value matches the given regular expression. |
replace(regex: string, value: string) |
Replaces all matches with the given replacement. |
uppercase() |
Coerces this value into an all-caps string. |
lowercase() |
Coerces this value into a lowercase string. |
trim() |
Coerces this value into a string and removes any leading or trailing whitespace. |
isNumber() |
Returns true if the value is a number. |
isBoolean() |
Returns true if the value is a boolean. |
isString() |
Returns true if the value is a String. |
isObject() |
Returns true if the value is an object. |
isArray() |
Returns true if the value is an array. |
isNull() |
Returns true if the value is null. |
intoNumber() |
Coerces the value into a number. |
intoBoolean() |
Coerces the value into a boolean. |
intoString() |
Coerces the value into a String. |
intoObject() |
Coerces the value into an object. |
intoArray() |
Coerces the value into an array. |
Instance functions must be called from an identifier.
// Illegal syntax. This will fail to compile.
// illegal: [1, 2, 3].hash()
array: [ 1, 2, 3 ]
legal: $array.hash()
In JEL, some expressions have a meta-form which takes a
configuration on the right-hand side. To use these variants,
provide the meta
tag anywhere on the left-hand side.
For example, to use a meta template:
type >> meta (input): {
// Any condition not met raises an error.
validations: {
$input.isNull(): I don't like null values!
}
// Perform any side effects in order.
>> log: Input hash: $input.hash()
// The final value of this field is
// returned by the template.
return >> if: {
$input.isArray() || $input.isObject():
container
_:
not a container
}
}
Meta generators have the same fields.
array >> meta [1, 2, 3]: {
// Perform side effects and validations.
>> if $($v % 2 == 0) log: Even number: $v
// The final value of this field is
// the next element.
return: $v * 2
}
Meta imports allow parsing unrecognized extensions:
>> meta import: {
file: unknown.ext
type: xjs
}
JEL provides specifications for defining class templates.
The class template is an outline of which fields are required for an object, their data type, as well as any default values and methods.
For example, to define a template with required fields:
Fruit >> class: {
type: string
color: string
}
To flag any field as optional, place a ?
token immediately
after the key.
Fruit >> class: {
type: string
color?: string
}
To provide default flags for these fields, simply attach them to the type definition:
Fruit >> class: {
type: string
color >> private: string
}
To provide a default value, use the extended field descriptor syntax.
Fruit >> class: {
type: string
color: {
type: string
default: yellow
}
}
The type definition can be any of the following options:
- A regular expression, e.g.
\\w{3}_\\w{3}
- Another type, starting with
@
string
number
boolean
array
object
any
To use this definition, pass a value into it using it a delegate expression.
fruit >> @Fruit: {
type: banana
color: yellow
}
If every field in the class definition is a regular field, the processor will generate a second template which may be used as a constructor.
Fruit >> class: {
type: string
color?: string
}
banana: $Fruit(banana, yellow)
banana2: $Fruit(banana)
Finally, class definitions also support nested template expressions. These can be defined as follows:
Fruit >> class: {
type: string
color?: string
print_color >> (): {
>> log if: {
$color: My color is $color!
_: I don't have a color.
}
}
}
fruit >> @Fruit: {
type: banana
}
>>: fruit.print_color()
To require that a template be overridden, simply leave
the field null
.
Fruit >> class: {
print_color >> (): null
}
The implementor is not required to specify any field flags or expressions, but is encouraged to do so for readability.
fruit >> @Fruit: {
print_color: {
>> log: I am red!
}
}
JEL also provides specifications for a second kind of type
type delcaration: the enum
type. This is essentially a
list of possible values and or patterns for a given field.
For example,
color >> enum: [
red
green
blue
'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$'
]
In JEL, A delegate expression is an expression is any expression that transforms the RHS value by passing it into a template or class.
This can be used to declare types--as in the previous example--or to pass the RHS value through a template before evalutating it.
For example, imagine that a program has the following field with example data:
height: [ 0, 64 ]
The user may wish to define an expression which transforms this field into a different syntax:
WORLD_HEIGHT >> var: 128
offset >> (array): [
$array[0],
$WORLD_HEIGHT - $array[1]
]
// 0 to 100 if WORLD_HEIGHT == 128
height >> @offset: [ 0, 28 ]
For an advanced example, add some validations to guarantee that the second parameter cannot be negative.
WORLD_HEIGHT >> var: 128
offset >> meta (array): {
validations: {
$WORLD_HEIGHT - $array[1] > 0:
$array[1] is too high!
}
return: [
$array[0],
$WORLD_HEIGHT - $array[1]
]
}
The templating system in JEL tolerates repeated template declarations within a key.
For example,
sum >> (a) (b): $a + $b
add2 >> (a): $sum($a)(2)
four: $sum(2)(2)
five: $add2(3)
In many cases, this will be easier to express when the parent function takes the form of a meta template.
circle >> meta (radius): {
circumference: 2 * 3.14 * $radius
return >> (color): {
circumference: $circumference
color: $color
}
}
red circle: $circle(1.5)(red)