Packages
expression
2.31.6
2.49.1
2.49.0
2.48.0
2.47.5
2.47.4
2.47.3
2.47.2
2.47.1
2.47.0
2.46.0
2.45.1
2.45.0
2.44.1
2.44.0
2.43.0
2.42.1
2.41.4
2.41.3
2.41.2
2.41.0
2.40.0
2.38.0
2.37.0
2.36.2
2.36.1
2.36.0
2.35.0
2.34.0
2.33.1
2.33.0
2.32.1
2.31.6
2.31.3
2.31.0
2.30.0
2.28.0
2.27.3
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.2
2.24.1
2.24.0
2.23.9
2.23.8
2.23.7
2.23.6
2.23.4
2.23.3
2.23.2
2.23.1
2.23.0
2.22.2
2.22.0
2.21.0
2.20.0
2.19.0
2.18.0
2.17.0
2.16.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.1
2.10.0
2.9.0
2.8.0
2.6.0
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.1
2.4.0
2.3.2
2.3.1
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.1
1.1.0
1.0.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.1.0
A Excel like expression parser, compatible with FLOIP Expression language.
Current section
Files
Jump to
Current section
Files
lib/expression/v2.ex
defmodule Expression.V2 do
@moduledoc """
A second attempt at the parser, hopefully a little easier to read & maintain.
`parse/1` parsed an Expression into AST.
`eval/3` evaluates the given AST using the context supplied.
For details on how this is done please read `Expression.V2.Parser` and
`Expression.V2.Compile`.
This parser & evaluator supports the following:
* [strings](https://hexdocs.pm/elixir/typespecs.html#basic-types) either double or single quoted.
* [integers](https://hexdocs.pm/elixir/typespecs.html#basic-types) such as `1`, `2`, `40`, `55`
* [floats](https://hexdocs.pm/elixir/typespecs.html#basic-types) such as `3.141592653589793`
* [booleans](https://hexdocs.pm/elixir/typespecs.html#basic-types) which can be written in any mixed case such as `tRue` or `TRUE`, `False` etc
* `Range.t` such as `1..10`, also with steps `1..10//2`
* `Date.t` such as `2022-01-01` which is parsed into `~D[2022-01-01]`
* `Time.t` such as `10:30` which is parsed into `~T[10:30:00]`
* ISO formatted `DateTime.t` such as `2022-05-24T00:00:00` which is parsed into `~U[2022-05-24 00:00:00.0Z]`
* US formatted `DateTime.t` such as `01-02-2020 23:23:23` which is parsed into `~U[2020-02-01T23:23:23Z]`
* Lists of any of the above, such as `[1, 2, 3]` or `[1, 1.234, "john"]`
* Reading properties off of nested objects such as maps with a full stop, such as `contact.name` returning `"Doe"` from `%{"contact" => %{"name" => "Doe"}}`
* Reading attributes off of maps, such as `contact[the_key]` which returns `"Doe"` from `%{"contact" => %{"name" => "Doe"}, "the_key" => "name"}`
* Anonymous functions with `&` and `&1` as capture operators, `&(&1 + 1)` is an anonymous function that increments the input by 1.
The result of a call to `eval/3` is a list of typed evaluated items. It is up to the integrating library to determine how
best to convert these into a final end user representation.
# Examples
iex> alias Expression.V2
iex> V2.eval("the date is @date(2022, 2, 20)")
["the date is ", ~D[2022-02-20]]
iex> V2.eval("the answer is @true")
["the answer is ", true]
iex> V2.eval("22 divided by 7 is @(22 / 7)")
["22 divided by 7 is ", 3.142857142857143]
iex> V2.eval(
...> "Hello @proper(contact.name)! Looking forward to meet you @date(2023, 2, 20)",
...> V2.Context.new(%{"contact" => %{"name" => "mary"}})
...> )
["Hello ", "Mary", "! Looking forward to meet you ", ~D[2023-02-20]]
iex> V2.eval("@map(1..3, &date(2023, 1, &1))")
[[~D[2023-01-01], ~D[2023-01-02], ~D[2023-01-03]]]
iex> V2.eval(
...> "Here is the multiplication table of @number: @(map(1..10, &(&1 * number)))",
...> V2.Context.new(%{"number" => 5})
...> )
[
"Here is the multiplication table of ",
5,
": ",
[5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
]
"""
alias Expression.V2.Compile
alias Expression.V2.Context
alias Expression.V2.Parser
@doc """
Parse a string with expressions into AST for the compile step
"""
@spec parse(String.t()) ::
{:ok, [term]}
| {:error, reason :: String.t(), bad_parts :: String.t()}
def parse(expression) do
case Parser.parse(expression) do
{:ok, ast, "", _, _, _} ->
{:ok, ast}
{:ok, _ast, remaining, _, _, _} ->
{:error, "Unable to parse remainder", remaining}
end
end
@spec escape(String.t()) :: String.t()
def escape(expression) when is_binary(expression) do
String.replace(expression, ~r/@([a-z]+)(\(|\.)/i, "@@\\g{1}\\g{2}")
end
@doc """
This function is referenced by `Expression.V2.Compile` to
make access to values in Maps or Lists easier
"""
@spec read_attribute(map | list, binary | integer) :: term
def read_attribute(map, item) when is_map(map), do: Map.get(map, item)
def read_attribute(list, index) when is_list(list) and is_integer(index),
do: Enum.at(list, index)
def read_attribute(list, range) when is_list(list) and is_struct(range, Range),
do: Enum.slice(list, range)
@doc """
Parse a string with an expression block into AST for the compile step
"""
@spec parse_block(String.t()) ::
{:ok, [term]} | {:error, reason :: String.t(), bad_parts :: String.t()}
def parse_block(expression_block) do
case Parser.expression(expression_block) do
{:ok, ast, "", _, _, _} -> {:ok, ast}
{:ok, _ast, _remainder, _, _, _} -> {:error, "Unable to parse", expression_block}
{:error, _ast, remaining, _, _, _} -> {:error, "Unable to parse remainder", remaining}
end
end
@doc """
Evaluate a string with an expression block against a context
"""
@spec eval_block(String.t(), context :: Context.t()) ::
term | {:error, reason :: String.t(), bad_parts :: String.t()}
def eval_block(expression_block, context \\ Context.new())
def eval_block(expression_block, map_context)
when is_map(map_context) and not is_struct(map_context, Context) do
eval_block(expression_block, Context.new(map_context))
end
def eval_block(expression_block, context) do
with {:ok, ast} <- parse_block(expression_block) do
hd(eval_block_ast(ast, context))
end
end
@doc """
Evaluate a string with expressions against a given context
"""
@spec eval(expression :: String.t(), context :: Context.t()) :: [term]
def eval(expression, context \\ Context.new())
def eval(expression, map_context)
when is_map(map_context) and not is_struct(map_context, Context) do
eval(expression, Context.new(map_context))
end
def eval(expression, context) when is_binary(expression) do
with {:ok, parsed_parts} <- parse(expression) do
eval_ast(parsed_parts, context)
end
end
@doc """
Evaluate a parsed Expression against a given context
"""
@spec eval_ast([term], context :: Context.t()) :: [term()]
def eval_ast(parsed_parts, context \\ Context.new()) do
Enum.flat_map(parsed_parts, fn
binary when is_binary(binary) -> [binary]
ast when is_list(ast) -> eval_block_ast(ast, context)
end)
end
@doc """
Evaluate the given AST against a given context
"""
@spec eval_block_ast([term], context :: Context.t()) :: [term]
def eval_block_ast(ast, context) when is_list(ast) do
function = Compile.compile(ast)
resp = function.(context)
if is_binary(resp) do
# NOTE: if the response was a binary, the user is expecting a
# a string to be returned so make sure we do that.
[eval_as_string(resp, context)]
else
[resp]
end
end
@doc """
Evaluate an expression and cast all items to strings before joining
the full result into a single string value to be returned.
This calls `eval/2` internally, maps the results with `default_value/2`
followed by `stringify/1` and then joins them.
"""
@spec eval_as_string(String.t(), Context.t()) :: String.t()
def eval_as_string(expression, context \\ Context.new()) do
{:ok, ast} = parse(expression)
ast
|> eval_ast(context)
|> Enum.zip(ast)
|> Enum.map_join("", fn
{nil, [{"__property__", _parts} = property]} ->
"@" <> unwrap_property(property)
{value, _ast} ->
value
|> default_value(context)
|> stringify()
end)
end
defp unwrap_property({"__property__", parts}),
do: Enum.map_join(parts, ".", &unwrap_property/1)
defp unwrap_property(parts) when is_list(parts),
do: Enum.map_join(parts, ".", &unwrap_property/1)
defp unwrap_property(part), do: part
@doc """
Return the default value for a potentially complex value.
Complex values can be Maps that have a `__value__` key, if that's
returned then we can to use the `__value__` value when eval'ing against
operators or functions.
"""
@spec default_value(term) :: term
def default_value(val, context \\ nil)
def default_value(%{"__value__" => default_value}, _context), do: default_value
def default_value(value, _context), do: value
@spec stringify(term) :: String.t()
def stringify(items) when is_list(items), do: Enum.map_join(items, "", &stringify/1)
def stringify(binary) when is_binary(binary), do: binary
def stringify(%DateTime{} = date), do: DateTime.to_iso8601(date)
def stringify(%Date{} = date), do: Date.to_iso8601(date)
def stringify(map) when is_map(map), do: "#{inspect(map)}"
def stringify(other), do: to_string(other)
@spec compile(expression :: String.t()) :: [term]
def compile(expression) when is_binary(expression) do
with {:ok, parts} <- parse(expression),
parts <- Enum.map([parts], &compile_block/1) do
hd(parts)
end
end
def compile_block({function_name, arguments})
when is_binary(function_name) and is_list(arguments) do
[{function_name, arguments}]
|> Compile.compile()
|> compile_block()
end
def compile_block(final), do: final
@doc """
Return the code generated for the Abstract Syntax tree or
Expression string provided.
"""
@spec debug(String.t() | [term]) :: String.t()
def debug(expression) when is_binary(expression) do
with {:ok, ast, "", _, _, _} <- Parser.expression(expression) do
debug(ast)
end
end
def debug(ast) do
ast
|> Compile.to_quoted()
|> Compile.wrap_in_context()
|> Macro.to_string()
end
end