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

+2 votes
in Spec by
edited by

What would be the preferred way to select one spec from multi-spec? E.g.

(defmulti entity :entity)

(s/def ::e1
  (s/keys :req [:e1/attr1
                :e1/attr2
                :e1/attrN]))

(defmethod entity :e1 [_] ::e1)

(s/def ::e2
  (s/merge
   ::e1
   (s/keys :req [:e2/attr1
                 :e2/attr2
                 :e2/attrN])))

(defmethod entity :e2 [_] ::e2)

(s/def ::entity (s/multi-spec entity :entity))

How to spec a function that accepts only ::e2 entity as an argument? ... or how to generate only ::e2 entity values?

Can't do (s/cat :e2 ::e2) or (s/gen ::e2) because ::e2 doesn't have the required :entity tag. Also, can't add :req-un [:e1/entity] and :req-un [:e2/entity] to ::e1 and ::e2 because it would clash in the s/merge.

The simplest way I've found is to use s/and, e.g:

(gen/generate (s/gen (s/and ::entity ::e2)))

But it is still hard to generate data for functions that accept homogeneous collection of any entity:

(s/and (s/coll-of ::entity :min-count 1)
       #(->> % (map :entity) (apply =)))

It would be easier if you could pass the entity tag to select the specific entity spec or it's generator, e.g. generate random entity and based on it's entity tag generate other elements.

In other words, I am looking for a way to get ::e2 entity spec via :e2 tag somehow, without maintaining a global helper map...

Or is there any other solution?

1 Answer

0 votes
by

I've was running into this and ended up not using multimethods for this use case. Instead, I'll define the specs for each of the possible sub types (including setting a constant value for, in your case, :entity), and then creating an overall spec using s/or. This way I can use the subspecs if needed, or if I want to refer to the overall set I can use the s/or spec

...