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

欢迎!有关如何工作的更多信息,请访问关于页面。

0
Spec
h3. 问题

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

h3. 详情

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

示例

* 一个 {{keyword?}} 规范
** 与 EDN,不应执行任何强制(可以呈现关键字)
** 与 JSON,应用 String->Keyword 强制
** 与基于字符串的格式(CSV、查询参数等),应用 String->Keyword 强制

* 一个 {{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*}} 内部调用,并以当前规范作为参数,它将返回 {{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)))

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


h3. 替代方案

支持这种方式的另一种选择是允许用协议扩展Spec。第三方库可以有一个新的{{Conforming}}协议,它包含3个参数的{{conform}},并在所有当前规范中添加对此协议的实现。目前这还不可能。

[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支持此类强制转换,但实际情况是,将不得不有人这样做。如果不在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-tools将初始规格集成到ring/http库中的示例。目前,需要将规格包装到规格记录中,才能启用3价符合性。这是我希望移除的样板代码。通过此更改,所有(第三方)规格都应能够即插即用。


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

;; 启用3-arity符合性
(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?) ;; 启用3-arity符合性
(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])))

;; 发射一个ring处理器,具有输入和输出验证(& swagger文档)
;; 根据请求内容类型(例如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
by

评论者:ikitommi

原本打算在我的clojure.spec分叉中创建内部PR,但最终却为实际仓库创建了一个真正的DUMMY PR。无论如何,这里它来了

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

如果这走得更远,我很乐意最终将其提交到Jira的补丁中。

0
by
_发表评论者: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 ::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_

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