invariant.core

API facade providing invariants on Clojure data structures.

acyclic

(acyclic name edge-fn)(acyclic name edge-fn describe-fn)

Generates an Invariant verifying that there are no cyclic properties within the element currently being verified. This needs two functions:

  • edge-fn: takes the state and the current value and produces a map of node IDs to a set of successor nodes,
  • describe-fn: takes the state and the current value and produces a function that maps node IDs to a more descriptive representation to be included in errors (defaults to returning the node ID itself).

edge-fn should produce a map like the following, describing the edges of a graph constructed from the value currently being verified:

{:a #{:b :c}
 :c #{:d}
 :d #{:a}}

describe-fn can be given to provide more information to errors (e.g. to retain more of the original input than just the node ID). E.g., if the input is a seq of nodes akin to {:id "A", :children #{...}} one could retain the full values using:

(defn describe-nodes
  [nodes]
  (into {} (map (juxt :id identity) nodes)))

Any error container produced by this invariant will provide the detected cycle, as well as the relevant edges within the :invariant/error key.

all

(all invariant seq-invariant)

Generates an Invariant that will be applied to the seq of all elements currently being verified.

and

(and invariant & more)

Generate an Invariant combining all of the given ones.

(invariant/and
    (invariant/value :int? (comp integer? :value))
    (-> (invariant/on [:children ALL])
        (invariant/each ...)))

any

(any)

An Invariant that will never produce an error.

as

(as state-key path reduce-fn initial-value)(as invariant state-key path reduce-fn initial-value)

Generates an Invariant that uses the given specter path, reduce fn and value to attach a key to the invariant state, based on the current value.

(-> (invariant/as :declared-variables [:declarations ALL :name] conj #{})
    (invariant/on [:usages ALL :name])
    ...)

The resulting invariant state can be used e.g. in bind, property and state and will be shaped similarly to the following:

{:declared-variables {"a", "b", "c"}}

An inner invariant can be supplied, in which case the state will be computed from the selected values.

bind

(bind bind-fn)

Generate an Invariant that will use bind-fn, applied to the invariant state and the value currently being verified, to decide on an invariant to use.

(-> (invariant/on [:functions ALL])
    (invariant/each
      (invariant/bind
        (fn [_ {:keys [function-name]}]
          (case function-name
            "F" (invariant/property :f-args-valid? ...)
            "G" (invariant/property :g-args-valid? ...)
            ...)))))

This can be used to do invariant dispatch based on concrete values.

check

(check invariant data)(check invariant initial-state data)

Like run but will return either nil or a seq of errors.

collect-as

(collect-as state-key path)(collect-as invariant state-key path)(collect-as invariant state-key path {:keys [unique?], :or {unique? true}})

Like as, collecting all elements at the given specter path at the desired key within the invariant state. If :unique? (default: true) is set the result will be a set.

compute-as

added in 0.1.2

(compute-as state-key f)(compute-as state-key f path)(compute-as invariant state-key f)(compute-as invariant state-key f path)

Like as, computing a value by applying f to the current state and all elements matching path and storing it under the given key.

(-> (invariant/as :name->dependencies ...)
    (invariant/on [:elements ALL])
    (invariant/compute-as
      :current-dependencies
      (fn [{:keys [name->dependencies]} [name]]
        (name->dependencies name))
      [:name])
    ...)

The above example will:

  • create :name->dependencies in the state by analyzing the top-level value,
  • iterate over each value in :elements,
  • create current-dependencies in the state by looking it up in the previously created :name->dependencies.

count-as

(count-as state-key path)(count-as invariant state-key path)

Like as, storing the number of elements at the given specter path at the desired key within the invariant state.

each

(each invariant element-invariant)

Generates an Invariant that will be individually applied to all elements currently being verified.

fail

(fail name)

Generate an Invariant that will always produce an error.

first-as

added in 0.1.1

(first-as state-key path)(first-as invariant state-key path)

Like as, storing the first element currently being verified under the given key.

fmap

(fmap invariant f)

Transform each element currently being verified.

(-> (invariant/on [:declarations ALL :name]
    (invariant/fmap
      #(assoc % :name-count (count (:name %))))))

holds?

(holds? invariant data)(holds? invariant initial-state data)

Returns true if running the invariant against the given data does not produce any errors.

is?

(is? invariant self-invariant)

Generates an Invariant that will be applied to the single element currently being verified. Will throw an exception if a selector like on matched multiple elements.

(-> (invariant/on [(must :right)])
    (invariant/is? tree-balanced-invariant))

This behaves like each but will not pollute the invariant error path with a zero index.

on

macro

(on invariant path)(on path)

Generates an Invariant that uses the given specter path to collect all pieces of data the invariant should apply to. This is basically a selector, producing elements subsequent invariants will be applied to.

(-> (invariant/on [:declarations ALL])
    (invariant/each ...))

Optionally, an inner invariant can be supplied in which case the selector is run on its result. This lets you, e.g., collect state while stepping through your data:

(-> (invariant/on [:body])
    (invariant/collect-as :variables [:declarations ALL])
    (invariant/on [:usages ALL])
    ...)

Here, you’ll collect all declarations at [:body :declarations ALL] before continueing with [:body :usages ALL].

If you need to generate a selector dynamically or from a path stored within a var/binding, use on*.

on*

(on* path path-form)(on* invariant path path-form)

Generates an Invariant that uses the given specter path to collect all pieces of data the invariant should apply to. This is basically a selector, producing elements subsequent invariants will be applied to.

(-> (invariant/on* [:declarations ALL] '[:declarations ALL])
    (invariant/each ...))

on should be preferred since it will automatically generate path-form for you.

on-current-value

(on-current-value)

An Invariant selector pointing at the current (i.e. top-level) value. Equivalent to (on [STAY]) without polluting the path with STAY elements.

on-values

macro

added in 0.1.3

(on-values selector-fn)(on-values invariant selector-fn)

Generates an Invariant that will run selector-fn on the invariant state and the seq of elements currently being verified, replacing the latter with the produced result.

This behaves like on but uses a function to directly generate the values to verify.

on-values*

added in 0.1.3

(on-values* invariant selector-fn path-form)

Generates an Invariant that will run selector-fn on the invariant state and the seq of elements currently being verified, replacing the latter with the produced result.

property

(property name pred-fn)

Generates a predicate whose pred-fn will be called with the invariant state and the value currently being verified.

(-> (invariant/on [:usages ALL :name])
    (invariant/collect-as :declared-variables [:declarations ALL :name])
    (invariant/each
      (invariant/property
        :declared?
        (fn [{:keys [declared-variables]} n]
          (contains? declared-variables n)))))

recursive

macro

(recursive [self-sym] invariant-form)

Generate a recursive invariant bound to self-sym within the body.

(invariant/recursive
  [self]
  (invariant/and
    (invariant/value :int? (comp integer? :value))
    (-> (invariant/on [:children ALL])
        (invariant/each self))))

The above could be used to verify e.g. the following nested map:

{:value 1
 :children [{:value :x
             :children [{:value 4}]}]}

run

(run invariant data)(run invariant initial-state data)

Verify that the given invariant holds for the given piece of data.

seq-property

added in 0.1.3

(seq-property name pred-fn)

Behaves like property but expects each element currently being verified to be a seq.

Invariant errors will contain the input seq as their :invariant/values.

state

(state name pred-fn)

Generates a predicate whose pred-fn will be called with the current state, ignoring any values currently being verified.

(-> (invariant/on-current-value)
    (invariant/as :count count)
    (invariant/is?
      (invariant/state :at-least-one? #(pos? (:count %)))))

If you need the values being verified to decide on whether the invariant holds, use property.

unique

(unique invariant name & [{:keys [unique-by], :or {unique-by identity}}])

Generates an Invariant verifying that all current elements are unique.

(-> (invariant/on [:declarations ALL :name])
    (invariant/unique :declarations-unique?))

Errors will be reported per-element.

value

(value name pred-fn)

Generates a stateless predicate whose pred-fn will be called with the value currently being verified.

(-> (invariant/on [:declarations ALL :name])
    (invariant/each
      (invariant/value :prefix-valid? #(string/starts-with? % "var_"))))

If you need the invariant state to decide on whether the invariant holds, use property.

values

added in 0.1.3

(values name pred-fn)

Behaves like value but expectes each element currently being verified to be a seq.

Invariant errors will contain the input seq as their :invariant/values.

with-error-context

(with-error-context invariant error-context-fn)

Generates an Invariant that attaches an error context to each invariant error produced by invariant.

(-> (invariant/on [:fields ALL])
    (invariant/each ...)
    (invariant/with-error-context
      (fn [_ {:keys [record-name]}]
        {:record-name record-name})))

The result of error-context-fn will be merged into each invariant error’s :invariant/error-context entry.

with-static-error-context

(with-static-error-context invariant error-context)

Like with-error-context but merging a static map into any invariant error’s :invariant/error-context entry.