Full ExPressions language description
February 19, 2024 · View on GitHub
Table of contents
- Data types
- Operators
- Function calls
- Special symbols
- Variable bindings
- Extend expressions with new functions
- Standard library functions
- 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
- 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}
- Access object's field value with braces (in braces can be any expression):
iex> ExPression.eval(~s({"en": "England", "fr": "France"}["fr"]))
{:ok, "France"}
- 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 inbindingsmap.UndefinedFunctionError- when function, that is used in expressions, was not found infunctions_moduleand 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_modulewas not provided.