Good morning everyone 👋🏽 I think I've found a bug in spec, and before I submit it, would appreciate if someone could look over my shoulder and educate me if the bug is in my thinking...
Specifically, this is about validating a value against a spec defined as (s/and :spec/one :spec/two)
First, an example that works like how I expect.
;; expected behaviour
(s/def :working.test1/one string?)
(s/def :working/test1
(s/keys :opt [:working.test1/one]))
(s/def :working.test2/two number?)
(s/def :working/test2
(s/keys :opt [:working.test2/two]))
(s/def :working/test
(s/and :working/test1
:working/test2))
(def test-value
{:working.test1/one "a string"
:working.test2/two 1.23456})
(s/valid? :working/test1
test-value) ;; true
(s/valid? :working/test2
test-value) ;; true
(s/valid? :working/test
test-value) ;; true
and here, in combination with one of the and'ed specs being itself defined with s/or , an example that behaves unexpectedly:
;; unexpected behaviour
(s/def :broken.test1/one
;; this s/or is required to trigger the unexpected behaviour
(s/or :s string?
:n number?))
(s/def :broken/test1
(s/keys :opt [:broken.test1/one]))
(s/def :broken.test2/two
number?)
(s/def :broken/test2
(s/keys :opt [:broken.test2/two]))
(s/def :broken/test
(s/and :broken/test1
:broken/test2))
(def test-value-2
{:broken.test1/one "one"
:broken.test2/two 123})
(def test-value-3
{:broken.test1/one 1.0
:broken.test2/two 123})
(s/valid? :broken/test1
test-value-2) ;; true
(s/valid? :broken/test1
test-value-3) ;; true
(s/valid? :broken/test2
test-value-2) ;; true
(s/valid? :broken/test2
test-value-3) ;; true
(s/valid? :broken/test
test-value-2) ;; false ;; wait, what?!
(s/valid? :broken/test
test-value-3) ;; false ;; !!!With a little bit of debugging on my side, it appears that the value being validated (at some point) becomes the conformed version of the value; IE [:s "one"] is not valid against :broken.test1/one
I think "Returns a spec that returns the conformed value." is relevant here
also, https://ask.clojure.org/index.php/11230/why-does-s-conform-transform-data discusses this I think
Thank you for the link; It appears to describe nearly the same situation as what I've posted here. I don't understand Alex's reasoning there though, flowing vs non-flowing, I'm not sure what to make of it. But I guess the statement > There doesn't seem to be a good mechanism to express "this value satisfies a series of spec simultaniously" still holds true.
I see the docs for s/and is actually clear about this:
> Returns a spec that returns the conformed value. Successive
> conformed values propagate through rest of predicates.
😢
I vaguely recall seeing an example somewhere where people worked around this by supplying a custom conformer for the first spec. So you could specify a :broken.test1/one-non-conforming or something like. Not sure if this is a good idea though.
one tool you can use here is the (undocumented) s/nonconforming which can be wrapped around the s/or spec in :broken.test1/one to change the s/or conforming behavior
(s/def :broken.test1/one (s/nonconforming (s/or :s string? :n number?)))
or alternately, you could wrap that in the :broken/test s/and, depending on what you want to happen during conform of :broken.test1/onethis is a case where flowing conformed values through s/and yields confusing results. there are other cases where flowing the conformed values is a useful tool though (and really in hindsight I think non-flowing would be a better default behavior for s/and, although changing that would break a lot of things now)
Thank you for the explanation and the hint of s/nonconforming 🙏🏽