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

欢迎!请参阅关于页面了解更多此功能的信息。

0
Spec
h3. 问题

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

h3. 详细信息

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

示例

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

* a {{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}})将不会工作 - 不能为具有相同名称的规范注册多个不同验证器的版本。

h3. 建议

在 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)))


h3. 替代方案

支持这种功能的另一种方法是允许规格通过协议进行扩展。第三方库可以创建一个新的{{Conforming}}协议,具有3-arity的{{conform}}函数,并为所有现有规范添加对此协议的实现。目前这还不可能。

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

16 答案

0
通过

该评论由:alexmiller 发布

我认为我们不希望通过conformers将spec转换为转换引擎,所以我怀疑我们可能对此不感兴趣。然而,我将把它留给Rich来评估。

0
通过

该评论由:ikitommi 发布

目前,Plumatic Schema是与边界使用的工具。现在,人们开始转向Spec,如果需要为应用程序使用两个不同的建模库,这将真的很糟糕。如果Spec不想通过conformers成为转换引擎,我希望提出另一种建议:允许第三方编写此类扩展:将Spec公开为记录/类型而不是具体的协议将完成这项工作?

0
通过

该评论由:kenrestivo 发布

我可以理解Clojure核心开发者可能不想在Spec中支持这类强制,但实际情况下,总有人会这么做。如果Spec自身不支持,那么就像Tommi创建的库一样,它必须是在其基础上构建的库来实现。

这里的用例是:我有一个conf文件是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

西蒙,如果你试图符合第三方规格,则这不会正常工作。这个建议的一个要点是第三方能够为现有的规格编写自己的符合器,而无需重新定义这些规格。

0
_评论者:ikitommi_

感谢评论。我很乐意提供更改补丁/示例存储库,希望能帮助判断这能否成为规范的一部分。您认为呢?

以下是使用spec-tools将初始规范集成到ring/http库的示例,目前,需要将规范包装到规范记录中才能启用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文档)
;; 根据请求内容类型选择兼容性(例如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
_评论者: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 提供_

至少在完成下一批实施更改之前,我们不会考虑这个问题。我仍然认为我们很可能拒绝这个请求。
...