RulEx (RulEx v1.0.0) View Source

RulEx is a rules engine and simplified expressions language for evaluating a set of conditions against a set of facts, yielding true or false on whether the provided facts adhere to the set of conditions given.

RulEx expressions

The expressions used by RulEx are inspired by Lisp, and are simply nested lists with the first element being an operand, think of it as a function, and the remaining elements arguments to this operand. Evaluating these expressions is done against a set of facts provided as a RulEx.DataBag, and the outcome is a boolean on whether or not the conditions match on the given facts.

Supported operands

Logical operands

These operands can only be used in RulEx.Behaviour.eval/2 callback, and only yield true or false results. They can be passed "facts" from outside the expressions using RulEx.DataBag and the value operands supported by RulEx.

  • The any operand :|, "|", which matches any list of RulEx expressions and yields true if one of them yields true, false otherwise.
  • The all operand :&, "&", which matches any list of RulEx expressions and yields true if non of them yields false, true otherwise.
  • The negation operand :!, which matches a single RulEx expressions and yields the negation of whatever the input expression yields, e.g. true for false, and false for true.
  • The equality operand :=, "=", which matches a two RulEx value expressions and yields true if they are equal, this operation is non strict, i.e. 1.0 = 0 is true.
  • The inequality operand :!=, "!=", which matches a two RulEx value expressions and yields true if they are not equal, this operation is non strict, i.e. 1.0 != 0 is false.
  • The less than operand :<, "<", which matches a two RulEx value expressions and yields true if the one on the left hand side is less than the one on the right hand side.
  • The greater than operand :>, ">", which matches a two RulEx value expressions and yields true if the one on the left hand side is greater than the one on the right hand side.
  • The less than or equals operand :<=, "<=", which matches a two RulEx value expressions and yields true if the one on the left hand side is less than or equals the one on the right hand side.
  • The greater than or equals operand :>=, ">=", which matches a two RulEx value expressions and yields true if the one on the left hand side is greater than or equals the one on the right hand side.
  • The contains operand :in, "in", which matches a "needle", a single RulEx value expressions, and a "haystack", a list of normal values (not RulEx expressions), and yields true if the needle exists in the given haystack.

Value operands

Value operands are used to represent data in a RulEx expressions, they are only two operands for this use case :val ("val") and :var ("var").

val operand can be used in order to store exact values in the expressions, this includes the conditions you want to mean when applying the logical operands.

var operand can be used in order to pass facts from outside of the expressions, this is done by using any Elixir term that implements the RulEx.DataBag protocol.

Both these operands accept 2 arguments, a data type, and a value, the value in a val expressions is the actual value to be yielded back, while in a var expressions is they key used with RulEx.DataBag. The values are validated against the data type argument given, and would fail/be rejected if they don't match properly, i.e. you cannot return the value "string" when the data type expressed is "numeric".

The supported data types are,

  1. "any", which will yield back any value without any validation.
  2. "number", which will yield back any numeric value, regardless if an integer or float.
  3. "integer", which will yield back only integer values.
  4. "float", which will yield back only float values.
  5. "string", which will yield back only strings.
  6. "boolean", which will yield back only boolean values, i.e. true and false.
  7. "list", which will yield back only lists of values, no validation is required on the values within the list.
  8. "map", which will yield back only map of arbitrary keys and values, no validation is required on either the keys or the values within the map.
  9. "time", which will yield back time values, it can be given string values and it will parse them, so long as those values are times as defined by ISO 8601.
  10. "date", which will yield back date values, it can be given string values and it will parse them, so long as those values are dates as defined by ISO 8601.
  11. "datetime", which will yield back date time values (naive datetime in Elixir terminology), it can be given string values and it will parse them, so long as those values are datetimes as defined by ISO 8601.

Currently RulEx does not support any other additional types, and all of it's operations regarding values are strictly typed, meaning it will reject to complete operations if types don't match.

Reserved operands

RulEx reserves all the previously defined expressions as defined in Logical operands and in Value operands, in both their string and atom formats. RulEx also disallows for any non-reserved operand that isn't a string. RulEx does however allow for adding custom operands as defined in Custom operand.

Custom operands

RulEx behaviour can be extended with any arbitrary custom operands by simply defining the RulEx.Behaviour.opernad/3 callback, this will receive the operand string, a list of arguments given to the operand, and the current data bag holding the facts being processed.

Example expressions

