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

欢迎!请参阅 关于 页以获取有关此工作方式的更多信息。

0
Spec
h3. 问题

在运行时边界的验证中,多个交换格式支持使用 {{clojure.spec}} 难以实现。

h3. 详细信息

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

例子

* 一个 {{keyword?}} 规范
** 使用 EDN,不应进行强制操作(它可以展示关键字)
** 使用 JSON,应用 String->Keyword 的强制转换
** 使用基于 String 的格式(CSV、查询参数等),应用 String->Keyword 的强制转换

* 一个 {{integer?}} 规范
** 使用 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. 建议

通过一个名为 {{conform*}} 的新 3-arity 接口和 {{clojure.spec/conform}} 支持 Spec 协议中的选择性符合,它们都接受一个额外的用户提供的回调/访问器函数。如果提供了回调,则在 Spec 的 {{conform*}} 中以当前规范为参数调用它,并将返回一个 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)))

;; 字符串
(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)感兴趣,所以我怀疑我们可能对此不感兴趣。然而,我将留给Rich评估。

0

评论者:ikitommi

目前,Plumatic Schema是在边缘使用的工具。现在,人们开始转向Spec,如果一个模型库必须用于他们的应用程序,那么这对Clojure Web开发故事来说真的很糟糕。如果Spec不想通过符合器成为转换引擎,我希望有一个替代建议,允许第三方编写此类扩展:将Spec公开为记录/类型而不是重新实现协议会起到作用?

0

评论者:kenrestivo

我可以理解Clojure核心开发者可能不想支持这种类型的约束,但实际现实是必须有人在某处这样做。如果这不是在Spec本身中,那么它将是在基于它的库(如Tommi的库)中完成。

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

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

西蒙,如果你试图符合第三方的规范,这是不行的。这个建议的一个关键点是第三方能够为自己编写的现有规范写入自己的相符器,而无需重新定义这些规范。

0
_评论人:ikitommi_

感谢评论。我很乐意提供一个补丁/样本库,包含此更改所需的内容,希望这可以帮助决定是否这将最终写入规范。您怎么看?

以下是将 spec 意初始集成到 ring/http 库中的样本,使用 spec-tools。目前,需要将规范包装成 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
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
by

评论者:ikitommi

作为补丁的初始工作。

0

评论者:ikitommi

有关此消息的最新动态是什么?

0

评论者:ikitommi

相关链接https://dev.clojure.org/jira/browse/CLJ-2251

0

评论者:marco.m

你好,有什么新消息吗?

0
_评论者:alexmiller_

我们至少要等完成了下一批实现变更之后才会考虑这个问题。我仍然认为我们很可能不会采纳这个建议。
...