I’m trying to spec a map which I thought was homogeneous (i.e. key type matching a spec -> value type matching a spec), so I used map-of. Now it turns out that these maps can occasionally have other entries where the key is a constant keyword and then the value is something different to the other value type. I can’t figure out the best way to spec this - any advice?
it's a bit cumbersome but you can spec the map as a collection of tuple (entry) types
https://www.cognitect.com/blog/2017/1/3/spec-destructuring is a complicated example of this
I see, so I’d define two s/tuple types, and then a coll-of :kind map? using s/or over the two types?
(s/def ::map-bindings
(s/every (s/or :mb ::map-binding
:nsk ::ns-keys
:msb (s/tuple #{:as :or :keys :syms :strs} any?)) :into {}))
(s/def ::map-special-binding
(s/keys :opt-un [::as ::or ::keys ::syms ::strs]))
(s/def ::map-binding-form (s/merge ::map-bindings ::map-special-binding))
is kind of the crux of that - an s/merge of special keys via s/every with an s/keysOk, thanks for the pointer, I think I’ll need some time to digest that but I can figure it out from there.
that example is probably more complicated than what you need but that's the main idea
Can I s/merge an s/map-of with an s/keys?
not successfully unless the map-of uses keyword keys
It doesn’t, unfortunately.
it's merging the requirements of the two specs so any data has to satisfy both
note that the tuple side (s/every of s/tuple) has an :msb branch that is effectively matching all of the known kws with any?
and then the s/keys side is s/opt so it's just ignoring anything else
So when I do an s/merge, every entry has to match both sides of the merge? I’m not sure I understood that.
yes
you are merging the requirements of the spec
so, it's kind of like s/and, but sub specs are effectively checked in parallel whereas s/and flows data through and checks sub specs serially
this also has implications for s/conform but you may not care about that
Here’s my little test case:
(s/def ::normal-key string?)
(s/def ::normal-val string?)
(s/def ::constant (s/map-of ::normal-key ::normal-val))
(s/def ::foo (s/coll-of string? :kind vector?))
(s/def ::special-case (s/keys :req-un [::foo]))
(s/conform ::constant {"foo" "bar"})
(s/conform ::special-case {:foo ["test"]})
(s/def ::mixed (s/every (s/or :constant (s/tuple ::normal-key ::normal-val)
:outlier (s/tuple #{:foo} ::foo))))
(s/def ::end-result (s/merge ::mixed ::special-case))
(s/conform ::end-result {"foo" "bar"})
(s/conform ::end-result {:foo ["test"]}))So I started with a map of string to string using ::constant. Then I realised I need ::special-case too. However my ::end-result can’t conform {"foo" "bar"}
(s/explain end-result {“foo” “bar”}) {“foo” “bar”} - failed: (contains? % :foo) spec: :cursive.extensions.specs/special-case
req-un has to be opt-un there
as string/string entries won't match that
Ah, of course - that works, thanks!