defmodule RulEx.Encoding do @moduledoc """ This behaviour defines how to translate RulEx expressions into any encoding formats your application may want/need, e.g. converting them into JSON values. This is useful when you want to store RulEx expressions into database and/or if you want to transfer the rules over the wire to other services/systems that may need it. ## Usage A custom RulEx encoding mechanism can be defined by simply using `RulEx.Encoding`, and passing a module defining your encode and decode functions defmodule MyApp.RulEx.Encoder do use RulEx.Encoding, encoder: CustomModule end Alternatively the internal encoder can be passed as an option when using `RulEx.Encoding` defmodule MyApp.RulEx.Encoder do use RulEx.Encoding def __encoder__, do: CustomModule end Using this module to build your encoder is useful as the generated encoder would do validation on the RulEx expressions being encoded or decoded by it. """ @doc """ Given a RulEx expression, encode it into any parsable value. This function will yield an error if given an invalid value in place of the RulEx expression, or if it fails to encode the provided expression. ## Examples iex> # Assuming the use of a JSON encoder iex> expression = [:|,[:>, [:val, "number", 10], [:var, "number", "x"]],[:=, [:val, "any", 10], [:var, "any", "x"]]] iex> encoded = "[\\"|\\",[\\">\\",[\\"val\\",\\"number\\",10],[\\"var\\",\\"number\\",\\"x\\"]],[\\"=\\",[\\"val\\",\\"any\\",10],[\\"var\\",\\"any\\",\\"x\\"]]]" iex> {:ok, ^encoded} = encode(expression) iex> {:error, _reason} = encode([]) """ @callback encode(RulEx.t()) :: {:ok, any} | {:error, term} @doc "Exactly identical to `RulEx.Encoding.encode/1` but raises `RulEx.EncodeError` in case of errors." @callback encode!(RulEx.t()) :: any | no_return @doc """ Given an encoded RulEx expression, decode it back into a RulEx expression. This function will yield an error if decoded value is an invalid RulEx expression, or if it fails to decode the provided value. ## Examples iex> # Assuming the use of a JSON encoder iex> encoded = "[\\"|\\",[\\">\\",[\\"val\\",\\"number\\",10],[\\"var\\",\\"number\\",\\"x\\"]],[\\"=\\",[\\"val\\",\\"any\\",10],[\\"var\\",\\"any\\",\\"x\\"]]]" iex> expression = [:|,[:>, [:val, "number", 10], [:var, "number", "x"]],[:=, [:val, "any", 10], [:var, "any", "x"]]] iex> {:ok, ^expression} = decode(encoded) iex> {:error, _reason} = decode("[]") """ @callback decode(any) :: {:ok, RulEx.t()} | {:error, term} @doc "Exactly identical to `RulEx.Encoding.decode/1` but raises `RulEx.DecodeError` in case of errors." @callback decode!(any) :: RulEx.t() | no_return defmacro __using__(opts) do encoder = Keyword.get(opts, :encoder, RulEx.Encoding.Json) quote do alias RulEx.{EncodeError, DecodeError} require RulEx.Guards @behaviour RulEx.Encoding @doc """ Default implementation for `RulEx.Encoding.encode/1`. Further more, this will return an error if the provided expression is not a valid RulEx expression. """ @impl RulEx.Encoding def encode(maybe_expr) do with true <- RulEx.Builder.expr?(maybe_expr) do __encoder__().encode(maybe_expr) else false -> {:error, "cannot encode invalid expression"} end end @doc """ Default implementation for `RulEx.Encoding.encode!/1`. Further more, this will return an error if the provided expression is not a valid RulEx expression. """ @impl RulEx.Encoding def encode!(maybe_expr) do case encode(maybe_expr) do {:ok, encoder} -> encoder {:error, reason} -> raise EncodeError, message: reason, given: maybe_expr end end @doc """ Default implementation for `RulEx.Encoding.decode/1`. Further more, this will return an error if the provided expression is not a valid RulEx expression. """ @impl RulEx.Encoding def decode(maybe_encoded_expr) do with {:ok, maybe_expr} <- __encoder__().decode(maybe_encoded_expr), true <- RulEx.Builder.expr?(maybe_expr) do {:ok, atomize_reserved_operands(maybe_expr)} else false -> {:error, "decoded an invalid expression"} reason -> reason end end @doc """ Default implementation for `RulEx.Encoding.decode!/1`. Further more, this will return an error if the provided expression is not a valid RulEx expression. """ @impl RulEx.Encoding def decode!(maybe_encoded_expr) do case decode(maybe_encoded_expr) do {:ok, encoder} -> encoder {:error, reason} -> raise DecodeError, message: reason, raw: maybe_encoded_expr, decoder: __encoder__() end end @spec __encoder__ :: module def __encoder__, do: unquote(encoder) defoverridable __encoder__: 0 # # Private APIs # defp atomize_reserved_operands([op | exprs] = expr) when RulEx.Guards.is_val_or_var(expr) do op = RulEx.Operands.rename(op) [op | exprs] end defp atomize_reserved_operands([op | exprs]) do op = RulEx.Operands.rename(op) exprs = Enum.map(exprs, &atomize_reserved_operands/1) [op | exprs] end end end end defmodule RulEx.Encoding.Json do use RulEx.Encoding def __encoder__, do: Application.get_env(:rul_ex, __MODULE__, encoder: Jason)[:encoder] end