2024 Clojure 状态调查! 中分享您的想法。

欢迎!请参阅关于 页面获取更多关于本站如何运作的信息。

0
Spec

问题



使用 {{clojure.spec}} 进行运行时边界验证支持多个交换格式是很难的。

详细信息



目前,在 clojure.spec (alpha-14) 中,合规器在创建 Spec 实例时附加到 Spec 上,并且每次进行合规化时都会被调用。在系统边界验证中,这不是很有用,在系统边界验证中,应根据运行时数据选择合规化/强制转换函数,例如交换格式。

示例



* 一个 {{keyword?}} spec
** 使用 EDN,不应该进行强制转换(它可以表示关键词)
** 使用 JSON,应该应用 String→Keyword 强制转换
** 使用字符串格式(CSV、查询参数等),应该应用 String→Keyword 强制转换

* 一个 {{integer?}} spec
** 使用 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}})则不起作用 - 不能用相同名称注册具有不同合规器的规范。

建议



在 Spec 协议中支持选择性合规化,使用新的 3 参数 {{conform*}} 和 {{clojure.spec/conform}},它们都接受额外的用户提供的回调/访问者函数。如果提供了回调,则从 Spec 的 {{conform*}} 内部调用它,将当前规范作为参数传递,它将返回 {{nil}} 或一个 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)))


替代方案



支持这种方式的另一种选择是允许规范通过协议进行扩展。第三方库可以通过拥有一个新的符合协议的3-参数conform方法和为所有当前规范添加实现来实现它。目前这还不可能。

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

16 条回答

0

备注:alexmiller

我认为我们并不想通过conformers将规范转变为一个转换引擎,所以我认为我们可能并不感兴趣。然而,我将留给Rich来评估。

0

备注:ikitommi

目前,Plumatic Schema是用于边界的工具。现在,人们开始转向Spec,如果必须使用两种不同的建模库来构建他们的应用程序,这对Clojure Web Development Story来说将是非常糟糕的。如果Spec不想通过conformers成为一个转换引擎,我希望能有替代建议,允许第三方编写此类扩展:将Spec作为记录/类型而不是归约协议公开?

0

备注:kenrestivo

我明白Clojure核心开发者可能不希望Spec支持这种类型的强制转换,但实际情况下,总有人必须这样做。如果不在Spec本身中实现,它将不得不在基于它的库(如Tommi的库)中实现。

此用例是:我有一个YAML格式的conf文件。我使用Clojure库解析YAML,将其转换为map。现在我需要验证这个map,但YAML不支持关键字,设置结构直接作为应用程序状态的一部分进入Component/Mount等,所以在读取配置后,启动应用程序的第一步运行s/conform是合理的。另外,其他合并配置的方法(例如环境变量、.properties文件等)也需要这种强制转换。

0

备注:ikitommi

关于对此进行评估的消息吗?我很乐意提供一个补丁或一个包含使用3参数匹配符的 {{clojure.spec}} 修改示例的链接。以下是一些想法: 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
_评论者:_

谢谢评论。我很乐意提供需要的补丁/示例存储库,希望能帮助决定这个是否最终能纳入规范。你怎么看?

以下是将spec-tools工具初步集成到ring/http库的示例,使用spec-tools。目前,需要将规范包装到spec记录中,以便启用3元组匹配符。这是我希望移除的样板代码。通过这次更改,所有(第三方)规范都应该能够出来就工作。


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

;; 启用3元组匹配符
(defn 枚举 [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?) ;; 启用 3 阶段合规性
(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

原本打算在我的clojure.spec分支中创建内部PR,但最终为实际仓库做了真正的DUMMY PR。无论如何,这里就是了

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

如果这个有进一步的进展,我很乐意将其最终化并创建一个补丁提交到Jira。

0
_评论者:_

欢迎评论。以下是对其进行测试的示例


(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 ::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

备注:ikitommi

有关这个问题有最新消息吗?

0

备注:ikitommi

相关链接 https://dev.clojure.org/jira/browse/CLJ-2251

0

评论者:marco.m

你好,有任何新消息吗?

0
_评论者:alexmiller_

我们至少要等到我们完成下一批实现更改后才会考虑这个问题。我仍然认为我们很可能会拒绝。
...