*Description*
Suppose we want to traverse a spec that caused some spec error, from the root spec embedded in the explain-data to a leaf of the pred that was the actual cause of the error.
We can usually use {{:path}} info in the explain-data for such a purpose:
user=> (s/explain-data (s/tuple integer? string?) [1 :a])
#:clojure.spec.alpha{:problems
({:path [1], ;; <- indicates the 1st subspec, ie. integer?, was the cause of the error
:pred clojure.core/string?,
:val :a,
:via [],
:in [1]}),
:spec ...,
:value [1 :a]}
user=>
If we traverse the spec tree along the {{:path}}, we can eventually reach the leaf pred that raised the spec error.
In some cases, however, it doesn't hold since some specs such as {{s/merge}}, {{s/and}} and {{s/&}} don't put any clue into {{:path}} that tells which subspec actually raised the error:
user=> (s/explain-data (s/merge (s/map-of integer? string?)
(s/coll-of (fn [[k v]] (= (str k) v))))
{1 "2"})
#:clojure.spec.alpha{:problems
({:path [], ;; <- doesn't tell us anything at all
:pred (fn [[k v]] (= (str k) v)), ;; <- we don't know which subspec this pred occurs in
:val [1 "2"],
:via [],
:in [0]}),
:spec ...,
:value {1 "2"}}
user=>
To achieve our purpose even in those cases, we have to make a nondeterministic choice: that is, choose a subspec arbitrarily and try traversing it down, and if something is wrong along the way, then backtrack to another subspec and so on.
From my experience that I implemented that backtracking algorithm in a library I'm working on ([repo|
https://github.com/athos/spectrace]), I think it's much harder to implement correctly than necessary. In fact, my implementation is probably broken in some corner cases, and I don't even know if it's possible in theory to implement it completely correctly.
*Proposal*
To make it easier to implement the spec traversal, this ticket proposes adding the index into {{:path}} that indicates which subspec raised the spec error for {{s/merge}}, {{s/and}} and {{s/&}}, as follows:
user=> (s/explain-data (s/merge (s/map-of integer? string?)
(s/coll-of (fn [[k v]] (= (str k) v))))
{1 "2"})
#:clojure.spec.alpha{:problems
({:path [1], ;; <- indicates the 1st subspec, ie. (s/coll-of (fn [[k v]] (= (str k) v))) has the actual cause of the error in it
:pred (fn [[k v]] (= (str k) v)),
:val [1 "2"],
:via [],
:in [0]}),
:spec ...,
:value {1 "2"}}
user=>
The enhancement, though it is indeed a breaking change, should reduce radically the effort needed to write the code traversing specs along the {{:path}}.