Welcome! Please see the About page for a little more info on how this works.

0 votes
in Spec by
edited by

I need to spec map-entry-like vectors whose keys are themselves spec-ed. Each vector looks like this:

[:my.spec-ed.key/foo 42]

The specific keys used vary, and I am hoping to write a predicate that would dynamically spec any map-entry-like vector based on its key the way maps are spec-ed entry by entry via (s/keys).

In this case let's say the key is spec-ed like this:

(s/def :my.spec-ed.key/foo int?)

The best I can come up with to spec the map-entry-like vector is this:

(s/def :my.spec-ed.key/pair (s/and vector? #(s/valid? (first %) (second %))))

This works, but I don't like it because (s/explain) is not as good when something goes wrong. For example, (s/explain :my.spec-ed.key/foo "foo") is informative:

"foo" - failed: int? spec: :my.spec-ed.key/foo

Conversely, (s/explain :my.spec-ed.key/pair [:my.spec-ed.key/foo "bar"]) doesn't end up explaining what really went wrong (that I passed a string instead of an int), I have to go look up the original spec to find that out:

[:my.spec-ed.key/foo "bar"] - failed: (valid? (first %) (second %)) spec: :my.spec-ed.key/pair

Does anyone have any ideas for a better way to spec my map-entry-like vectors? For what it's worth, these vectors don't actually come from maps, and are not actually going to maps, so I can't use any of the spec functions designed to work on maps specifically.

2 Answers

+1 vote
by
selected by
 
Best answer

For fixed length stuff like this, tuple is usually best...

(s/tuple #{:my.spec-ed.key/foo} :my.spec-ed.key/foo)

by
Thank you so much for taking the time to answer Alex, quickly, and on a Saturday night no less.

I am afraid I did not communicate my problem well enough and have edited my question.

I am looking for a predicate that would work with an arbitray, spec-ed key, just as spec works with map keys and (s/keys). The example I gave accomplishes this, albeit imperfectly, using s/valid? within the predicate. With your solution, unless I'm missing something, for each of my dozens of key specs I would need to write an accompanying tuple spec. Or no?
by
You would need to do it with every spec, but macros are a great tool for removing that kind of tedium, so I don't think that's necessarily a limiting factor.

Another option would be to use s/keys* which is intended for kv args is a sequential collection. Something like `(s/keys* :req [:my.spec-ed.key/foo])`. If you want to constrain it to just vectors and a single pair, you can use `(s/and vector? #(= 2 (count %)) (s/keys* :req [:my.spec-ed/key]))`.
0 votes
by
edited by

Thanks to @alexmiller's answer I was able to home in on this specific solution:

(s/def :my.spec-ed.key/pair (s/and vector? #(= 2 (count %)) (s/keys*)))

This will validate against any previously spec-ed key.

Of course as Alex points out one can always write a macro, this worked for me, it's a macro that creates a corresponding -tuple spec for each spec in a namespace, and then (s/or)s them all together into one master "pairs" spec. The idea is you'd run it just once per ns after the other specs are defined (it does not filter out the specs it itself makes):

(defmacro pairs-spec
  [spec-id]
  (let [spec-ns (namespace spec-id)
        ors (->> (s/registry)
                 (map first)
                 (filter #(= spec-ns (namespace %)))
                 (mapcat (fn [ky] `(~(keyword spec-ns
                                              (str (name ky)
                                                   "-tuple"))
                                    (s/tuple #{~ky} ~ky)))))]
    `(s/def ~spec-id (s/or ~@ors))))

(pairs-spec :my.spec-ed.key/pair)

The latter does not give nice results from (s/explain) because it builds up one big (s/or) statement, with one entry for each previously spec-ed key in the namespace. Because of this, and because I don't want to have to worry about whether all my keys have been spec-ed at the time the macro runs, I am preferring the (s/keys*) solution above.

Thanks for the help!!

...