Clojure spec如何局部覆盖map定义中指定字段的生成器
解决方案
Clojure Spec 原生支持覆盖指定spec的生成器,有两种常用实现方式可以满足你的需求:
方式1:直接使用s/gen的重载参数(兼容性最好)
s/gen支持传入第二个参数作为生成器覆盖映射,键为要覆盖的spec名称,值为自定义生成器,无需额外封装:
;; 示例:id池大小为5,所有生成的customer的id都从这个池子里选取 (s/gen ::customer {:customer/id (gen/let [id-pool (gen/vector (s/gen :customer/id) 5)] (gen/element id-pool))})
你也可以把id池大小作为参数动态传入:
(defn customer-generator [id-count] (s/gen ::customer {:customer/id (gen/let [id-pool (gen/vector (s/gen :customer/id) id-count)] (gen/element id-pool))}))
方式2:封装动态绑定宏(实现你想要的with-generators语法)
如果你需要在多处代码块内统一覆盖生成器,可以利用Spec内置的动态变量*gen-overrides*封装宏:
(defmacro with-generators [overrides & body] `(binding [clojure.spec.alpha/*gen-overrides* (merge clojure.spec.alpha/*gen-overrides* (hash-map ~@overrides))] ~@body))
封装完成后就可以用你期望的语法调用:
(let [id-count 5] (with-generators [:customer/id (gen/let [id-pool (gen/vector (s/gen :customer/id) id-count)] (gen/element id-pool))] ;; 代码块内所有调用s/gen ::customer的地方都会使用覆盖后的id生成器 (s/gen ::customer)))
注意:
*gen-overrides*变量在Clojure Spec 1.2.0及以上版本提供,如果你使用的是更早版本的Spec,建议使用第一种传参方式。
补充说明
你原有实现存在逻辑问题:直接对s/gen返回的生成器对象执行assoc是错误的,assoc应该作用于生成器产出的customer实例上,而不是生成器本身,使用原生的生成器覆盖机制可以完全避免这类问题。
内容的提问来源于stack exchange,提问作者Tim Clemons
相关产品推荐
相关产品推荐

