tl;dr - if not that’s ok but if it does then I want to show you strange behavior of using records and collections within spec/or
(I haven’t tested yet if it is the same for other combining macros)
Ok, It’s hard to explain in just few words what is this strange behavior so I think it’s best to show you an example
Let’s say we have a simple spec definition of person:
(s/def ::name string?)
(s/def ::age pos-int?)
(s/def ::person
(s/keys :req [::name ::age]))
(s/def ::people
(s/coll-of ::person))
definition of record Err
which we’ll use to represent our error
(defrecord Err [msg])
and some results definition:
one can be either person
or error
:
(s/def ::person-result
(s/or :ok ::person, :err any?))
and another is either people
(coll-of person) or error
:
(s/def ::people-result
(s/or :ok ::people
:err any?))
:err
is any?
because for now it really doesn’t matter what it actually is, result is the same.
Now we want to validate it (all data in this post will be correct representations of their specs).
First with just one person:
(let [person-ok {::name "John", ::age 42}
person-err (->Err "foo")]
(s/valid? ::person-result person-ok) ; returns true
(s/valid? ::person-result person-err) ; returns true
)
everything works fine here. Both person-ok
and person-err
are correct representations of ::person-result
spec (mostly because :err
is any?
so it’ll accept anything).
Magic starts when we want to validate people
which is collection.
(let [people-ok []
people-err (->Err "foo")]
(s/valid? ::people-result people-ok) ; returns true
(s/valid? ::people-result people-err) ; throws exception
; Unhandled java.lang.UnsupportedOperationException
; Can't create empty: user.Err
)
To make things better everything works fine if in our ::people-result
spec we first define :err
.
any?
is no longer enough to validate our error so let’s create a spec definition for it first:
(s/def ::msg string?)
(s/def ::err
(s/keys :req-un [::msg]))
(s/valid? ::err (->Err "foo")) ; returns true - looks fine
now we can move to working spec:
; :err is definied first unlike in ::people-result where :ok was first
(s/def ::people-result-2
(s/or :err ::err
:ok ::people))
(let [people-ok []
people-err (->Err "foo")]
(s/valid ::people-result-2 people-ok) ; returns true
(s/valid ::people-result-2 people-err) ; returns true
)
and again everything works.
It looks like a bug for me