clojure-spec

About: http://clojure.org/about/spec Guide: http://clojure.org/guides/spec API: https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html
Kira McLean 2020-12-01T19:48:42.441500Z

What’s the best to way to list the keys that a map is missing? E.g. with a problem like this for a specced map, what do you guys do to collect those key-names? I’m looking at using spec to validate data in a web app so ultimately I’d like to translate the missing keys into a message like “<key-name> is required” but I’m not sure what’s the best way to get a hold of the names of the missing keys.

{:path [],
  :pred (clojure.core/fn [%] (clojure.core/contains? % :key-name)),
  :val
  {},
  :via [,,,],
  :in []}

Kira McLean 2020-12-01T19:56:25.443700Z

I was looking through expound to try to see how they do it.. it looks like the core of it is https://github.com/bhb/expound/blob/1c0d78570be3865eab8e69c1b568c4e7acee5bd8/src/expound/printer.cljc#L187-L194, where form is a :predhttps://github.com/bhb/expound/blob/1c0d78570be3865eab8e69c1b568c4e7acee5bd8/src/expound/alpha.cljc#L317.. but I don’t understand how that :expound.spec/contains-key-pred works..

Kira McLean 2020-12-01T19:59:55.445Z

I don’t see how it returns the name of the failed key.. when I try something similar it just returns ::s/invalid, which seems like what I would expect.

bbrinck 2020-12-01T20:31:12.445300Z

@kiraemclean Expound is just parsing the s-expression of the pred. The trick is that expound will take the pred e.g. something like (clojure.core/fn [%] (clojure.core/contains? % :expound.printer/foo)) And then it will throw away the clojure.core/fn [%] part with the (nth form 2) on line 188

bbrinck 2020-12-01T20:31:29.445500Z

Then it uses spec to parse the s-expression to pull out the keyword of the spec

bbrinck 2020-12-01T20:32:38.445700Z

Here’s a little script to show each step. It’s just an example to show how Expound figures out each part of the :pred

(let [first-problem (first (::s/problems (s/explain-data ::some-spec {})))]
    {
     :pred (:pred first-problem)
     :part-of-pred (nth (:pred first-problem)  2)
     :match (s/conform :expound.spec/contains-key-pred (nth (:pred first-problem)  2))
     }
    )
  ;; This returns:
  #_ {:pred (clojure.core/fn [%] (clojure.core/contains? % :expound.printer/foo)),
   :part-of-pred (clojure.core/contains? % :expound.printer/foo),
   :match [:simple {:contains clojure.core/contains?, :arg %, :kw :expound.printer/foo}]}

bbrinck 2020-12-01T20:33:53.446Z

Does that help?

Kira McLean 2020-12-01T20:45:26.446200Z

ah cool, yeah that does make sense. Thank you! So is that how you’d recommend going about getting a hold of those keys? Is there a simpler way to just get spec to tell me which req keys are missing?

bbrinck 2020-12-01T20:49:34.446500Z

@kiraemclean Unfortunately, as of spec1, that’s the way I know of to get the missing keys. Things may change in spec2, but I don’t know of any specific plans for this.

bbrinck 2020-12-01T20:52:21.446700Z

It’d be really nice if spec2 included both the missing key and the fully-qualified key in the case of keys included via :req-un.

bbrinck 2020-12-01T20:53:09.446900Z

(right now if you use :req-un, sometimes Expound will warn &lt;can't find spec for unqualified spec identifier&gt; because it doesn’t know what spec you meant by the kw :foo)

bbrinck 2020-12-01T20:55:32.447100Z

@kiraemclean This is apparently how Phrase works as well https://cljdoc.org/d/phrase/phrase/0.3-alpha4/doc/examples#required-keys

Kira McLean 2020-12-01T21:52:12.447400Z

This is really helpful, then. Thanks for all the info!