Spek is a boolean expression engine for Elixir.

It allows you to model, optimize, and evaluate rules using composable expressions.

This module contains all expression builder, evaluation, and optimization functions. Please refer to the readme for details.

Example

Given this 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

We can compose, optimize, and evaluate rules like this:

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"
  }
}

Summary

Builder Functions

Builds an expression that requires all children to be true.

Builds an expression that requires both children to be true.

Builds an expression that requires at least one child to be true.

Builds an expression that requires at least one of two children to be true.

Builds an expression that is always false.

Builds an expression that always evaluates to the same value.

Builds an expression that evaluates to true unless both children are true.

Negates the given expression.

Builds an expression that requires all of its children to be false.

Builds an expression that evaluates to true if both children are false.

Builds an expression that is always true.

Builds the exclusive or of the given expressions.

Evaluation Functions

Collects all tagged results from an expression into a list.

Collects the success or error results of an expression into a list.

Evaluates the given expression and returns :ok or an error tuple.

Evaluates the given expression and raises an exception if it is not satisfied.

Evaluates the given expression and returns the result as a boolean.

Evaluates the given expression with eval_tree/2 and collects the results into a list with collect_results/2.

Evaluates the given expression with eval_tree/2 and collects the results into a list with collect_results/2.

Evaluates the given expression with eval_tree_all/2 and collects the results into a list with collect_results/2.

Evaluates the given expression with eval_tree_all/2 and collects the results into a list with collect_results/2.

Evaluates the given expression and returns the expression annotated with evaluation results.

Evaluates the given expression and returns the expression annotated with evaluation results or raises an error.

Evaluates the full given expression and returns the expression annotated with the expression results.

Evaluates the full given expression and returns the expression annotated with evaluation results or raises an error.

Filters the given enumerable to only retain the items that satisfy the given expression.

Filters the given enumerable to only retain the items that do not satisfy the given expression.

Converts a check result to a boolean.

Optimization Functions

Performs boolean algebra transformations on the given expression.

Types

The evaluation context as passed to the evaluation functions.

A boolean expression tree.

An expression result value that results in a failed evaluation.

The result of an expression evaluation.

An expression result value that results in a successful evaluation.

Builder Functions

all_of(children)

(since 0.1.0)
@spec all_of([expression()]) :: Spek.AllOf.t()

Builds an expression that requires all children to be true.

Example

iex> all_of([
...>   check(MyModule, :session_active, []),
...>   check(MyModule, :permissions_valid, [])
...> ])
%Spek.AllOf{
  children: [
    %Spek.Check{module: MyModule, fun: :session_active, args: []},
    %Spek.Check{module: MyModule, fun: :permissions_valid, args: []}
  ]
}

all_of(a, b)

(since 0.1.0)
@spec all_of(expression(), expression()) :: Spek.AllOf.t()

Builds an expression that requires both children to be true.

Example

iex> all_of(
...>   check(RenderingChecks, :color_profile_valid, []),
...>   check(RenderingChecks, :frame_rate_supported, [])
...> )
%Spek.AllOf{
  children: [
    %Spek.Check{module: RenderingChecks, fun: :color_profile_valid, args: []},
    %Spek.Check{module: RenderingChecks, fun: :frame_rate_supported, args: []}
  ]
}

any_of(children)

(since 0.1.0)
@spec any_of([expression()]) :: Spek.AnyOf.t()

Builds an expression that requires at least one child to be true.

Example

iex> any_of([
...>   check(AudioPipelineChecks, :waveform_detected, []),
...>   check(AudioPipelineChecks, :silence_threshold_exceeded, [])
...> ])
%Spek.AnyOf{
  children: [
    %Spek.Check{module: AudioPipelineChecks, fun: :waveform_detected, args: []},
    %Spek.Check{module: AudioPipelineChecks, fun: :silence_threshold_exceeded, args: []}
  ]
}

