Current section
Files
Jump to
Current section
Files
lib/regolix.ex
defmodule Regolix do
@moduledoc """
Elixir wrapper for the Regorus Rego policy engine.
## Basic Usage
{:ok, engine} = Regolix.new()
{:ok, engine} = Regolix.add_policy(engine, "authz.rego", \"""
package authz
default allow = false
allow if input.user == "admin"
\""")
{:ok, engine} = Regolix.set_input(engine, %{"user" => "admin"})
{:ok, true} = Regolix.eval_query(engine, "data.authz.allow")
## Coverage Tracking
Track which policy lines are executed during evaluation:
{result, coverage} = Regolix.with_coverage(engine, fn e ->
Regolix.eval_query!(e, "data.authz.allow")
end)
# coverage => %{"authz.rego" => %{covered: [1, 2, 5], not_covered: [9, 10]}}
For multi-query accumulation:
engine = Regolix.enable_coverage!(engine)
Regolix.eval_query!(engine, "data.authz.allow")
Regolix.eval_query!(engine, "data.rbac.check")
coverage = Regolix.get_coverage_report!(engine)
engine = Regolix.disable_coverage!(engine)
"""
alias Regolix.{Error, Native}
@type engine :: reference()
@type json_encodable :: map() | list() | String.t() | number() | boolean() | nil
@type eval_result :: json_encodable() | :undefined
@type coverage_report :: %{
String.t() => %{covered: [pos_integer()], not_covered: [pos_integer()]}
}
@doc """
Creates a new Rego policy engine.
## Examples
{:ok, engine} = Regolix.new()
"""
@spec new() :: {:ok, engine()}
def new do
{:ok, Native.native_new()}
end
@doc """
Creates a new Rego policy engine. Raises on error.
"""
@spec new!() :: engine()
def new! do
Native.native_new()
end
@doc """
Adds a Rego policy to the engine.
## Examples
{:ok, engine} = Regolix.add_policy(engine, "authz.rego", "package authz")
"""
@spec add_policy(engine(), String.t(), String.t()) :: {:ok, engine()} | {:error, Error.t()}
def add_policy(engine, name, source) do
case Native.native_add_policy(engine, name, source) do
{:ok, {}} -> {:ok, engine}
{:error, {type, message}} -> {:error, %Error{type: type, message: message}}
end
end
@doc """
Adds a Rego policy to the engine. Raises on error.
"""
@spec add_policy!(engine(), String.t(), String.t()) :: engine()
def add_policy!(engine, name, source) do
case add_policy(engine, name, source) do
{:ok, engine} -> engine
{:error, error} -> raise error
end
end
@doc """
Returns the list of package names loaded in the engine.
## Examples
packages = Regolix.get_packages(engine)
# => ["data.authz", "data.rbac"]
"""
@spec get_packages(engine()) :: [String.t()]
def get_packages(engine) do
case Native.native_get_packages(engine) do
{:ok, packages} -> packages
packages when is_list(packages) -> packages
end
end
@doc """
Sets the input document for policy evaluation.
Accepts Elixir terms (maps, lists, etc.) which are automatically JSON-encoded.
## Examples
{:ok, engine} = Regolix.set_input(engine, %{"user" => "alice"})
"""
@spec set_input(engine(), json_encodable()) :: {:ok, engine()} | {:error, Error.t()}
def set_input(engine, input) do
with {:ok, json} <- encode_json(input),
{:ok, {}} <- Native.native_set_input(engine, json) do
{:ok, engine}
else
{:error, {type, message}} ->
{:error, %Error{type: type, message: message}}
{:error, %Jason.EncodeError{} = e} ->
{:error, %Error{type: :json_error, message: Exception.message(e)}}
{:error, %Protocol.UndefinedError{} = e} ->
{:error, %Error{type: :json_error, message: Exception.message(e)}}
end
end
@doc """
Sets the input document. Raises on error.
"""
@spec set_input!(engine(), json_encodable()) :: engine()
def set_input!(engine, input) do
case set_input(engine, input) do
{:ok, engine} -> engine
{:error, error} -> raise error
end
end
@doc """
Adds data to the engine's data document.
Can be called multiple times to merge data.
## Examples
{:ok, engine} = Regolix.add_data(engine, %{"users" => %{"alice" => %{"role" => "admin"}}})
"""
@spec add_data(engine(), json_encodable()) :: {:ok, engine()} | {:error, Error.t()}
def add_data(engine, data) do
with {:ok, json} <- encode_json(data),
{:ok, {}} <- Native.native_add_data(engine, json) do
{:ok, engine}
else
{:error, {type, message}} ->
{:error, %Error{type: type, message: message}}
{:error, %Jason.EncodeError{} = e} ->
{:error, %Error{type: :json_error, message: Exception.message(e)}}
{:error, %Protocol.UndefinedError{} = e} ->
{:error, %Error{type: :json_error, message: Exception.message(e)}}
end
end
@doc """
Adds data to the engine. Raises on error.
"""
@spec add_data!(engine(), json_encodable()) :: engine()
def add_data!(engine, data) do
case add_data(engine, data) do
{:ok, engine} -> engine
{:error, error} -> raise error
end
end
@doc """
Clears all data from the engine, keeping policies intact.
## Examples
{:ok, engine} = Regolix.clear_data(engine)
"""
@spec clear_data(engine()) :: {:ok, engine()} | {:error, Error.t()}
def clear_data(engine) do
case Native.native_clear_data(engine) do
{:ok, {}} -> {:ok, engine}
{:error, {type, message}} -> {:error, %Error{type: type, message: message}}
end
end
@doc """
Clears all data from the engine. Raises on error.
"""
@spec clear_data!(engine()) :: engine()
def clear_data!(engine) do
case clear_data(engine) do
{:ok, engine} -> engine
{:error, error} -> raise error
end
end
@doc """
Enables coverage tracking on the engine.
After enabling, all query evaluations will track which policy lines are executed.
Use `get_coverage_report/1` to retrieve the accumulated coverage data.
## Examples
engine = Regolix.enable_coverage!(engine)
"""
@spec enable_coverage!(engine()) :: engine()
def enable_coverage!(engine) do
case Native.native_enable_coverage(engine, true) do
{:ok, {}} -> engine
{:error, {type, message}} -> raise %Error{type: type, message: message}
end
end
@doc """
Disables coverage tracking on the engine.
Coverage data is retained until explicitly cleared with `clear_coverage!/1`.
## Examples
engine = Regolix.disable_coverage!(engine)
"""
@spec disable_coverage!(engine()) :: engine()
def disable_coverage!(engine) do
case Native.native_enable_coverage(engine, false) do
{:ok, {}} -> engine
{:error, {type, message}} -> raise %Error{type: type, message: message}
end
end
@doc """
Returns the accumulated coverage report.
Returns coverage data for all policy files, showing which lines were
executed (covered) and which were not (not_covered).
## Examples
{:ok, coverage} = Regolix.get_coverage_report(engine)
# => %{"authz.rego" => %{covered: [1, 2, 5], not_covered: [9, 10]}}
"""
@spec get_coverage_report(engine()) :: {:ok, coverage_report()} | {:error, Error.t()}
def get_coverage_report(engine) do
case Native.native_get_coverage_report(engine) do
{:ok, report} -> {:ok, report}
{:error, {type, message}} -> {:error, %Error{type: type, message: message}}
end
end
@doc """
Returns the accumulated coverage report. Raises on error.
"""
@spec get_coverage_report!(engine()) :: coverage_report()
def get_coverage_report!(engine) do
case get_coverage_report(engine) do
{:ok, report} -> report
{:error, error} -> raise error
end
end
@doc """
Clears accumulated coverage data without disabling coverage.
Use this to reset coverage between test runs while keeping coverage enabled.
## Examples
engine = Regolix.clear_coverage!(engine)
"""
@spec clear_coverage!(engine()) :: engine()
def clear_coverage!(engine) do
case Native.native_clear_coverage(engine) do
{:ok, {}} -> engine
{:error, {type, message}} -> raise %Error{type: type, message: message}
end
end
@doc """
Executes a function with coverage tracking enabled, returning both the result and coverage.
This is the recommended API for single-operation coverage. Coverage is automatically
enabled before the function runs and disabled/cleared afterward.
## Examples
{result, coverage} = Regolix.with_coverage(engine, fn e ->
Regolix.eval_query!(e, "data.authz.allow")
end)
# result => true
# coverage => %{"authz.rego" => %{covered: [1, 2, 5], not_covered: [9, 10]}}
For multi-query accumulation, use the raw primitives:
- `enable_coverage!/1`
- `disable_coverage!/1`
- `get_coverage_report!/1`
- `clear_coverage!/1`
"""
@spec with_coverage(engine(), (engine() -> result)) :: {result, coverage_report()}
when result: var
def with_coverage(engine, fun) when is_function(fun, 1) do
engine = enable_coverage!(engine)
engine = clear_coverage!(engine)
try do
result = fun.(engine)
coverage = get_coverage_report!(engine)
{result, coverage}
after
# Clean up: disable coverage and clear data
disable_coverage!(engine)
clear_coverage!(engine)
end
end
@doc """
Evaluates a Rego query against the engine.
Returns the result as Elixir terms, or `:undefined` if the query has no result.
## Examples
{:ok, true} = Regolix.eval_query(engine, "data.authz.allow")
{:ok, :undefined} = Regolix.eval_query(engine, "data.authz.nonexistent")
"""
@spec eval_query(engine(), String.t()) :: {:ok, eval_result()} | {:error, Error.t()}
def eval_query(engine, query) do
case Native.native_eval_query(engine, query) do
{:ok, result} -> {:ok, result}
{:error, {type, message}} -> {:error, %Error{type: type, message: message}}
end
end
@doc """
Evaluates a Rego query. Raises on error.
"""
@spec eval_query!(engine(), String.t()) :: eval_result()
def eval_query!(engine, query) do
case eval_query(engine, query) do
{:ok, result} -> result
{:error, error} -> raise error
end
end
@type rule_info :: %{
name: String.t(),
description: String.t(),
start_line: pos_integer(),
end_line: pos_integer()
}
@doc """
Returns metadata about rules defined in loaded policies.
Parses the policy sources to extract rule names, descriptions (from comments),
and line ranges. Useful for mapping coverage line numbers to human-readable rule names.
## Examples
rules = Regolix.get_rules(engine)
# => %{
# "policy.rego" => [
# %{name: "allow", description: "Allow if not denied", start_line: 10, end_line: 15},
# %{name: "deny", description: "Deny sanctioned countries", start_line: 20, end_line: 25}
# ]
# }
"""
@spec get_rules(engine()) :: {:ok, %{String.t() => [rule_info()]}} | {:error, Error.t()}
def get_rules(engine) do
case Native.native_get_rules(engine) do
{:ok, rules} -> {:ok, rules}
{:error, {type, message}} -> {:error, %Error{type: type, message: message}}
end
end
@doc """
Returns metadata about rules defined in loaded policies. Raises on error.
"""
@spec get_rules!(engine()) :: %{String.t() => [rule_info()]}
def get_rules!(engine) do
case get_rules(engine) do
{:ok, rules} -> rules
{:error, error} -> raise error
end
end
defp encode_json(term) do
Jason.encode(term)
end
end