请分享您的想法,参加2024 Clojure状态调查!

欢迎!请查看关于页面以了解更多关于如何使用本站的信息。

0
Spec
h3. 问题

在运行时边界验证中支持多种交换格式时使用{{clojure.spec}}很困难。

h3. 详细信息

目前,在clojure.spec(alpha-14)中,验证器在创建时附加到Spec实例上,并在每次匹配时被调用。这在系统边界验证中不是非常有用,在这种情况下,应根据运行时数据(例如,交换格式)选择匹配/强制转换函数。

示例

* a {{keyword?}} 规范
** 使用EDN,不应进行强制转换(它可以表示关键字)
** 使用JSON,应应用String→Keyword强制转换
** 使用基于字符串的格式(CSV,查询参数...),应应用String→Keyword强制转换

* a {{integer?}} 规范
** 使用EDN,不应进行强制转换(它可以表示数字)
** 使用JSON,不应进行强制转换(它可以表示数字)
** 使用基于字符串的格式(CSV,查询参数...),应应用String→Long强制转换

这里是一个更完整的示例


(s/def ::id integer?)
(s/def ::name string?)
(s/def ::title keyword?)
(s/def ::person (s/keys :opt [::id], :req-un [::name ::title]))

;; 这是我们以不同交换格式查看数据的方式
(def edn-person {::id 1, :name "Tiina", :title :boss})
(def json-person {::id 1, :name "Tiina", :title "boss"})
(def string-person {::id "1", :name "Tiina", :title "boss"})

;; 这是我们要的效果
(def conformed-person edn-person)


要用这种方法,现在需要手动为所有不同的交换格式创建带有不同验证器的新的边界规范。未限定的关键字可以映射到{{s/keys}}中以使用(例如,{{::title}} → {{::title$JSON}}),但若在边界上公开完全限定的键(如示例中的{{::id}})则不会这样做 - 无法以相同名称注册多个、不同验证方式的规范版本。

h3. 建议

在规范协议中支持选择性匹配,使用新的3函数参数{{conform*}}和{{clojure.spec/conform}},两者都接受额外的用户提供的回调/访问者函数。如果提供回调,它将在Spec的{{conform*}}内部使用当前规范作为参数调用,并返回一个2函数参数的匹配器函数,该函数应用于实际的匹配。

实际的匹配器实现可以维护在第三方库中,如spec-tools[1]。

使用方法如下


;; edn
(assert (= conformed-person (s/conform ::person edn-person)))
(assert (= conformed-person (s/conform ::person edn-person nil)))

;; json
(assert (= conformed-person (s/conform ::person json-person json-conforming-matcher)))

;; string
(assert (= conformed-person (s/conform ::person string-person string-conforming-matcher)))


h3. 替代方案

支持这种功能的另一个选项是允许使用协议扩展Spec。第三方库可以拥有一个符合3个参数的新{{Conforming}}协议,并在所有当前Spec中添加其实例。目前这是不可能的。

[1] https://github.com/metosin/spec-tools

16 个答案

0

评论者:alexmiller

我认为我们不会对通过conformers将spec转换成转换引擎感兴趣,所以我怀疑我们可能也不感兴趣。然而,我将让Rich来判断。

0

评论者:ikitommi

目前,Plumatic Schema是用于边界的工具。现在,人们开始转向Spec,如果一个人不得不使用两个不同的建模库来构建应用,那么这对Clojure Web开发故事将是非常不利的。如果Spec不想通过conformers成为转换引擎,我希望提供一个替代建议:允许第三方编写这种类型的扩展:将Spec暴露为记录/类型而不是具体化的协议会起作用?

0

评论者:kenrestivo

我可以理解Clojure核心开发者可能不想支持这种类型的强制转换,但实际现实是,某个人将不得不这样做。如果Spec本身不支持,它将不得不由基于它的库来实现,如Tommi的库。

这里的用例是:我有一个YAML格式的配置文件。我使用Clojure库解析YAML,将其转换为map。现在我需要验证这个map,但YAML不支持关键词,例如,并且设置结构直接作为应用状态的一部分进入Component/Mount等,因此,在读取配置后启动应用的第一步运行s/conform对此很有意义。再加上合并配置的其他方法(环境变量、.properties文件等),这种强制转换将必不可少。

