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

欢迎!请查看 关于 页面以了解此功能的工作原理。

0
Spec
h3. 问题

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

h3. 详情

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

示例

* 一个 {{keyword?}} spec
** 使用 EDN,不应进行强制转换(它可以显示 Keywords)
** 使用 JSON,应该应用 String->Keyword 强制转换
** 使用基于 String 的格式(CSV、查询参数、...),应应用 String->Keyword 强制转换

* 一个 {{integer?}} spec
** 使用 EDN,不应进行强制转换(它可以显示数字)
** 使用 JSON,不应进行强制转换(它可以显示数字)
** 使用基于 String 的格式(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-arity {{conform*}} 和 {{clojure.spec/conform}} 支持在 Spec 协议中进行选择性合规,这两个函数都接受一个额外的用户提供的回调/访问者函数。如果提供了回调,它将在 Spec 的 {{conform*}} 调用中调用,以当前规范为参数,并将返回 {{nil}} 或一个 2-arity 合规器函数,该函数应用于实际合规。

实际的合规匹配器实现可以维护在第三方库中,例如 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通过协议进行扩展。第三方库可以有一个新的{{Conforming}}协议,具有3-arity的{{conform}},并在所有当前规范中添加对其的实现。目前这是不可能的。

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

16 答案

0

评论者:alexmiller

我认为我们并不想通过conformer将spec变成一个转换引擎,所以我怀疑我们可能对此不感兴趣。但是,我会让Rich来评估。

0

评论者:ikitommi

目前,Plumatic Schema是在边缘使用的主要工具。现在,人们开始转向Spec,如果必须为应用程序使用两个不同的建模库,那将对Clojure Web开发故事非常不好。如果spec不想通过conformer成为一个转换引擎,我希望有几项替代建议允许第三方编写这种扩展:将Spec作为记录/类型暴露而不是实现协议将完成这项工作?

0

评论者:kenrestivo

我可以理解为什么Clojure核心开发者可能不希望Spec支持这种类型的转换,但实际现实是有人必须这样做。如果它不在Spec本身中,就必须在基于它的库中完成,比如Tommi的库。

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

0

评论者:ikitommi

有关这个评估的最新消息?我将很乐意提供一个补丁或一个链接到修改后的 {{clojure.spec}} 以及包含3 arithmetic符合度示例的用法。

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_

感谢评论。我很乐意提供包含所需更改的补丁/示例存储库,希望能有助于决定这最终是否会在规范中。你怎么看?

以下是将初始规范集成到ring/http库的示例,使用spec-tools。目前,需要将规范包装到spec记录中才能启用3-arity符合度。这是我想要去除的样板代码。通过此更改,它应该对所有(第三方)规范工作良好。


(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

评论者:ikitommi

本意想在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 ::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
0

评论者:marco.m

你好,有什么消息吗?

0
_评论者:alexmiller_

我们至少要在进行下一批实现更改后才会查看这个问题。我仍然认为我们更有可能拒绝这个请求。
...