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

+3 votes
in Libs by

I've been trying out Clojure spec and I often face situations in which I have to come up with a custom predicate function. Then sometimes these predicate functions require a custom generator. How can I share this predicate+generator bundle between different attributes?

(Also not so important but, can I call this bundle of predicate-fn + generator a spec?)

Suppose I have the custom predicate:

   (defn local-date? [x]
      (instance? LocalDate x))

and then I define a brand new generator function for it:

(defn local-date-generator []
  (gen/fmap #(LocalDate/ofInstant (Instant/ofEpochMilli %) (ZoneId/of "UTC")) 

I can go on and define an attribute like:

(s/def :person/birth-date (s/with-gen local-date local-date-generator))

but then I would have to define this s/with-gen every time I create a LocalDate attribute.

How can I abstract that away?

I've come to the conclusion that the right way to do that would be to define an attribute in a namespace (e.g myprojec.specs) in which I create an attribute with that spec

(s/def ::local-date (s/with-gen local-date local-date-generator))

and then just re-use it elsewhere

(s/def :person/birth-date :myproject.specs/localdate)

What are the alternatives?

1 Answer

+3 votes

I think you're on the right path for sure. I think maybe a useful intermediate point is that you could def (not s/def) the (s/with-gen local-date local-date-generator) - that is a spec+custom generator, named in a var. That's somewhat orthogonal to whether you want to define a named attribute (users of this spec+generator may want to put it on other attributes). Really, they can do either, by referring to spec var or by aliasing to a named attribute you create. The tradeoffs are subtle and may change with spec 2 in the future.

With respect to bundling multiple things - namespaces are the way to bundle either vars referring to specs, or consolidate the loading of attributes. You may want to separate those two if you want to give users the maximum flexibility (a namespace for specs+generators, a namespace that defines attributes).

There's a lot of options, and a fair amount of "it depends", so sorry I can't give more prescriptive advice.

hey Alex! def'ing doesnt work, apparently it doesnt carry the generators forward. So I am currently creating a proxy spec with the generators and referencing it from other specs. Is this expected or should it be tagged as a bug?

(def local-date-time? #(instance? LocalDateTime %))
(def local-date? #(instance? LocalDate %))

(defn gen-local-date []
  (generators/fmap #(-> (Instant/ofEpochMilli %)
                        (LocalDateTime/ofInstant (ZoneId/of "UTC"))

(defn gen-local-date-time []
  (generators/fmap #(-> (Instant/ofEpochMilli %)
                        (LocalDateTime/ofInstant (ZoneId/of "UTC")))

(def date-spec (s/with-gen local-date-time? gen-local-date-time))

(s/def ::some-date date-spec)

(generators/generate (s/gen ::some-date))