any_of(a, b)

(since 0.1.0)
@spec any_of(expression(), expression()) :: Spek.AnyOf.t()

Builds an expression that requires at least one of two children to be true.

Example

iex> any_of(
...>   check(SubtitleChecks, :burn_in_detected, []),
...>   check(SubtitleChecks, :timecode_aligned, [])
...> )
%Spek.AnyOf{
  children: [
    %Spek.Check{module: SubtitleChecks, fun: :burn_in_detected, args: []},
    %Spek.Check{module: SubtitleChecks, fun: :timecode_aligned, args: []}
  ]
}

check(module, fun, args \\ [:ctx])

(since 0.1.0)
@spec check(module(), fun(), Spek.Check.args()) :: Spek.Check.t()

Builds a check.

Example

iex> check(WeatherChecks, :temperature_below_freezing, [0])
%Spek.Check{module: WeatherChecks, fun: :temperature_below_freezing, args: [0]}

fail(result \\ false)

(since 0.1.0)
@spec fail(falsy()) :: Spek.Literal.t()

Builds an expression that is always false.

Raises an ArgumentError if the result is not false, :error, or an error tuple. Use pass/1 for a truthy result, or literal/1 to derive the outcome from the result.

Example

iex> fail()
%Spek.Literal{result: false, satisfied?: false}

iex> fail(:error)
%Spek.Literal{result: :error, satisfied?: false}

iex> fail({:error, :insufficient_lighting})
%Spek.Literal{result: {:error, :insufficient_lighting}, satisfied?: false}

literal(result)

(since 0.1.0)
@spec literal(result()) :: Spek.Literal.t()

Builds an expression that always evaluates to the same value.

Examples

iex> literal(true)
%Spek.Literal{result: true, satisfied?: true}

iex> literal(:ok)
%Spek.Literal{result: :ok, satisfied?: true}

iex> literal({:ok, "render_queue_ready"})
%Spek.Literal{result: {:ok, "render_queue_ready"}, satisfied?: true}

iex> literal(false)
%Spek.Literal{result: false, satisfied?: false}

iex> literal(:error)
%Spek.Literal{result: :error, satisfied?: false}

iex> literal({:error, :codec_not_supported})
%Spek.Literal{result: {:error, :codec_not_supported}, satisfied?: false}

nand(a, b)

(since 0.1.0)
@spec nand(expression(), expression()) :: expression()

Builds an expression that evaluates to true unless both children are true.

Example

iex> nand(check(RenderChecks, :gpu_available, []), check(RenderChecks, :texture_cache_warm, []))
%Spek.Not{
  expression: %Spek.AllOf{
    children: [
      %Spek.Check{module: RenderChecks, fun: :gpu_available, args: []},
      %Spek.Check{module: RenderChecks, fun: :texture_cache_warm, args: []}
    ]
  }
}

negate(expression)

(since 0.1.0)
@spec negate(expression()) :: Spek.Not.t()

Negates the given expression.

Examples

iex> negate(literal(true))
%Spek.Not{expression: %Spek.Literal{result: true, satisfied?: true}}

iex> negate(check(EncodingChecks, :keyframe_aligned, []))
%Spek.Not{
  expression: %Spek.Check{module: EncodingChecks, fun: :keyframe_aligned, args: []}
}

none(children)

(since 0.1.0)
@spec none([expression()]) :: expression()

Builds an expression that requires all of its children to be false.

Example

iex> none([check(AudioChecks, :noise_floor_exceeded, []), check(AudioChecks, :clipping_detected, [])])
%Spek.Not{
  expression: %Spek.AnyOf{
    children: [
      %Spek.Check{module: AudioChecks, fun: :noise_floor_exceeded, args: []},
      %Spek.Check{module: AudioChecks, fun: :clipping_detected, args: []}
    ]
  }
}

nor(a, b)

(since 0.1.0)
@spec nor(expression(), expression()) :: expression()

