polix.parser
Parser for the policy DSL.
Transforms policy expressions (vector-based S-expressions) into AST nodes that can be evaluated. Supports document accessors, function calls, literals, and thunks for delayed evaluation.
binding-accessor?
(binding-accessor? k)Returns true if k is a binding accessor keyword.
Binding accessors are namespaced keywords that reference a bound variable from a quantifier, such as :u/role or :team/members. They have a namespace that is not a reserved namespace (doc, fn, self, param, event).
classify-token
(classify-token token position)Classifies a token into its AST node type based on position.
Returns a Result containing an ast/ASTNode with the appropriate type: - ::ast/doc-accessor for document accessors (value is a path vector) - ::ast/self-accessor for self accessors (value is a path vector) - ::ast/param-accessor for param accessors (value is keyword) - ::ast/event-accessor for event accessors (value is a path vector) - ::ast/doc-accessor for binding accessors (value is path, metadata has namespace) - ::ast/thunk for thunkable forms - ::ast/literal for all other values
For document accessors, the token’s name is parsed as a dot-separated path: - :doc/role becomes [:role] - :doc/user.name becomes [:user :name]
For binding accessors, the namespace is preserved in metadata: - :u/role becomes [:role] with metadata {:binding-ns "u"}
Returns {:ok ast-node} on success or {:error error-map} on failure.
doc-accessor?
(doc-accessor? k)Returns true if k is a document accessor keyword.
Document accessors are namespaced keywords with namespace "doc", such as :doc/actor-role.
event-accessor?
(event-accessor? k)Returns true if k is an event accessor keyword.
Event accessors reference event data in triggers, such as :event/target-id or :event/amount.
extract-doc-keys
(extract-doc-keys ast)Extracts all document accessor paths from a policy ast.
Returns a set of path vectors representing nested document access paths. Each path is a vector of keywords.
(extract-doc-keys (parse-policy [:= :doc/role "admin"]))
;=> #{[:role]}
(extract-doc-keys (parse-policy [:= :doc/user.name "Alice"]))
;=> #{[:user :name]}
extract-param-keys
(extract-param-keys ast)Extracts all parameter keys from a policy ast.
Returns a set of keywords representing required parameters. Traverses the entire AST including quantifier bodies and let bindings.
(extract-param-keys (r/unwrap (parse-policy [:= :doc/role :param/role])))
;=> #{:role}
(extract-param-keys (r/unwrap (parse-policy [:and
[:= :doc/role :param/role]
[:> :doc/level :param/min-level]])))
;=> #{:role :min-level}
fn-accessor?
(fn-accessor? k)Returns true if k is a function accessor keyword.
Function accessors are namespaced keywords with namespace "fn", such as :fn/count or :fn/sum.
let-binding?
(let-binding? form)Returns true if form is a let binding expression.
Let bindings have the form [:let [bindings...] body].
param-accessor?
(param-accessor? k)Returns true if k is a parameter accessor keyword.
Parameter accessors reference policy parameters, such as :param/role or :param/min-level.
parse-binding
(parse-binding binding-form position)Parses a quantifier binding form.
Supports two forms: - [name collection-path] - basic binding - [name collection-path :where predicate] - filtered binding
The binding form specifies a variable name and the collection to iterate over. The name can be a symbol or keyword, and the collection path must be a :doc/ accessor or a binding accessor (for nested quantifiers).
(parse-binding '[u :doc/users] [0 0])
;=> {:ok {:name :u, :namespace "doc", :path [:users]}}
(parse-binding '[u :doc/users :where [:= :u/active true]] [0 0])
;=> {:ok {:name :u, :namespace "doc", :path [:users], :where <ast>}}
Returns {:ok binding-map} on success or {:error error-map} on failure.
parse-doc-path
(parse-doc-path path-str)Parses a dot-separated path string into a vector of keywords.
Takes a path string from a document accessor keyword and returns a vector of keywords representing the nested path.
(parse-doc-path "role") ;=> [:role]
(parse-doc-path "user.name") ;=> [:user :name]
(parse-doc-path "user.profile.email") ;=> [:user :profile :email]
Returns {:ok path-vector} on success or {:error error-map} on failure for malformed paths (empty segments, leading/trailing dots).
parse-policy
(parse-policy expr)(parse-policy expr position)Parses a policy DSL expression expr into an AST.
The DSL supports: - Document accessors: :doc/key-name - Self accessors: :self/computed-value (from let bindings) - Parameter accessors: :param/role (policy parameters) - Event accessors: :event/target-id (trigger event data) - Binding accessors: :u/field (within quantifier bodies) - Function calls: [:fn-name arg1 arg2 ...] - Quantifiers: [:forall [u :doc/users] body], [:exists [t :doc/teams] body] - Value functions: [:fn/count :doc/users], [:fn/count [:u :doc/users :where [...]]] - Policy references: [:auth/admin], [:auth/has-role {:role "editor"}] - Let bindings: [:let [x :doc/value] [:= :self/x 5]] - Literals: strings, numbers, keywords, etc. - Thunks: Clojure vars and function calls wrapped for delayed evaluation
Takes an optional position vector [start end] for tracking location (defaults to [0 0]).
Returns {:ok ASTNode} on success or {:error error-map} on failure.
policy-reference?
(policy-reference? form)Returns true if form is a policy reference.
Policy references are vectors starting with a namespaced keyword that is not a built-in operator or accessor: - [:auth/admin] - [:auth/has-role {:role "editor"}]
quantifier-op?
(quantifier-op? op)Returns true if op is a quantifier operator (:forall or :exists).
self-accessor?
(self-accessor? k)Returns true if k is a self accessor keyword.
Self accessors reference values bound in let expressions, such as :self/computed-value.
thunkable?
(thunkable? form)Returns true if form should be wrapped in a thunk for delayed evaluation.
Vars and non-empty lists (function calls) should be thunked.
valid-function-name?
(valid-function-name? v)Returns true if v is a valid function name for the policy DSL.
Valid function names are keywords or symbols.