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. 建议

使用新的 3-arity {{conform*}} 和 {{clojure.spec/conform}} 支持在 Spec 协议中选择性合规,它们都接受额外的用户提供的回调/访问者函数。如果提供了回调,它将在 Specs {{conform*}} 内部调用,以当前规范作为参数,它将返回 {{nil}} 或应用于实际 confrom 的 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个参数的{{conform}}函数,并为所有当前的规格添加该协议的实现。目前,这是不可能的。

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

16 个答案

0

评论者:alexmiller

我认为我们可能不希望将spec转换为通过conformers转换的转换引擎,所以我推测我们可能不感兴趣。然而,我会让Rich评估。

0

评论者:ikitommi

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

0

评论者:kenrestivo

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

这里的用例是:我有一个YAML配置文件。我使用Clojure库解析YAML,将其转换为映射。现在我必须验证这个映射,但YAML不支持关键字,例如,设置的架构直接作为应用程序状态的一部分进入Component/Mount等,因此在应用程序启动后读取配置时将其作为第一步运行s/conform是有意义的。此外,加入其他合并配置的方法,例如环境变量、.properties文件等,这种强制转换将 somewhere是必需的。

0

评论者:ikitommi

关于评估这个问题有什么新闻吗?我很乐意提供一个补丁或修改后的{{clojure.spec}}的链接,其中包含3-arity conform的使用示例。一些自言自语: 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库中的示例。目前,需要将规范包装到spec记录中才能启用3-arity conforming。这是我希望看到删除的样板。与此更改,它应该为所有(第三方)规范提供即用型功能。


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

;;启用3-arity conforming
(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 conforming
(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实际成了repo。无论如何,下面是它

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_

我们至少在完成下一批实现更改之前不会考虑这一点。我仍然认为我们最可能会拒绝这个。
...