Builds an expression that evaluates to true if both children are false.

Example

iex> nor(check(VideoChecks, :frame_dropped, []), check(VideoChecks, :desync_detected, []))
%Spek.Not{
  expression: %Spek.AnyOf{
    children: [
      %Spek.Check{module: VideoChecks, fun: :frame_dropped, args: []},
      %Spek.Check{module: VideoChecks, fun: :desync_detected, args: []}
    ]
  }
}

pass(result \\ true)

(since 0.1.0)
@spec pass(truthy()) :: Spek.Literal.t()

Builds an expression that is always true.

Raises an ArgumentError if the result is not true, :ok, or an ok tuple. Use fail/1 for a falsy result, or literal/1 to derive the outcome from the result.

Example

iex> pass()
%Spek.Literal{result: true, satisfied?: true}

iex> pass(:ok)
%Spek.Literal{result: :ok, satisfied?: true}

iex> pass({:ok, "proxy_stream_ready"})
%Spek.Literal{result: {:ok, "proxy_stream_ready"}, satisfied?: true}

xor(a, b)

(since 0.1.0)
@spec xor(expression(), expression()) :: expression()

Builds the exclusive or of the given expressions.

Example

iex> xor(check(PipelineChecks, :transcode_complete, []), check(PipelineChecks, :thumbnail_generated, []))
%Spek.AnyOf{
  children: [
    %Spek.AllOf{
      children: [
        %Spek.Check{module: PipelineChecks, fun: :transcode_complete, args: []},
        %Spek.Not{
          expression: %Spek.Check{
            module: PipelineChecks,
            fun: :thumbnail_generated,
            args: []
          }
        }
      ]
    },
    %Spek.AllOf{
      children: [
        %Spek.Not{
          expression: %Spek.Check{
            module: PipelineChecks,
            fun: :transcode_complete,
            args: []
          }
        },
        %Spek.Check{module: PipelineChecks, fun: :thumbnail_generated, args: []}
      ]
    }
  ]
}

Evaluation Functions

collect_results(expression)

(since 0.2.0)
@spec collect_results(expression()) :: [term()]

Collects all tagged results from an expression into a list.

Results are returned without their :ok / :error tags, in evaluation order, and are deduplicated.

Examples

Literals and checks contribute only tagged tuple payloads. The plain result values :ok, :error, true and false are not returned.

iex> Spek.collect_results(
...>   %Literal{result: {:ok, "good"}, satisfied?: true}
...> )
["good"]

iex> Spek.collect_results(
...>   %Literal{result: true, satisfied?: true}
...> )
[]

iex> Spek.collect_results(
...>   %Check{
...>     module: Checks,
...>     fun: :some_check,
...>     args: [],
...>     result: {:error, "bad"},
...>     satisfied?: false
...>   }
...> )
["bad"]

The results of nested expressions are flattened.

iex> expression =
...>   %AllOf{
...>     children: [
...>       %Literal{result: {:ok, "a"}, satisfied?: true},
...>       %Literal{result: {:error, "b"}, satisfied?: false},
...>       %Check{
...>         module: Checks,
...>         fun: :some_check,
...>         args: [],
...>         result: {:ok, "c"},
...>         satisfied?: true
...>       }
...>     ]
...>   }
iex> Spek.collect_results(expression)
["a", "b", "c"]

Tagged results within Not expressions are collected as well.

iex> Spek.collect_results(
...>   %Not{
...>     satisfied?: true,
...>     expression: %Literal{result: {:error, "bad"}, satisfied?: false}
...>   }
...> )
["bad"]

collect_results(expression, only)

(since 0.2.0)
@spec collect_results(expression(), :ok | :error) :: [term()]

Collects the success or error results of an expression into a list.

Results are returned in evaluation order and are deduplicated.

Examples

The returned list only contains results using :ok or :error tuples.

