clojure.spec.alpha
allows one to use non-resolvable specs when defining a new one:
(s/def :foo/bar (s/or :nope :foo/foo))
Here, :foo/foo
can’t be resolved and so using :foo/bar
will raise an exception on usage:
(s/valid? :foo/bar 42)
;; Exception Unable to resolve spec: :foo/foo clojure.spec.alpha/reg-resolve! (alpha.clj:69)
This is something that happens in my code when I make typos like :my-ns/my-spec
instead of ::my-ns/my-spec
. I’d like to catch those with unit-tests.
Diving in the clojure.spec.alpha
source code I found I can get all specs with (keys (s/registry))
so my test look like this:
(ns my-ns.spec-test
(:require [clojure.test :refer :all]
[clojure.spec.alpha :as s]
;; :require all the relevant namespaces to populate the
;; global spec registry.
[my-ns.spec1]
[my-ns.spec2]))
(deftest resolvable-specs
(doseq [spec (keys (s/registry))]
(is (resolvable? spec))))
;; ^^^^^^^^^^^ placeholder; that’s the function I want
Unfortunately there’s not such thing as s/resolvable?
in clojure.spec.alpha
. The only solution I’ve found so far is to call (s/valid? spec 42)
and assume it not raising an exception means it’s resolvable but it doesn’t check all branches:
(s/def :int/int int?)
(s/def :bool/bool bool?)
(s/def :my/spec (s/or :int :int/int
:other (s/or :bool bool/bool
:nope :idont/exist)))
(s/valid? :my/spec 1) ; <- matches the :int branch
;; => true
(s/valid? :my/spec :foo)
;; Exception Unable to resolve spec: :idont/exist clojure.spec.alpha/reg-resolve! (alpha.clj:69)
I checked the exception stacktrace as well as the source code to see if I could find any function to fully resolve a spec without using a test value like 42
or :foo
above but couldn’t find any.
Is there a way check that, for a given spec, all specs it refers to in all its branches do exist?
I was able to do the following:
(ns my-ns.utils
(:require [clojure.spec.alpha :as s]))
(defn- unresolvable-spec
[spec]
(try
(do (s/describe spec) nil)
(catch Exception e
(if-let [[_ ns* name*] (re-matches #"Unable to resolve spec: :([^/]+)/(.+)$" (.getMessage e))]
(keyword ns* name*)
(throw e)))))
(defn unresolvable?
"Test if a spec is unresolvable, and if so return a sequence
of the unresolvable specs it refers to."
[spec]
(cond
(symbol? spec)
nil
(keyword? spec)
(if-let [unresolvable (unresolvable-spec spec)]
[unresolvable]
(not-empty (distinct (unresolvable? (s/describe spec)))))
(seq? spec)
(case (first spec)
or (->> spec (take-nth 2) rest (mapcat unresolvable?))
and (->> spec rest (mapcat unresolvable?))
;; undecidable
nil)
:default (unresolvable-spec spec)))
(def resolvable? (complement unresolvable?))
It works on s/and
's and s/or
's, which was my minimal use-case:
(u/resolvable? :int/int) ;; => true
(u/resolvable? :my/spec) ;; => false
(u/unresolvable? :my/spec) ;; => (:idont/exist)
But it has some flaws:
clojure.spec.alpha
clojure.spec.alpha
doesn’t have a function that doesn’t raise an exception and (2) the functions that raise one don’t use anything more specific than Exception
I’d be happy to accept any other answer if someone has something more robust.