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))
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))))