iex> Spek.collect_results(
...>   %Literal{result: {:ok, "good"}, satisfied?: true},
...>   :ok
...> )
["good"]

iex> Spek.collect_results(
...>   %Check{
...>     module: Checks,
...>     fun: :some_check,
...>     args: [],
...>     result: {:error, "bad"},
...>     satisfied?: false
...>   },
...>   :error
...> )
["bad"]

The plain result values :ok, :error, true and false are not returned.

iex> Spek.collect_results(
...>   %Check{
...>     module: Checks,
...>     fun: :some_check,
...>     args: [],
...>     result: false,
...>     satisfied?: false
...>   },
...>   :error
...> )
[]

The results of nested expressions are flattened.

iex> expression =
...>   %AllOf{
...>     children: [
...>       %Literal{result: {:ok, "a"}, satisfied?: true},
...>       %Literal{result: {:error, "b"}, satisfied?: false},
...>       %Check{
...>         module: Checks,
...>         fun: :some_check,
...>         args: [],
...>         result: {:ok, "c"},
...>         satisfied?: true
...>       }
...>     ]
...>   }
iex> Spek.collect_results(expression, :ok)
["a", "c"]
iex> Spek.collect_results(expression, :error)
["b"]

Not reverses which tagged results are returned:

iex> expression =
...>   %Not{
...>     satisfied?: true,
...>     expression: %Literal{
...>       result: {:error, "bad"},
...>       satisfied?: false
...>     }
...>   }
iex> Spek.collect_results(expression, :ok)
["bad"]
iex> Spek.collect_results(expression, :error)
[]

eval(expression, context \\ [])

(since 0.1.0)
@spec eval(expression(), context()) :: :ok | {:error, Spek.EvaluationError.t()}

Evaluates the given expression and returns :ok or an error tuple.

Stops early as soon as the final outcome is determined.

Examples

iex> eval(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "scene_042_render_complete"
...> )
:ok

iex> eval(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "shot_042_render_complete"
...> )
{:error, %Spek.EvaluationError{message: "rule evaluation failed"}}

eval!(expression, context \\ [])

(since 0.1.0)
@spec eval!(expression(), context()) :: :ok | no_return()

Evaluates the given expression and raises an exception if it is not satisfied.

Stops early as soon as the final outcome is determined.

Examples

iex> eval!(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "scene_042_render_complete"
...> )
:ok

iex> eval!(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "shot_042_render_complete"
...> )
** (Spek.EvaluationError) rule evaluation failed

eval?(expr, context \\ [])

(since 0.1.0)
@spec eval?(expression(), context()) :: boolean()

Evaluates the given expression and returns the result as a boolean.

Stops early as soon as the final outcome is determined.

Examples

iex> eval?(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "scene_042_render_complete"
...> )
true

iex> eval?(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "shot_042_render_complete"
...> )
false

eval_collect(expression, context \\ [])

(since 0.2.0)
@spec eval_collect(expression(), context()) ::
  {:ok, [term()]} | {:error, Spek.EvaluationError.t()}

Evaluates the given expression with eval_tree/2 and collects the results into a list with collect_results/2.

In the success case, an :ok tuple with the list of success results is returned.

In the error case, an :error tuple with a Spek.EvaluationError struct that contains both the evaluation tree and the list of error results.

Examples

iex> eval_collect(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01-12"
...> )
{:ok, [~D[2000-01-12]]}

iex> eval_collect(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01.12"
...> )
{
  :error,
  %Spek.EvaluationError{
    expression: %Spek.Check{
      module: Date,
      fun: :from_iso8601,
      args: [:ctx],
      result: {:error, :invalid_format},
      satisfied?: false
    },
    results: [:invalid_format],
    message: "rule evaluation failed"
  }
}

eval_collect!(expression, context \\ [])

(since 0.2.0)
@spec eval_collect!(expression(), context()) :: [term()] | no_return()