You can view the test suite for example expressions of a variety of types and forms, in short an expression is simply a list of arbitrarily nested RulEx valid values, this includes other RulEx expressions and normal Elixir terms, e.g. [:=, [:val, "any", "hello"], [:val, "any, "world"]] is a valid expression.

Think of RulEx expressions very similarly to how Lisp syntax works, these can be built and manipulated by code easily due to their easy to use structure. Some examples are given below, for more varied examples please check the test suite!

[ :!
, [ :=
  , [ :val
    , "string"
    , "some value"
    ]
  , [ :val
    , "string"
    , "some other value"
    ]
  ]
]

[ :|
, [ :=
  , [ :val
    , "string"
    , "some value"
    ]
  , [ :val
    , "string"
    , "some other value"
    ]
  ]
, [ :=
  , [ :val
    , "numeric"
    , 10
    ]
  , [ :val
    , "numeric"
    , 10.0
    ]
  ]
]

Storing and transporting these expressions

RulEx provides the RulEx.EncodeError behaviour used to define converting RulEx expressions from and to Elixir terms, this can be helpful when you need to store these expressions and/or to transfer these expressions over the wire. By default, a JSON encoding is implemented for you.

Usage

Simply use RulEx as is, as it implements the RulEx.Behaviour fully. However, if you wan to add custom operands to the set of supported rules, simply define the RulEx.Behaviour as is, and then run wild. To do so simply use RulEx.Behaviour in your module, then implement your custom operands by overriding the RulEx.Behaviour.operand/3 callback.

Caveats and quirks

  • If no arguments are given to the any (:|) operand, it will yield back false, follow this issue in Elixir for a discussion around this behaviour.
  • If no arguments are given to the all (:&) operand, it will yield back true.
  • Value expressions can be used in the RulEx.Behaviour.eval/2 callback, the results will simply be converted based on their truthiness as defined by Elixir and Erlang (i.e. only false and nil are falsy values).
  • Logical expressions will always yield an error if passed to the RulEx.Behaviour.value/2 callbacks.
  • Results coming back from custom RulEx.Behaviour.operand/3 are treated like value expressions and are converted to booleans based on their truthiness.
  • If given a reserved expression but as a string instead of an atom, RulEx will convert it to an atom and use it as if it was passed as the atom for the reserved expression.
  • The comparison operands, <, >, <=, >=, =, and != all validate that both arguments are val or var expressions of the same type before doing anything, and will yield an error otherwise.

Link to this section Summary

Types

The set of all valid values that represent an "argument" in a RulEx expression.

The set of all supported operands by RulEx, these include a set of reserved operands, as well as any arbitrary strings that can be used to extend RulEx behaviour to match any custom domain that needs rules evaluation.

t()

A RulEx expression is a list of operands and arguments, ideally these expressions must start with one operand and then be followed by any arbitrarily long set of arguments, the arguments can also be other RulEx expressions.

Functions

Default implementation for RulEx.Encoding.decode/1.

Default implementation for RulEx.Encoding.decode!/1.

Default implementation for RulEx.Encoding.encode/1.

Default implementation for RulEx.Encoding.encode!/1.

Default implementation for RulEx.Behaviour.eval/2.

Default implementation for RulEx.Behaviour.eval!/2.

Default implementation for RulEx.Behaviour.expr?/1.

Default implementation for RulEx.Behaviour.operand/3. If not overridden this will always yield an error. No catchall clause is needed as it is already implemented.

Default implementation for RulEx.Behaviour.value/2.

Default implementation for RulEx.Behaviour.value!/2.

Link to this section Types

Specs

arg() ::
  String.t()
  | number()
  | DateTime.t()
  | NaiveDateTime.t()
  | Date.t()
  | [arg()]
  | any()

The set of all valid values that represent an "argument" in a RulEx expression.

Specs

op() ::
  :| | :& | :! | := | :!= | :< | :> | :<= | :>= | :in | :val | :var | String.t()

The set of all supported operands by RulEx, these include a set of reserved operands, as well as any arbitrary strings that can be used to extend RulEx behaviour to match any custom domain that needs rules evaluation.

Specs

t() :: [op() | arg() | t()]

A RulEx expression is a list of operands and arguments, ideally these expressions must start with one operand and then be followed by any arbitrarily long set of arguments, the arguments can also be other RulEx expressions.

In order to validate whether a given RulEx expression is valid or not you can use the callback RulEx.Behaviour.expr?/1. Operands and expressions can further be validated with the guards defined in the RulEx.Guards module.

Link to this section Functions

Link to this function

decode(maybe_encoded_expr)

View Source

Default implementation for RulEx.Encoding.decode/1.

Further more, this will return an error if the provided expression is not a valid RulEx expression.

Link to this function

decode!(maybe_encoded_expr)

View Source

Default implementation for RulEx.Encoding.decode!/1.

Further more, this will return an error if the provided expression is not a valid RulEx expression.

Default implementation for RulEx.Encoding.encode/1.

Further more, this will return an error if the provided expression is not a valid RulEx expression.

Default implementation for RulEx.Encoding.encode!/1.

Further more, this will return an error if the provided expression is not a valid RulEx expression.

Default implementation for RulEx.Behaviour.eval/2.

Examples

iex> import Elixir.RulEx
iex> # Success cases
iex> truthy_expression = [:=, [:val, "string", "hello"], [:var, "string", "what?"]]
iex> falsy_expression = [:=, [:val, "string", "hello"], [:val, "string", "world"]]
iex> {:ok, true} = eval(truthy_expression, %{"what?" => "hello"})
iex> {:ok, false} = eval(falsy_expression, %{"what?" => "hello"})
iex> invalid_expression = []
iex> {:error, _reason} = eval(invalid_expression, %{})

Default implementation for RulEx.Behaviour.eval!/2.

Default implementation for RulEx.Behaviour.expr?/1.

Examples

iex> import Elixir.RulEx
iex> correct_expression = [:=, [:val, "string", "hello"], [:var, "string", "what?"]]
iex> incorrect_expression = []
iex> true = expr?(correct_expression)
iex> false = expr?(incorrect_expression)

Default implementation for RulEx.Behaviour.operand/3. If not overridden this will always yield an error. No catchall clause is needed as it is already implemented.

Examples

iex> import Elixir.RulEx
iex> {:error, _reason} = operand("whatever", "any value", %{})

Default implementation for RulEx.Behaviour.value/2.

Examples

iex> import Elixir.RulEx
iex> # Success cases
iex> val_expression = [:val, "string", "foo"]
iex> var_expression = [:var, "number", "x"]
iex> {:ok, "foo"} = value(val_expression, %{})
iex> {:ok, 10} = value(var_expression, %{"x" => 10})
iex> # Error cases
iex> {:error, _reason} = value(var_expression, %{})
iex> {:error, _reason} = value([], %{})

Default implementation for RulEx.Behaviour.value!/2.