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

欢迎!有关如何工作的更多信息,请参阅关于页面。

0
h3. 问题

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

h3. 详细信息

在 clojure.spec (alpha-14) 中,合规器在创建 Spec 实例时附加到 Spec 实例,并在每次合规时调用。这在系统边界验证中并不很有用,在这种验证中,合规/转换函数应根据运行时数据选择,例如交换格式。

示例

* a {{keyword?}} spec
** 使用EDN,不应执行转换(它可以表示关键字)
** 使用JSON,应该应用String->关键字转换
** 使用基于字符串的格式(CSV、查询参数、...),应该应用String->关键字转换

* 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}} &=gt; {{::title$JSON}}),但如果完全限定的键在边界上公开(例如例子中的 {{::id}}),则不行 - 不能用相同的名称注册多个,合规方式不同的规范版本。

h3. 建议

在规范协议中支持选择合规性,使用新的 3 参数 {{conform*}} 和 {{clojure.spec/conform}},都包含一个额外的用户提供的回调/访问器函数。如果提供了回调,它将从规范 {{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)))

;; 字符串
(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 评论

我认为我们不感兴趣将spec通过conformer转换为一个转换引擎,所以我怀疑我们可能也不感兴趣。但是,我会留供Rich评估。

0

由:ikitommi 评论

目前,Plumatic Schema是用于边界的工具。现在,人们开始转向Spec,如果需要为他们的应用使用两种不同的建模库,那么这对Clojure Web开发故事来说将是非常糟糕的。如果Spec不希望通过conformer成为一个转换引擎,我希望能有另一种建议,允许第三方编写这种类型的扩展:将Spec作为记录/类型而不是具象协议暴露出来会完成这项工作?

0

由:kenrestivo 评论

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

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

0

由:ikitommi 评论

对这个评估有什么新闻吗?我将很高兴提供补丁或者修改后带有如何使用3-arity一致性的示例的{{clojure.spec}}链接。一些随想: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库initial spec-integration的示例。现在,需要将spec封装到spec records中才能启用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文档)的环形处理程序
;; 基于请求的内容类型(如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_

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