Evaluates the given expression with eval_tree/2 and collects the results into a list with collect_results/2.

In the success case, the list of success results is returned.

In the error case, a Spek.EvaluationError exception is raised that contains both the evaluation tree and the list of error results.

Examples

iex> eval_collect!(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01-12"
...> )
[~D[2000-01-12]]

iex> eval_collect!(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01.12"
...> )
** (Spek.EvaluationError) rule evaluation failed

eval_collect_all(expression, context \\ [])

(since 0.2.0)
@spec eval_collect_all(expression(), context()) ::
  {:ok, [term()]} | {:error, Spek.EvaluationError.t()}

Evaluates the given expression with eval_tree_all/2 and collects the results into a list with collect_results/2.

In the success case, an :ok tuple with the list of success results is returned.

In the error case, an :error tuple with a Spek.EvaluationError struct that contains both the evaluation tree and the list of error results.

Examples

iex> eval_collect_all(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01-12"
...> )
{:ok, [~D[2000-01-12]]}

iex> eval_collect_all(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01.12"
...> )
{
  :error,
  %Spek.EvaluationError{
    expression: %Spek.Check{
      module: Date,
      fun: :from_iso8601,
      args: [:ctx],
      result: {:error, :invalid_format},
      satisfied?: false
    },
    results: [:invalid_format],
    message: "rule evaluation failed"
  }
}

eval_collect_all!(expression, context \\ [])

(since 0.2.0)
@spec eval_collect_all!(expression(), context()) :: [term()] | no_return()

Evaluates the given expression with eval_tree_all/2 and collects the results into a list with collect_results/2.

In the success case, the list of success results is returned.

In the error case, a Spek.EvaluationError exception is raised that contains both the evaluation tree and the list of error results.

Examples

iex> eval_collect_all!(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01-12"
...> )
[~D[2000-01-12]]

iex> eval_collect_all!(
...>   %Check{module: Date, fun: :from_iso8601, args: [:ctx]},
...>   "2000-01.12"
...> )
** (Spek.EvaluationError) rule evaluation failed

eval_tree(expression, context \\ [])

(since 0.1.0)
@spec eval_tree(expression(), context()) ::
  {:ok, expression()} | {:error, Spek.EvaluationError.t()}

Evaluates the given expression and returns the expression annotated with evaluation results.

Stops early as soon as the final outcome is determined. The returned expression only contains the evaluated parts.

Examples

iex> eval_tree(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "scene_042_render_complete"
...> )
{
  :ok,
  %Spek.Check{
    module: String,
    fun: :starts_with?,
    args: [:ctx, "scene_"],
    result: true,
    satisfied?: true
  }
}

iex> eval_tree(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "shot_042_render_complete"
...> )
{
  :error,
  %Spek.EvaluationError{
    expression: %Spek.Check{
      module: String,
      fun: :starts_with?,
      args: [:ctx, "scene_"],
      result: false,
      satisfied?: false
    },
    message: "rule evaluation failed"
  }
}

eval_tree!(expression, context \\ [])

(since 0.1.0)
@spec eval_tree!(expression(), context()) :: expression() | no_return()

Evaluates the given expression and returns the expression annotated with evaluation results or raises an error.

Stops early as soon as the final outcome is determined. The returned expression only contains the evaluated parts.

Raises an exception if the rule is not satisfied. Unlike eval!/2, the exception contains the evaluated expression.

Examples

iex> eval_tree!(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "scene_042_render_complete"
...> )
%Spek.Check{
  module: String,
  fun: :starts_with?,
  args: [:ctx, "scene_"],
  result: true,
  satisfied?: true
}

iex> eval_tree!(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "shot_042_render_complete"
...> )
** (Spek.EvaluationError) rule evaluation failed

eval_tree_all(expression, context \\ [])

(since 0.1.0)
@spec eval_tree_all(expression(), context()) ::
  {:ok, expression()} | {:error, Spek.EvaluationError.t()}

