Search code examples
clojureclojure.spec

How to generate the same value for two different paths in spec?


I'm trying to learn how to use overrides with s/gen.

I have a ::parent map which contains a ::child map. Both parent and child have keys in common. The requirement is that the keys have the same value between parent and child, e.g. {:a 1 :b 2 :child {:a 1 :b 2}. I know this seems redundant, but the problem domain requires it.

The code below generates examples, but the requirement above is not met.

Is there a way to use the same generated value in two locations?

(ns blah
  (:require [clojure.spec.alpha :as s]
            [clojure.spec.gen.alpha :as gen])) 

(s/def ::a (s/int-in 1 5))
(s/def ::b (s/int-in 1 6))

(s/def ::child
  (s/keys :req-un [::a ::b]))

(defn- parent-gen []
  (let [a #(s/gen ::a)
        b #(s/gen ::b)]
    (s/gen ::parent-nogen
           ; overrides map follows
           {::a a ::b b
            ::child #(s/gen ::child
                            ; another overrides map
                            {::a a ::b b})))

(s/def ::parent-nogen
  (s/keys :req-un [::a ::b ::child]))

(s/def ::parent
  (s/with-gen ::parent-nogen parent-gen))

(gen/sample (s/gen ::parent))

Solution

  • You can do this with test.check's fmap:

    (s/def ::a (s/int-in 1 5))
    (s/def ::b (s/int-in 1 6))
    (s/def ::child (s/keys :req-un [::a ::b]))
    (s/def ::parent (s/keys :req-un [::a ::b ::child]))
    (gen/sample
      (s/gen ::parent
             {::parent ;; override default gen with fmap'd version
              #(gen/fmap
                (fn [{:keys [a b child] :as p}]
                  (assoc p :child (assoc child :a a :b b)))
                (s/gen ::parent))}))
    =>
    ({:a 1, :b 2, :child {:a 1, :b 2}}
     {:a 2, :b 2, :child {:a 2, :b 2}}
     {:a 1, :b 1, :child {:a 1, :b 1}}
     {:a 3, :b 2, :child {:a 3, :b 2}}
     {:a 2, :b 4, :child {:a 2, :b 4}}
     {:a 4, :b 4, :child {:a 4, :b 4}}
     {:a 3, :b 3, :child {:a 3, :b 3}}
     {:a 4, :b 4, :child {:a 4, :b 4}}
     {:a 3, :b 4, :child {:a 3, :b 4}}
     {:a 3, :b 4, :child {:a 3, :b 4}})
    

    fmap takes a function f and a generator gen, and returns a new generator that applies f to every value generated from gen. Here we pass it the default generator for ::parent, and a function that takes those parent maps and copies the appropriate keys into the :child map.

    If you want this spec to enforce that equality (besides just generation), you'll need to add an s/and to the ::parent spec with a predicate to check that:

    (s/def ::parent
      (s/and (s/keys :req-un [::a ::b ::child])
             #(= (select-keys % [:a :b])
                 (select-keys (:child %) [:a :b]))))
    

    Edit: here's another way to do the same thing with gen/let that allows for a more "natural" let-like syntax:

    (gen/sample
      (gen/let [{:keys [a b] :as parent} (s/gen ::parent)
                child (s/gen ::child)]
        (assoc parent :child (assoc child :a a :b b))))