Spek - Boolean expression engine for domain rules

I just released Spek, a boolean expression engine based on the one written for LetMe 2.0.0.

Features

  • Expression structs and builder functions for boolean logic: AllOf, AnyOf, Not, Literal, Check.
  • Evaluation of boolean expressions with optional early stopping and optional evaluation tree output.
  • Optimization of boolean expressions using boolean algebra transformation.
  • Macros for concisely defining reusable check functions.

The readme is quite comprehensive, so let me just post some code examples here.

Defining a check module

defmodule DeviceChecks do
	import Spek.Macros

	defcheck device_online(device, reason: :device_offline) do
		device.online?
	end

	defcheck battery_above_20(device, reason: :battery_too_low) do
		device.battery_level > 20
	end

	defcheck charging(device) do
		device.charging?
	end

	defcheck low_power_mode_enabled(device) do
		device.low_power_mode?
	end
end

(The use of the defcheck macro is optional.)

Composing, optimizing, and evaluating expressions

iex> import Spek
iex> battery_safe =
...>   all_of([
...>     DeviceChecks.device_online_check(),
...>     DeviceChecks.battery_above_20_check()
...>   ])
iex> charging_safe =
...>   all_of([
...>     DeviceChecks.device_online_check(),
...>     DeviceChecks.charging_check()
...>   ])
iex> rule =
...>   any_of([
...>     battery_safe,
...>     charging_safe,
...>     DeviceChecks.low_power_mode_enabled_check()
...>   ])
%Spek.AnyOf{
	children: [
		%Spek.AllOf{
			children: [
				%Spek.Check{
					module: DeviceChecks,
					fun: :device_online,
					args: [:ctx]
				},
				%Spek.Check{
					module: DeviceChecks,
					fun: :battery_above_20,
					args: [:ctx]
				}
			]
		},
		%Spek.AllOf{
			children: [
				%Spek.Check{
					module: DeviceChecks,
					fun: :device_online,
					args: [:ctx]
				},
				%Spek.Check{
					module: DeviceChecks,
					fun: :charging,
					args: [:ctx]
				}
			]
		},
		%Spek.Check{
			module: DeviceChecks,
			fun: :low_power_mode_enabled,
			args: [:ctx]
		}
	]
}
iex> rule = optimize(rule)
%Spek.AnyOf{
	children: [
		%Spek.AllOf{
			children: [
				%Spek.Check{
					module: DeviceChecks,
					fun: :device_online,
					args: [:ctx]
				},
				%Spek.AnyOf{
					children: [
						%Spek.Check{
							module: DeviceChecks,
							fun: :battery_above_20,
							args: [:ctx]
						},
						%Spek.Check{
							module: DeviceChecks,
							fun: :charging,
							args: [:ctx]
						}
					]
				}
			]
		},
		%Spek.Check{
			module: DeviceChecks,
			fun: :low_power_mode_enabled,
			args: [:ctx]
		}
	]
}
iex> device = %{
...>   online?: true,
...>   battery_level: 12,
...>   charging?: false,
...>   low_power_mode?: false
...> }
iex> Spek.eval?(rule, device)
false
iex> device = %{
...>   online?: false,
...>   battery_level: 25,
...>   charging?: false,
...>   low_power_mode?: false
...> }
iex> Spek.eval_tree(rule, device)
{
	:error,
	%Spek.EvaluationError{
		expression: %Spek.AnyOf{
			children: [
				%Spek.AllOf{
					satisfied?: false,
					children: [
						%Spek.Check{
							module: DeviceChecks,
							fun: :device_online,
							args: [:ctx],
							result: {:error, :device_offline},
							satisfied?: false
						}
					]
				},
				%Spek.Check{
					module: DeviceChecks,
					fun: :low_power_mode_enabled,
					args: [:ctx],
					result: {:error, :failed},
					satisfied?: false
				}
			],
			satisfied?: false
		},
		message: "rule evaluation failed"
	}
}

Feedback

Your feedback is appreciated, especially when it comes to API design or feature ideas.

What may come in a future release:

  • Serialization/deserialization of expressions
  • Extract errors of an evaluated expression into a list of error reasons
6 Likes

Hi, congratulations on the release :tada:. I have a question: when and what to use this library for?

3 Likes

Use cases can be:

  • Complex domain rules with composable conditions
  • Specification pattern implementations
  • Workflow, pipeline, and feature gating conditions
  • User-configurable decision systems
  • Auditable decision logs with per-check results and success/failure reasons

Version 0.2.0 adds various functions to collect evaluation results into flat lists.