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
defp encode_json(term) do
Jason.encode(term)
end
end