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