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

欢迎!请参阅 关于 页面了解这一工作的更多信息。

0
Spec
h3. 问题

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

h3. 详细信息

目前在进行 clojure.spec (alpha-14) 时,符合者在创建 Spec 实例时附加到 Spec 实例上,并且每次符合都会触发它们。这在系统边界验证中不太有用,在这个验证中,符合/强制转换函数应根据运行时数据选择,例如交换格式。

示例

* a {{keyword?}} spec
** 使用 EDN,不应进行强制转换(它可以表示关键字)
** 使用 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}}),则不会工作 - 您不能将相同名称的不同符合 spec 的多个版本注册为不同名称。

h3. 建议

在 Spec 协议中支持选择性符合,使用新的 3 个参数 {{conform*}} 和 {{clojure.spec/conform}},它们都包含一个额外的由用户提供的回调/访问者函数。如果提供了回调,它将在 Specs 的 {{conform*}} 中由当前 spec 作为参数调用,并返回要么是 {{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. 替代方案

支持这种方案的另一选择是允许使用协议扩展Spec。第三方库可以有一个新的{{Conforming}}协议,它包含3参数的{{conform}}实现,并在所有当前Spec上添加对此协议的实现。目前,这还不可能。

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

16 个答案

0

评论者:alexmiller

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

0

评论者:ikitommi

目前,Plumatic Schema是用于边界的工具。现在,人们开始转向Spec,如果必须为应用程序使用两个不同的建模库,这将非常不利于Clojure Web开发的故事。如果Spec不希望通过conformers成为转换引擎,我希望有替代建议允许第三方编写这种类型的扩展:将Spec暴露为Records/Types而不是具体化的协议会起作用吗?

0

评论者:kenrestivo

我可以理解Clojure核心开发人员可能不希望Spec支持这种类型的强制类型转换,但实际情况是,有人必须这样做。如果它不是在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

Simon,如果你正在尝试匹配第三方的规范,这将不会工作。这个建议的一个要点是,第三方可以用自己的从新规范,而不必将这些规范重新定义。

0
_评论者:ikitommi_

感谢您的评论。我很愿意提供一个带有所需更改的补丁/示例仓库,希望能帮助决定是否将此纳入规范。您怎么看?

下面是使用 spec-tools 将 spec 集成到 ring/http 库中的示例。目前,需要将 spec 包装在 spec 记录中来启用 3-arity conforming。这是我想要删除的样板代码。有了这个改变,所有(第三方)spec 应该可以开箱即用。


(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 文档的环形处理程序
;; 根据请求内容类型(例如 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
已解答
0

评论者:marco.m

你好,有什么新闻吗?

0
_评论者:alexmiller_

我们至少要等到实施下一批更改之后才会考虑这个问题。我仍然认为我们可能不会接受这个请求。
...