0

评论者:ikitommi

关于评估这个问题的进展有何消息?我很乐意提供一个补丁或提供一个修改过{{clojure.spec}}的链接,其中包含使用3个参数一致性的示例。一些思考: http://www.metosin.fi/blog/clojure-spec-as-a-runtime-transformation-engine/

0

评论者:alexmiller

Rich 还没有看过它。我猜想我们可能对这个更改不感兴趣。虽然我认为文章中描述了一些有趣的问题,但我并不认同那里采取的大多数方法。

0

评论由:sbelak

为什么不直接使用s/or (或s/alt)然后根据标签进行分发。比如

`
(s/def ::id (s/and (s/or :int integer?

                     :str string?)
               (s/conformer (fn [[tag x]]
                              (case tag
                                :int x
                                :str (Integer/parseInt x))))))

`

我经常在使用 https://github.com/sbelak/huri 的过程中使用这个模式,并且通过一点语法糖,它可以工作得相当好。

0

评论由:imre

Simon,如果你试图一致地遵循第三方规范,这将不起作用。这个建议的一个要点是,第三方可以为现有的规格编写自己的格式化程序,而不必重新定义这些规格。

0
_评论由:ikitommi_

感谢评论。我很乐意提供一个补丁/示例仓库,其中包含为完成这项工作所需的更改,希望能够决定这能否最终包含在规范中。你怎么认为呢?

以下是将spec集成到ring/http库中的示例,使用spec-tools。目前,需要将规格包装成spec记录以启用3参数一致性。我希望能移除这个样板代码。有了这个更改,所有(第三方)规格都应该能直接工作。


(require '[compojure.api.sweet :refer :all])
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])

;; 启用3参数一致性
(defn enum [values]
  (st/spec (s/and (st/spec keyword?) values)))

(s/def ::id int?)
(s/def ::name string?)
(s/def ::description string?)
(s/def ::size (enum #{:L :M :S}))
(s/def ::country (st/spec keyword?) ;; to enable 3-arity conforming
(s/def ::city string?)
(s/def ::origin (s/keys :req-un [::country ::city]))
(s/def ::new-pizza (st/spec (s/keys :req-un [::name ::size ::origin] :opt-un [::description])))
(s/def ::pizza (st/spec (s/keys :req [::id] :req-un [::name ::size ::origin] :opt-un [::description])))

;; 生成带有输入和输出验证(以及Swagger文档)的ring处理器
;; 根据请求的内容类型(例如json/edn)选择符合要求,并从映射中移除额外键
(context "/spec" [])
  (resource)
    {:coercion :spec}
     :parameters {:body-params ::new-pizza}
     :responses {200 {:schema ::pizza}}
     :post {:handler (fn [{new-pizza :body-params}])}
                   (ok (assoc new-pizza ::id 1))}})
0
通过

评论者:ikitommi

本意是为我fork的clojure.spec创建内联PR,但最后还是在真正的仓库里做了一个假的PR。不管怎样,这里就是它

https://github.com/clojure/spec.alpha/pull/1

如果这个方向有进一步的发展,很高兴使它在Jira中最终确定并创建补丁。

0
通过
_评论由:ikitommi_

欢迎提出意见。这里是一个样本测试


(deftest conforming-callback-test)
  (let [string->int-conforming]
        (fn [spec])
          (condp = spec)
            int? (fn [_ x _])
                   (cond)
                     (int? x) x
                     (string? x) (try)
                                   (Long/parseLong x)
                                   (catch Exception _)
                                     ::s/invalid)
                     :else)
                     nil))]

    (testing "没有符合要求的回调")
      (is (= 1 (s/conform int? 1)))
      (is (= ::s/invalid (s/conform int? "1"))))

    (testing "有符合要求的回调")
      (is (= 1 (s/conform int? 1 string->int-conforming)))
      (is (= 1 (s/conform int? "1" string->int-conforming))))))
0
通过

评论者:ikitommi

初始工作作为一个补丁。

0
by

评论者:ikitommi

关于这个有任何消息吗?

0
by
0
by

评论者:marco.m

你好,有什么新消息吗?

0
by
_评论者:alexmiller_

我们至少要等下一批实现更改做好后再来看这个。我仍然认为我们最有可能拒绝这个。”
...