Evaluates the full given expression and returns the expression annotated with the expression results.

Always evaluates the entire expression, even if the final outcome could be determined earlier.

Examples

iex> eval_tree_all(
...>   all_of([
...>     %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>     %Check{module: String, fun: :ends_with?, args: [:ctx, "_render_complete"]}
...>   ]),
...>   "scene_042_render_complete"
...> )
{
  :ok,
  %Spek.AllOf{
    children: [
      %Spek.Check{
        module: String,
        fun: :starts_with?,
        args: [:ctx, "scene_"],
        result: true,
        satisfied?: true
      },
      %Spek.Check{
        module: String,
        fun: :ends_with?,
        args: [:ctx, "_render_complete"],
        result: true,
        satisfied?: true
      }
    ],
    satisfied?: true
  }
}

iex> eval_tree_all(
...>   all_of([
...>     %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>     %Check{module: String, fun: :ends_with?, args: [:ctx, "_render_complete"]}
...>   ]),
...>   "shot_042_render_complete"
...> )
{
  :error,
  %Spek.EvaluationError{
    expression: %Spek.AllOf{
      children: [
        %Spek.Check{
          module: String,
          fun: :starts_with?,
          args: [:ctx, "scene_"],
          result: false,
          satisfied?: false
        },
        %Spek.Check{
          module: String,
          fun: :ends_with?,
          args: [:ctx, "_render_complete"],
          result: true,
          satisfied?: true
        }
      ],
      satisfied?: false
    },
    message: "rule evaluation failed"
  }
}

eval_tree_all!(expression, context \\ [])

(since 0.1.0)
@spec eval_tree_all!(expression(), context()) :: expression() | no_return()

Evaluates the full given expression and returns the expression annotated with evaluation results or raises an error.

Raises if the expression is not satisfied. Unlike eval!/2, the raised exception contains the evaluated expression.

Always evaluates the entire expression, even if the final outcome could be determined earlier.

Examples

iex> eval_tree_all!(
...>   any_of([
...>     %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>     %Check{module: String, fun: :ends_with?, args: [:ctx, "_render_complete"]}
...>   ]),
...>   "scene_042_render_complete"
...> )
%Spek.AnyOf{
  satisfied?: true,
  children: [
    %Spek.Check{
      module: String,
      fun: :starts_with?,
      args: [:ctx, "scene_"],
      result: true,
      satisfied?: true
    },
    %Spek.Check{
      module: String,
      fun: :ends_with?,
      args: [:ctx, "_render_complete"],
      result: true,
      satisfied?: true
    }
  ]
}

iex> eval_tree_all!(
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]},
...>   "shot_042_render_complete"
...> )
** (Spek.EvaluationError) rule evaluation failed

filter(items, expression)

(since 0.1.0)
@spec filter(Enumerable.t(), expression()) :: Enumerable.t()

Filters the given enumerable to only retain the items that satisfy the given expression.

Example

iex> filter(
...>   ["scene_042_render_complete", "shot_017_proxy_ready"],
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]}
...> )
["scene_042_render_complete"]

reject(items, expression)

(since 0.1.0)
@spec reject(Enumerable.t(), expression()) :: Enumerable.t()

Filters the given enumerable to only retain the items that do not satisfy the given expression.

Example

iex> reject(
...>   ["scene_042_render_complete", "shot_017_proxy_ready"],
...>   %Check{module: String, fun: :starts_with?, args: [:ctx, "scene_"]}
...> )
["shot_017_proxy_ready"]

to_boolean(bool)

(since 0.1.0)
@spec to_boolean(result()) :: boolean()

Converts a check result to a boolean.

The evaluation functions apply this mapping to whatever a check function returns, and the functions generated by Spek.Macros.defcheck/2 call it directly. Use it when you write a check function by hand and need the boolean outcome of a result.

