Fork me on GitHub
#clojure-spec
<
2018-09-18
>
esp106:09:44

Is there a way to have predicates return more detailed error messages? I have some complex predicates that operate on subtrees to do things like make sure all branches of the subtree are of the same type. Instead of just having the predicate return false at the root, I'd like to be able to have the predicate provide detailed path information about where within the subtree it detected an error. It would be great if I could have the predicate behave like the built-in spec functions so it could push this detailed information into the explain-data format. Is this possible to do in any supported way without reverse engineering spec internals?

misha06:09:58

decomposing your predicates in simpler ones, and recomposing them with spec's s/and, s/or and s/*?+ comes to mind. former will bring you closer to "can understand the exact issue from a predicate form w/o error messages", latter will give you exact paths in s/explain-data. @esp1

esp107:09:08

Thanks for the reply @misha! I don't think decomposing the predicate will really work for me though; let me give some more detail about why. The structure I'm spec'ing is essentially a lisp-like expression syntax. For instance I want to validate that in the expression [ := :Foo/bar {:a 1, :b {:c "blah"} } ] that the arguments to the := comparison are both of the same type. The predicate to do this looks up the type schema I have associated with the :Foo/bar argument (which looks something like {:a :int, :b {:c :int} } and checks if the map argument matches this structure. In this example the type schema says that the value of :c should be an integer, but the value is actually a string, so validation should return an error. I'd like the error message to report the expected and actual types, along with the fact that the type mismatch error occurs in the second argument value at the path [:a :c]. If I just have a boolean predicate I'd just get a spec error indicating that a type mismatch error occurred at the root level. The ability to associate error messages with expound is nice - I hadn't realized you could do that. I suppose I could use a custom printer in expound that essentially walks the argument subtree again once a spec error occurs and prints out the error detail. This feels kinda hacky tho. I was hoping I could make the predicate such that it could provide detailed error info directly into the structure returned by s/explain-data.

Timo Freiberg12:09:04

can i use a s/or spec but have conform only return the value instead of [:key val]?

Timo Freiberg13:09:57

not sure how ugly this is, but i "solved" it by wrapping the s/or in an s/and and putting a (s/conformer second) at the end

Timo Freiberg13:09:45

so

(s/and
 (s/or :a #{"a"} :b ::b-spec)
 (s/conformer second))

taylor13:09:15

(s/conform (s/nonconforming (s/or :i int? :s string?)) "foo")
=> "foo"

taylor13:09:30

@UA11M92KE s/nonconforming is what you want, I think

Timo Freiberg13:09:36

ah, awesome! that makes it easier

misha13:09:41

@esp1 well, sounds like you already have destructured spec for :Foo/bar, which should give you detailed paths to errors. You just need to figure out how to validate against it w/o enclosing it in an opaque predicate

misha13:09:39

number of layers and complexity of this would depend on the diversity of the expressions you want to support.

misha13:09:11

way simpler version might be just

(defn validate-expr [expr]
  (s/assert (s/tuple keyword? any? any?) expr)
  (let [[op spec y] expr]
    (s/assert spec y)))

misha13:09:09

I assume, this is not the case, hence extra layer of case or multimethod

jrychter18:09:55

Looking for advice — I've been looking to eliminate some verbosity in my specs and came up with this:

(defn sorted-set-of [spec]
  (s/and (s/coll-of spec)
         (s/conformer (partial apply sorted-set))))
Will this bite me in the future? (using a defn to define specs) Will it play well with generators once I get to those?

Alex Miller (Clojure team)19:09:39

You don’t need that - look into :into in coll-of

Alex Miller (Clojure team)19:09:24

(s/coll-of int? :into (sorted-set)) does what you want

jrychter19:09:50

Oh, thank you, I didn't know about :into! Seems like it's exactly what I need. Trying it out.

Alex Miller (Clojure team)19:09:39

Will work with gen automatically too