分享您的想法,请完成 2024 年 Clojure 状态调查!

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

0
Spec
h3. 问题

在运行时边界的验证中,支持多种交换格式时使用 {{clojure.spec}} 难度较大。

h3. 详细信息

目前,在 clojure.spec(alpha-14)中,验证器在创建 Spec 实例时附加到 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. 建议

在 Spec 协议中支持选择性验证,通过一个新的三参数 {{conform*}} 和 {{clojure.spec/conform}},两者都接受一个额外的用户提供的回调/访问者函数。如果提供了回调,则从 Specs 的 {{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
(断言 (= conformed-person (s/conform ::person string-person string-conforming-matcher)))


h3. 替代方案

支持这种方法的另一种选项是允许Spec通过协议进行扩展。第三方库可以创建一个带有3元 {{Conforming}} 协议的 {{conform}},并在所有当前规范中为其添加实现。目前这还不可能。

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

16 个回答

0

评论由:alexmiller 发表

我认为我们可能不希望将spec通过conformers变成一个转换引擎,所以我怀疑我们可能不感兴趣。然而,我会让Rich来评估。

0

评论由:ikitommi 发表

目前,Plumatic Schema 是在边界处使用的工具。现在,人们开始转向 Spec,如果人们需要在应用程序中使用两个不同的建模库,这将真的对 Clojure Web 开发故事非常不好。如果 Spec 不想通过 conformers 成为转换引擎,我希望我提出的替代建议允许第三方编写这种类型的扩展:通过将 Spec 作为记录/类型而不是具体化协议来公开,会起到作用吗?

0

评论由:kenrestivo 发表

我可以看出为什么 Clojure 核心开发者可能不希望 Spec 支持这种类型的强制类型转换,但实际的现实是,有人必须这么做。如果这不在 Spec 本身中,它将需要在基于它的库中进行,如 Tommi 的库。

这里的用例是:我有一个 YAML 的配置文件。我用 Clojure 库解析 YAML,将其转换为映射。现在我需要验证映射,但 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

西蒙,如果你正在尝试协调第三方规范,这将不起作用。这个建议的一个要点是,第三方可以为他们自己的规范编写自己的协调器,而无需重新定义这些规范。

0
_评论由:ikitommi_

感谢您的评论。我愿意提供一个补丁/示例仓库,包含对此所需的更改,希望这有助于决定是否将其添加到spec中。您怎么看?

以下是将spec初步集成到ring/http库中的示例,使用spec-tools。目前,需要将spec封装在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?) ;; 启用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
by

评论由:ikitommi 发表

本意是想在我分叉的clojure.spec中创建内部公关,但最终只创建了真正的DUMMY公关。无论如何,这里是它的样子

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_

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