Raises an ArgumentError if the value is not a valid result.

Examples

iex> to_boolean(true)
true

iex> to_boolean(:ok)
true

iex> to_boolean({:ok, "proxy_stream_ready"})
true

iex> to_boolean(false)
false

iex> to_boolean(:error)
false

iex> to_boolean({:error, :codec_not_supported})
false

Optimization Functions

optimize(expression)

(since 0.1.0)
@spec optimize(expression()) :: expression()

Performs boolean algebra transformations on the given expression.

The transformations are applied repeatedly until no further optimization applies.

Optimization may remove, deduplicate and reorder checks, so check functions must be free of side effects.

Some transformations remove a sub-expression whose result cannot change the outcome, and eval_collect/2 and the other collecting functions do not return the results of what was removed. Given A or (A and B), a failing A makes the outcome independent of B, so the optimized expression returns the result of A alone.

The identity, annihilation, absorption and complement laws remove sub-expressions this way. Deduplication, factoring, De Morgan's laws, double negation elimination and the negation of a literal rewrite the expression without removing anything, and leave the collected results unchanged.

Examples

iex> Spek.optimize(%AnyOf{
...>   children: [
...>     %AllOf{
...>       children: [
...>         %Check{module: RenderChecks, fun: :gpu_available, args: []},
...>         %Check{module: RenderChecks, fun: :texture_cache_warm, args: []}
...>       ]
...>     },
...>     %AllOf{
...>       children: [
...>         %Check{module: RenderChecks, fun: :color_space_valid, args: []},
...>         %Check{module: RenderChecks, fun: :gpu_available, args: []}
...>       ]
...>     },
...>     %Check{module: RenderChecks, fun: :fallback_renderer_enabled, args: []}
...>   ]
...> })
%AnyOf{
  children: [
    %AllOf{
      children: [
        %Check{module: RenderChecks, fun: :gpu_available, args: []},
        %AnyOf{
          children: [
            %Check{module: RenderChecks, fun: :texture_cache_warm, args: []},
            %Check{module: RenderChecks, fun: :color_space_valid, args: []}
          ]
        }
      ]
    },
    %Check{module: RenderChecks, fun: :fallback_renderer_enabled, args: []}
  ]
}

Optimizations

OptimizationFormula
Identity (AND)A and true = A
Identity (OR)A or false = A
Annihilation (AND)A and false = false
Annihilation (OR)A or true = true
Double negation eliminationnot (not A) = A
Negation of literalsnot true = false, not false = true
De Morgan’s law (AND)not (A and B) = (not A) or (not B)
De Morgan’s law (OR)not (A or B) = (not A) and (not B)
Empty conjunctionallof() = true
Empty disjunctionanyof() = false
Single-child conjunction eliminationallof(A) = A
Single-child disjunction eliminationanyof(A) = A
Deduplication (AND)A and A = A
Deduplication (OR)A or A = A
Complement (AND)A and (not A) = false
Complement (OR)A or (not A) = true
Factoring OR over AND(A and B) or (A and C) = A and (B or C)
Factoring AND over OR(A or B) and (A or C) = A or (B and C)
Absorption (OR)A or (A and B) = A
Absorption (AND)A and (A or B) = A

Types

context()

@type context() :: term()

The evaluation context as passed to the evaluation functions.

If a check uses a {:ctx, key} argument, the context must be a map or a keyword list containing that key.

expression()

@type expression() ::
  Spek.AllOf.t()
  | Spek.AnyOf.t()
  | Spek.Check.t()
  | Spek.Literal.t()
  | Spek.Not.t()

A boolean expression tree.

falsy()

@type falsy() :: false | :error | {:error, term()}

An expression result value that results in a failed evaluation.

result()

@type result() :: truthy() | falsy()

The result of an expression evaluation.

truthy()

@type truthy() :: true | :ok | {:ok, term()}

An expression result value that results in a successful evaluation.