Fork me on GitHub
#clojure-spec
<
2022-09-09
>
Cam Saul20:09:03

What's the easiest way to do something like a "soft cut" e.g. core logic conda to lock a regex spec into validating an optional argument with a specific spec if it meets some condition, and failing otherwise? Suppose I have a spec like this:

(s/def ::my-map
  (s/keys :opt-un [...]))

(s/def ::my-spec
  (s/cat :my-map (s/? ::my-map) :options (s/* (s/cat :k keyword? :v any?))))
If I pass in a map that fails the ::my-map spec I get an error like
identity - failed: Extra input in: [0] spec: ::my-spec
because it then tries to use that argument as an option. How can I force it to validate maps with the ::my-map spec? I've tried stuff like (s/& map? ::my-map) but that doesn't work either

Alex Miller (Clojure team)21:09:58

just checking - you have an optional followed by kv pairs, and with what input do you get this error? something that's neither a map or keyword as first arg?

Alex Miller (Clojure team)21:09:48

I'm not sure if there's an easy alternative for you - seems like you could possibly treat this as two specs based on even/odd arg count. it's an unusual usage but you could probably build an s/multi that switched based on that and used different specs

Cam Saul21:09:01

I ended up doing something like

(s/& 
 (s/cat :my-map (s/? map?) :options (s/* (s/cat :k keyword? :v any?)))
 (s/keys :opt-un [:my-map])
which works okay I guess but is a little icky I think

Cam Saul21:09:06

Here's a more detailed example including something with neither a map nor keyword first arg. I just get an extra input error

(s/def ::k
  integer?)

(s/def ::my-map
  (s/keys :req-un [::k]))

(s/def ::my-spec
  (s/cat :my-map (s/? ::my-map) :options (s/* (s/cat :k keyword? :v any?))))

(s/explain-str ::my-spec [{:k 100}])
(s/explain-str ::my-spec [{:k 100} :x :y])
;; => "Success!\n"

(s/explain-str ::my-spec [100])
;; => "(100) - failed: Extra input in: [0] spec: ::my-spec\n"

(s/explain-str ::my-spec [{}])
;; => "({}) - failed: Extra input in: [0] spec: ::my-spec\n"

Cam Saul21:09:25

defmulti is actually one real-world example of where you have a usage like this. The metadata attribute map is optional and after that it supports dispatch-fn as a positional argument and then a few optional key-value arguments like :default and :hierarchy if I recall correctly.

Cam Saul21:09:04

This came up for me because I was working on a def macro that did something similar (supports and optional attribute map and keyword options) and I tried to tweak the spec to validate one of the keys in the attribute map specifically and it made things fail with confusing errors. In the example above I want the last failure to tell me it's failing because :k is not an integer?

Cam Saul21:09:10

This is the working version I came up with:

(s/def ::my-spec
  (s/& (s/cat :my-map (s/? map?) :options (s/* (s/cat :k keyword? :v any?)))
       (s/keys :opt-un [::my-map])))

(s/explain-str ::my-spec [{}])
=> "{} - failed: (contains? % :k) in: [:my-map] at: [:my-map] spec: ::my-map\n"
I was wondering if maybe there was some easier way to do this, e.g. some way to define my so-called "soft cut" inline rather than having to wrap the whole thing in s/&

thiru21:09:19

If I were to start using Spec today is it recommended to use Spec 1 or Spec 2? Also, my codebase is Clojure 1.8 and can't upgrade. The only issue I see with this is that ident? is missing but that's easy enough to add. Is there any strong reasons to avoid using Spec with Clojure versions before 1.9?

Alex Miller (Clojure team)21:09:08

spec 2 has not been released and is not under active dev at this moment

1
Alex Miller (Clojure team)21:09:54

I think someone did have a backport of spec at one point (we are not maintaining anything official though)

thiru21:09:23

Excellent, thank you for the super fast response Alex! 🙂