Skip to content
/ xjs-jel Public

A JSON-based data templating language designed for complex functional configurations

License

Notifications You must be signed in to change notification settings

ExJson/xjs-jel

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

55 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

JSON-based Expression Language (JEL)

What is JEL?

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.

Why Use JEL?

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.

Simple Inlining and Templating

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
}

Expanded Inline Syntax

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'}

Arithmetic Expressions

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)

Statemtents via Data Instructions

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
}

Data Templating

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 Flags

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)

Additional Flags

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)

Combining Multiple Flags

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

Conditional Expressions and Statements

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.

Generators and Loops

JEL supports a handful of advanced scripting concepts including loops and generators.

Array 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

Object Generators

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
}

Destructuring

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 ]

Functions

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.

Provided Static Functions

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.

Provided Instance Functions

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()

Meta Expressions

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
}

Class Prototyping

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})$'
]

Delegate Expressions

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] 
  ]
}

Higher Order Templates

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)

About

A JSON-based data templating language designed for complex functional configurations

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages