Packages
boxic_feel
0.1.0
A native Elixir parser and evaluator for the Friendly Enough Expression Language defined by DMN.
Current section
Files
Jump to
Current section
Files
lib/boxic_feel.ex
defmodule Boxic.FEEL do
@moduledoc """
Parses and evaluates FEEL expressions.
The public facade accepts FEEL source through `parse/1` and `evaluate/2`, or
an already parsed syntax tree through `evaluate_ast/2`.
"""
alias Boxic.FEEL.{AST, Error, Evaluator, Parser, Tokenizer}
@type ast ::
{:literal, term()}
| {:identifier, String.t()}
| {:unary, atom(), ast()}
| {:binary, atom(), ast(), ast()}
| {:list, [ast()]}
| {:context, [{String.t(), ast()}]}
| {:path, ast(), String.t()}
| {:filter, ast(), ast()}
| {:range, boolean(), boolean(), ast(), ast()}
| {:in, ast(), ast()}
| {:unary_test, atom(), ast()}
| {:unary_tests, [ast()]}
| {:sequence, ast(), ast()}
| {:if, ast(), ast(), ast()}
| {:for, String.t(), ast(), ast()}
| {:for, [{String.t(), ast()}], ast()}
| {:quantifier, :some | :every, String.t(), ast(), ast()}
| {:between, ast(), ast(), ast()}
| {:instance_of, ast(), String.t()}
| {:function, [String.t()], ast()}
| {:call, ast(), [ast()]}
@type context :: %{optional(String.t()) => term()}
@doc """
Parses a FEEL expression into Boxic's public AST representation.
iex> Boxic.FEEL.parse("1 + 2")
{:ok, {:binary, :plus, {:literal, Decimal.new("1")}, {:literal, Decimal.new("2")}}}
"""
@spec parse(String.t()) :: {:ok, ast()} | {:error, Error.t()}
def parse(expression) when is_binary(expression) do
trimmed = String.trim(expression)
if trimmed == "" do
{:error, %Error{code: :empty_expression, message: "expression cannot be empty"}}
else
with {:ok, tokens} <- Tokenizer.tokenize(trimmed),
{:ok, ast, []} <- Parser.parse_expression(tokens),
:ok <- AST.validate(ast) do
{:ok, ast}
else
{:ok, _ast, rest} ->
{:error, %Error{code: :invalid_syntax, message: "unexpected tokens: #{inspect(rest)}"}}
{:error, %Error{} = error} ->
{:error, error}
end
end
end
@doc """
Parses and evaluates a FEEL expression.
iex> Boxic.FEEL.evaluate("price * quantity", %{
...> "price" => Decimal.new("12.50"),
...> "quantity" => Decimal.new("2")
...> })
{:ok, Decimal.new("25.00")}
"""
@spec evaluate(String.t(), context()) :: {:ok, term()} | {:error, Error.t()}
def evaluate(expression, context \\ %{}) when is_binary(expression) and is_map(context) do
with {:ok, ast} <- parse(expression), do: evaluate_ast(ast, context)
end
@doc "Evaluates an AST previously returned by `parse/1`."
@spec evaluate_ast(ast(), context()) :: {:ok, term()} | {:error, Error.t()}
def evaluate_ast(ast, context) when is_map(context) do
with :ok <- AST.validate(ast), do: Evaluator.eval(ast, context)
end
@doc """
Evaluates a FEEL unary test against `value`.
iex> Boxic.FEEL.evaluate_unary_test(">= 18", Decimal.new("21"))
{:ok, true}
"""
@spec evaluate_unary_test(String.t(), term(), context()) ::
{:ok, boolean()} | {:error, Error.t()}
def evaluate_unary_test(test_expression, value, context \\ %{})
when is_binary(test_expression) and is_map(context) do
Evaluator.evaluate_unary_test(test_expression, value, context)
end
end