polix.core

Core functionality for polix - a DSL for writing declarative policies.

Polix provides a vector-based DSL for defining policies that evaluate against documents. Policies support document accessors (:doc/key), function calls, and literals.

Quick Start

Define a policy:

(require '[polix.core :as polix])

(polix/defpolicy AdminOnly
  "Only admins can access"
  [:= :doc/role "admin"])

Unify a policy with a document:

(polix/unify (:ast AdminOnly) {:role "admin"})
;=> {}  ; satisfied

(polix/unify (:ast AdminOnly) {:role "guest"})
;=> {[:role] [[:conflict [:= "admin"] "guest"]]}  ; conflict

(polix/unify (:ast AdminOnly) {})
;=> {[:role] [[:= "admin"]]}  ; open residual

Compiled Policies

For optimized evaluation with constraint merging and simplification:

(def checker (polix/compile-policies
               [[:= :doc/role "admin"]
                [:> :doc/level 5]]))

(checker {:role "admin" :level 10})  ;=> {}
(checker {:role "guest"})            ;=> {[:role] [[:conflict [:= "admin"] "guest"]]}
(checker {:role "admin"})            ;=> {[:level] [[:> 5]]}

Result Types

Unification and compiled policies return one of three result types:

  • {} (empty map) — satisfied, all constraints met
  • {:path [constraints]} — open residual, awaiting more data
  • {:path [[:conflict C witness]]} — conflict, constraint violated

Use satisfied?, residual?, has-conflicts?, and open-residual? predicates to check result types.

Main Concepts

  • Document: Any associative data structure (map, record, etc.)
  • Policy: Declarative rule defined via defpolicy
  • AST: Abstract syntax tree representation of policies
  • Unify: Evaluates policy against document, returns residual
  • Compiler: Merges and optimizes policies before evaluation

Namespaces

The implementation is split across multiple namespaces:

all-conflicts?

Returns true if all constraints in the residual are conflicts.

Used to determine if NOT of a fully-conflicted residual should be satisfied.

analyze-policy

Analyzes a policy to determine its requirements and characteristics.

Returns a map with: - :params — set of required parameter keys - :doc-keys — set of document paths accessed - :parameterized? — true if policy requires any params

Example:

(analyze-policy [:= :doc/role :param/role])
;=> {:params #{:role}
;    :doc-keys #{[:role]}
;    :parameterized? true}

ast-node

bind-params

(bind-params policy params)

Partially binds parameters to a policy, returning a policy context.

Takes a policy expression and a map of param bindings. Returns a map with :policy and :params that can be used with unify.

Example:

;; Create a partial binding
(def bound (bind-params [:auth/has-role] {:role "admin"}))
;=> {:policy [:auth/has-role] :params {:role "admin"}}

;; Evaluate with the bound params
(unify (:policy bound) document {:params (:params bound)})

classify-token

combine-residuals

Combines two residuals with OR semantics.

Returns: - {} if either is satisfied - Combined residual otherwise

compile-policies

conflict

Creates a conflict constraint tuple.

(conflict :< 10 11) ;=> [:conflict :< 10 11]

conflict-constraint

Extracts the inner constraint from a conflict.

conflict-residual

Creates a residual with a single conflict constraint.

(conflict-residual :x :< 10 15) ;=> {:x :conflict :< 10 15}

conflict-witness

Extracts the witness value from a conflict.

conflict?

Returns true if x is a conflict constraint tuple.

create-registry

Creates a new registry with built-in namespace entries.

The registry manages namespace resolution for policy evaluation, including document accessors (:doc/), function references (:fn/), and user-defined modules.

Example:

(create-registry)
;=> #RegistryRecord{:entries {...} :version 0}

defpolicy

macro

(defpolicy name & args)

Defines a policy with a name, optional docstring, and policy expression.

See polix.policy/defpolicy for full documentation.

detect-cycle

Detects if a dependency graph contains a cycle.

Returns nil if no cycle, or a vector representing the cycle path.

doc-accessor

doc-accessor?

error

error?

event-accessor?

Returns true if keyword is an event accessor (:event/key).

extract-doc-keys

extract-param-keys

Extracts all parameter keys from a policy AST.

Lower-level function that works directly on parsed AST nodes. For most use cases, prefer required-params which works on policy expressions directly.

function-call

has-conflicts?

Returns true if result contains conflict constraints.

A conflict indicates a constraint was evaluated against concrete data and failed. Use this instead of checking for nil.

let-binding?

Returns true if form is a let binding ([:let [...] body]).

literal

load-module

Loads a single module definition into a registry.

Returns the updated registry. Does not validate imports.

load-modules

Loads multiple modules into a registry with dependency resolution.

Validates all module definitions, checks for circular imports, verifies that all imports exist, and loads modules in topological order (dependencies first).

Returns {:ok registry} on success, {:error error-map} on failure.

Example:

(load-modules (create-registry)
              [{:namespace :common
                :policies {:active [:= :doc/status "active"]}}
               {:namespace :auth
                :imports [:common]
                :policies {:admin [:= :doc/role "admin"]}}])

merge-policies

merge-residuals

Merges two residuals with AND semantics.

Returns: - nil if either is nil (legacy) - The merged residual otherwise

module-namespaces

Returns the set of user-defined module namespaces in the registry.

negate

Negates an AST node, returning its logical complement.

Example:

(negate [:= :doc/role "admin"])
;=> [:!= :doc/role "admin"]

(negate [:and [:= :doc/a 1] [:= :doc/b 2]])
;=> [:or [:!= :doc/a 1] [:!= :doc/b 2]]

ok

ok?

open-residual?

Returns true if result is an open residual (no conflicts).

An open residual contains constraints awaiting evaluation but no definite failures.

param-accessor?

Returns true if keyword is a parameter accessor (:param/key).

param-defaults

Returns default values for a policy’s parameters.

Returns a map of param-key to default value.

Example:

(param-defaults registry :auth :min-level)
;=> {:min 0}

parameterized-policies

Returns all parameterized policies in a module.

Returns a map of policy-key to param info.

Example:

(parameterized-policies registry :auth)
;=> {:has-role {:params #{:role} :defaults {} :description nil}}

parse-policy

policy-info

Returns information about a policy in the registry.

Returns a map with: - :expr — the policy expression - :params — set of required parameter keys - :param-defs — map of param key to definition - :defaults — map of param key to default value - :description — policy description if provided - :parameterized? — true if policy requires params

Returns nil if the policy is not found.

Example:

(policy-info registry :auth :has-role)
;=> {:expr [:= :doc/role :param/role]
;    :params #{:role}
;    :defaults {}
;    :parameterized? true}

policy-reference?

Returns true if form is a policy reference ([:ns/policy]).

register-alias

Registers an alias from one namespace to another.

Returns a new registry with the alias added.

Example:

(-> (create-registry)
    (register-alias :a :auth))

register-module

Registers a module namespace in the registry.

Modules define named policies that can be referenced by other modules. Returns a new registry with the module added.

Example:

(-> (create-registry)
    (register-module :auth {:policies {:admin [:= :doc/role "admin"]}}))

required-params

Returns the set of required parameter keys for a policy.

Example:

(required-params [:= :doc/role :param/role])
;=> #{:role}

residual->constraints

residual?

Returns true if result is a residual (non-empty constraint map).

A residual contains path keys with remaining constraints.

resolve-namespace

Resolves a namespace key to its entry, following aliases.

Returns the namespace entry or nil if not found.

resolve-policy

Resolves a policy from a module namespace.

Returns the policy AST or nil if not found.

Example:

(resolve-policy registry :auth :admin)
;=> [:= :doc/role "admin"]

result->policy

satisfied?

Returns true if result represents a satisfied policy (empty residual).

A satisfied result is {} (empty map).

self-accessor?

Returns true if keyword is a self-accessor (:self/key).

thunk

thunkable?

topological-sort

Returns nodes in dependency order (dependencies first).

Uses DFS-based topological sort.

unify

Unifies a policy with a document, returning a residual.

Takes a policy (AST, constraint set, or policy expression vector) and a document. Returns:

  • {} — satisfied (all constraints met)
  • {:path [constraints]} — open residual (awaiting more data)
  • {:path [[:conflict C w]]} — conflict (constraint violated by witness w)

Example:

(unify [:= :doc/role "admin"] {:role "admin"})
;=> {}

(unify [:= :doc/role "admin"] {:role "guest"})
;=> {[:role] [[:conflict [:= "admin"] "guest"]]}

(unify [:= :doc/role "admin"] {})
;=> {[:role] [[:= "admin"]]}

unwrap

validate-params

(validate-params policy params)

Validates that all required params are provided for a policy.

Returns {:ok params} if all required params are present, {:error {:missing #{...}}} if any are missing.

Example:

(validate-params [:= :doc/role :param/role] {:role "admin"})
;=> {:ok {:role "admin"}}

(validate-params [:= :doc/role :param/role] {})
;=> {:error {:missing #{:role}}}