Skip to content

Latest commit

 

History

History
193 lines (173 loc) · 6.99 KB

FULL_DESCRIPTION.md

File metadata and controls

193 lines (173 loc) · 6.99 KB

Full ExPressions language description

Table of contents

  1. Data types
    1. Numbers
    2. Booleans (and null)
    3. Strings
    4. Arrays
    5. Objects
  2. Operators
    1. Access operators
    2. String operators
    3. Arithmetic operators (and braces)
    4. Logical operators
  3. Function calls
  4. Special symbols
  5. Variable bindings
  6. Extend expressions with new functions
  7. Standard library functions
  8. Errors handling

Data types

Language has support for all JSON data types:

Numbers

Integers and floats:

iex> ExPression.eval("1")
{:ok, 1}
iex> ExPression.eval("1.01")
{:ok, 1.01}
iex> ExPression.eval("1e-3")
{:ok, 0.001}

Booleans (and null)

iex> ExPression.eval("true")
{:ok, true}
iex> ExPression.eval("false")
{:ok, false}
iex> ExPression.eval("null")
{:ok, nil}

Strings

iex> ExPression.eval(~s("Hello, World!"))
{:ok, "Hello, World!"}
iex> ExPression.eval(~s("Привет, мир!"))
{:ok, "Привет, мир!"}

Arrays

iex> ExPression.eval("[1, [2, 3]]")
{:ok, [1, [2, 3]]}

Objects

iex> ExPression.eval(~s({"en": "England", "fr": "France"}))
{:ok, %{"en" => "England", "fr" => "France"}}

Note:

Functions in expressions can return any of Elixir types:

defmodule MyFunctions do
  def new_date(y, m, d), do: Date.new(y, m, d)
end

iex> ExPression.eval("new_date(2024, 1, 1)", functions_module: MyFunctions)
{:ok, ~D[2022-01-30]}

Operators

Access operators

  1. Access object's field value using dot syntax:
iex> ExPression.eval(~s({"en": "England", "fr": "France"}.fr))
{:ok, "France"}
# dot operator also works with atom keys
iex> ExPression.eval("x.year", bindings: %{"x" => ~D[2022-02-02]})
{:ok, 2022}
  1. Access object's field value with braces (in braces can be any expression):
iex> ExPression.eval(~s({"en": "England", "fr": "France"}["fr"]))
{:ok, "France"}
  1. Access array's element by index:
iex> ExPression.eval(~s([1, 2, 3][0]))
{:ok, 1}

String operators

+ (concatenation):

iex> ExPression.eval(~s("abc" + "def"))
{:ok, "abcdef"}

Arithmetic operators (and braces)

+, -, *, /:

iex> ExPression.eval("1 + 2 * (3 + 4) / 5")
{:ok, 3.8}

Braces are supported in general, not only for arithemtic operators, but here is an example.

Logical operators

and, or, not, ==, !=, <, <=, >, >=:

iex> ExPression.eval("not (1 == 2) or false")
{:ok, true}

Operators follow python semantics:

iex> ExPression.eval(~s(0 or "" or [] or [1, 2]))
{:ok, [1, 2]}
iex> ExPression.eval(~s([] < [1, 2] and "12" < "123"))
{:ok, true}

Function calls

Familiar syntax for function calls with standard library of functions.

iex> ExPression.eval("min(1, 2)")
{:ok, 1}

Special symbols

Language supports special symbols ($) for most frequent operations in your domain field:

defmodule MyFunctions do
  # use $ special symbol in expressions
  def handle_special("$", date_str), do: Date.from_iso8601!(date_str)
  # Use diff function in expressions
  def diff(date_1, date_2), do: Date.diff(date_1, date_2)
end

iex> ExPression.eval(~s/diff($"2023-02-02", $"2022-02-02")/, functions_module: MyFunctions)
{:ok, 365}

Special symbol can be followed by an identifier ($i_am_special) or a string ($"I am special!"). To use special symbols, you have to provide functions_module and define a function handle_special(symbol, value). Return value of the function will be used in expression.

Variable bindings

Pass variables to expressions using :bindings option:

iex> ExPression.eval(~s({"en": "England", "fr": "France"}[country_code]), bindings: %{"country_code" => "en"})
{:ok, "England"}

Extend expressions with new functions

Define functions that you want to allow your user to use in expressions in some module, then pass this module with :functions_module option:

defmodule MyFunctions do
  # Use my_sum function in expressions
  def my_sum(a, b), do: a + b
end

iex> ExPression.eval("my_sum(1, 2)", functions_module: MyFunctions)
{:ok, 3}

In case your function and function from ExPression's standard library have the same name and arity, your function will be called.

Standard library functions

Functions have same names and signatures as python's builtins.

Function Argument types Description
len(term) term can be string or array or object Return the length (the number of items) of an object
abs(term) number Returns the absolute value of a number
str(term) term can be any of ExPression types In case of string does nothing, in other cases - returns string representation of data
int(term) term can be number or string truncates number to integer
round(number) number rounds number to an integer
round(number, precision) number rounds number with given precision
min(array) array returns minimum of an array, raises Enum.EmptyError in case of empty array
min(a, b) a and b can be any comparable types returns minimum of a and b
max(array) array returns maximum of an array, raises Enum.EmptyError in case of empty array
max(a, b) a and b can be any comparable types returns maximum of a and b
pow(base, exponent) base and exponent have to be numbers (integer or floating point) returns a to the power of b. If both are integers - result is integer. Otherwise - floating point

Errors handling

In case of expected error, functions will return {:error, error} tuple, where error is a structure %ExPression.Error{}. In unexpected cases functions may raise an exception. Here is a list of types of possible errors:

  • UndefinedVariableError - when variable, that is used in expressions, was not found in bindings map.
  • UndefinedFunctionError - when function, that is used in expressions, was not found in functions_module and standard library.
  • FunctionCallException - when function, called in expression, raised an exception.
  • BadOperationArgumentTypes - invalid types of arguments for operation (for example, 1 + "str").
  • SpecialWithoutModule - when special symbol is used in expression, but module :functions_module was not provided.