Skip to content

About

Predicator in elixir

Resources

Code of conduct

Contributing

Stars

3 stars

Watchers

2 watching

Forks

Repository files navigation

Predicator

CI Hex.pm Version Hex Downloads Hex Docs codecov License

A small, safe predicate language for Elixir: parse a rule a person wrote, such as renewals < 3 AND NOT on_hold, and evaluate it against a context of data. A rule compiles to a flat list of instructions that a small stack machine runs, so the text never becomes code. Short programs - assignments, if/else and while - run the same way and write their results into the context.

Why a predicate language of its own

Without one, an application that lets its users or its configuration decide when something may happen has two poor choices. It can write every condition in code, so each new rule waits for a release; or it can evaluate the text a person typed, so a typo or a hostile string runs inside the application. With Predicator the rule is data: it is parsed by a grammar of comparisons, logic, arithmetic, membership, dates, lists and function calls, compiled to instructions that are plain terms you can store, and evaluated with no eval anywhere in the path. A rule that names a value the data does not carry comes back as an error value rather than a crash, and the same compiled rule runs as often as you like.

Installation

Add predicator to your list of dependencies in mix.exs:

def deps do
  [
    {:predicator, "~> 9.4"}
  ]
end

Basic usage

A library loan: a copy of a book lent to a patron, due on a date, renewed, returned or lost. The library lets staff write the renewal rule as text. Compile it once, evaluate it against each loan, and read a missing value as an error rather than a crash; a short program records the renewal itself.

iex> {:ok, may_renew} = Predicator.compile("status == 'on_loan' AND renewals < 3 AND NOT on_hold")
iex> Predicator.evaluate(may_renew, %{"status" => "on_loan", "renewals" => 1, "on_hold" => false})
{:ok, true}
iex> Predicator.evaluate(may_renew, %{"status" => "on_loan", "renewals" => 3, "on_hold" => false})
{:ok, false}
iex> {:error, error} = Predicator.evaluate(may_renew, %{"status" => "on_loan"})
iex> error.variable
"renewals"
iex> {:ok, context} = Predicator.execute("if renewals < 3 { renewals = renewals + 1; due_in_days = 21 }", %{"renewals" => 1})
iex> context.data
%{"due_in_days" => 21, "renewals" => 2}

may_renew is a plain list of instructions, so it can sit in ETS, in a process's state or in a database column until the next loan comes back.

Documentation

Compatibility

Predicator needs Elixir 1.18 or later (elixir: "~> 1.18" in mix.exs) and has no runtime dependencies.

A compiled rule carries the version of the instruction set it was compiled for, which moves separately from the package version. Check that version before evaluating a rule stored under an earlier release, and the rule either still runs or is refused with a message naming why, never silently mis-run; Embed compiled programs shows the check. Every change a caller can observe is recorded in the changelog.

Contributing

docs/contributing.md has the quality-check commands and the checklists for adding operators and data types.

License

MIT - see LICENSE.

About

Predicator in elixir

Resources

Code of conduct

Contributing

Stars

3 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages