Pinpointer is yet another clojure.spec error reporter based on a precise error analysis.
It has the following features:
- Visually pinpoints which portion of the input data is causing the spec error, based on the spec error analysis of
spectrace
, a fine-grained spec error analyzer - Formats and colorizes the error reports in an easy-to-grasp manner
- Tracks 'value changes', i.e. reports the spec errors correctly even when
s/conformer
in the spec transforms the input data - Extensible to user-defined spec macros (not documented yet)
Notice: Pinpointer is built on top of clojure.spec, which is one of Clojure's new features that have been developed most actively. So, it's still in alpha and its APIs are also subject to change.
Add the following to your :dependencies
:
clojure.spec provides an API named explain
, which describes which portion of the input data caused a spec error:
=> (s/def ::x integer?)
:user/x
=> (s/def ::y string?)
:user/y
=> (s/explain (s/keys :req-un [::x ::y]) {:y 1})
In: [:y] val: 1 fails spec: :user/y at: [:y] predicate: string?
val: {:y 1} fails predicate: (contains? % :x)
nil
=>
As you can see above, the result of explain
is simple and plain, but it is often not easy to understand intuitively what was wrong. And it will take longer time to find out where the actual problem is as the spec and input data are getting larger.
Pinpointer provides an API compatible with explain
, which is named pinpoint
, and it shows the spec errors in a visually easy-to-grasp manner:
=> (require '[pinpointer.core :as p])
nil
=> (p/pinpoint (s/keys :req-un [::x ::y]) {:y 1})
Detected 2 spec errors:
----------------------------------------------------------------------
(1/2)
Cause: {:y 1}
^^^^^^
Expected: (fn [%] (contains? % :x))
----------------------------------------------------------------------
(2/2)
Cause: {:y 1}
^
Expected: string?
----------------------------------------------------------------------
nil
=>
You can also colorize the error reports by adding the option {:colorize :ansi}
to increase the readability:
pinpoint
has several other options. See the docstring for more details.
If you'd rather completely replace the explain
facility for any kinds of spec error reporting, it would be helpful to replace s/*explain-out*
with pinpointer.core/pinpoint-out
instead:
=> (set! s/*explain-out* p/pinpoint-out)
#function[pinpointer.core/pinpoint-out]
=>
;; from now on, p/pinpoint-out will be used in place of s/explain-printer
=>
=> (defn f [x] (inc x))
#'user/f
=> (s/fdef f
:args (s/cat :x (s/and integer? even?))
:ret (s/and integer? odd?))
user/f
=> (require '[clojure.spec.test.alpha :as t])
nil
=> (t/instrument)
[user/f]
=> (f 3)
ExceptionInfo Call to #'user/f did not conform to spec:
Detected 1 spec error:
----------------------------------------------------------------------
(1/1)
Cause: (3)
^
Expected: even?
----------------------------------------------------------------------
user/eval3842 (form-init1169392389971828339.clj:1)
clojure.core/ex-info (core.clj:4744)
=>
Pinpointer also supports ClojureScript. Note, however, that it may use eval
for its spec error analysis in general (especially, when analyzing specs with a literal fn in them) while ClojureScript doesn't have a platform-independent eval
fn.
If you use Pinpointer from a self-hosted ClojureScript implementation equipped with eval
such as Planck or Lumo, it should work fine even in general cases as follows:
=> (require '[planck.core :as planck])
nil
=> (p/pinpoint (s/and integer? #(> % 10)) 5 {:eval planck/eval})
Detected 1 spec error:
----------------------------------------------------------------------
(1/1)
Cause: 5
^
Expected: (fn [%] (> % 10))
----------------------------------------------------------------------
nil
=>
There are a couple of known issues in Pinpointer, primarily due to clojure.spec
's bugs. They can be found on Issues page, being tagged with spec bug
.
If you found something wrong when using Pinpointer and you want to report an issue, check whether or not it's already been filed there first.
Copyright © 2016-2018 Shogo Ohta (@athos0220)
Distributed under the Eclipse Public License 1.0.