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

0 votes
in Spec by

I am trying to write a spec for existing data. The moment I try to put one spec that contains an or into other it stops working. We are using confrom quite extensively so being able to get the or labels is important.

(s/def :a/b int?)
(s/def ::inner (s/or
                 :ident (s/tuple #{:a/b} :a/b)
                 :temp string?))
(s/def ::one int?)
(s/def ::two int?)
(s/def ::outer (s/and
            (s/keys :req [::inner])
                  :one (s/keys :req [::one])
                  :two (s/keys :req [::two]))))
(s/valid? ::outer {::inner [:a/b 1]
               ::one 1})


In the examle above if ::inner is just the spec under the :ident branch of the or then spec sees it as valid, but the moment there are two or specs involved it breaks.

Is there another way I'm supposed to construct specs such as these?

1 Answer

+2 votes
selected by
Best answer

::outer combines two s/keys specs with s/and. s/and flows conformed specs so the second spec will see the conformed value, not the original value. Also of importance, s/keys always conforms all registered keys in the spec. Combined these are leading to the failure of ::outer.

That is, when ::outer is conformed, it passes the input into the first spec and returns the conformed value {::inner [:ident [:a/b 1]]}, which is passed as input to the second spec. The second spec sees a registered key ::inner and tries to conform [:ident [:a/b 1]] against ::inner, which will fail.

I think there are probably a couple things possible here and without knowing the full story it's hard to say what's best. One is using s/merge instead of s/and - that will NOT pass the conformed value to each spec so simply replacing s/and with s/merge in your example will make things valid as expected. The caveat here is that you will only get the conformed value of the last spec in the merge. It's unclear if that's what you want.

Another option for the ::one/::two case is to use the or support built into s/keys :req to combine all of this into one spec:

(s/keys :req [::inner (or ::one ::two)])

Thank you.  s/merge gives me exactly the kind of shape I was looking for