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

欢迎!请参阅关于页面以了解更多关于此如何工作的信息。

0 投票
Spec
h3. 问题

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

h3. 详细信息

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

例子

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

* a {{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. 建议

建议在Spec协议中支持选型符合,并提供一个新的3个参数的{{conform*}}和{{clojure.spec/conform}},都将包含一个额外的用户提供的回调/访问者函数。如果提供回调,它将从Spec的{{conform*}}内部调用,并将当前规范作为参数传递,然后它将返回一个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}},并在所有当前规格上实现它。目前这是不可能的。

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

16 个答案

0 投票

评论者:alexmiller

我认为我们可能不希望通过conformer将spec转变成一个转换引擎,所以我怀疑我们可能并不感兴趣。不过,我会留待Rich评估。

0 投票

评论者:ikitommi

目前,Plumatic Schema用于边界。现在,人们开始转用Spec,如果必须为他们的应用程序使用两个不同的建模库,那么这对于Clojure Web开发故事将是非常糟糕的。如果Spec不想通过conformer作为转换引擎,我期待有替代建议,允许第三方编写这类扩展:将Spec公开为记录/类型而不是具体的协议将完成这项工作?

0 投票

评论者:kenrestivo

我可以理解Clojure核心开发人员可能不希望Spec支持这种类型的强制,但现实情况是,有人必须这样做。如果Spec本身不提供这种支持,那么它将不得不在基于它的库中完成,例如Tommi的库。

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

Simon,如果你试图使规范与第三方规范相一致,那么这不会工作。这个建议的一个观点是第三方能够为自己的现有规范编写自己的一致性适配器,而无需重新定义这些规范。

0 投票
评论者:ikitommi

感谢评论。我很乐意提供一个补丁/示例仓库,其中包含必需的更改,希望这有助于判断这些更改是否会最终添加到规范中。您怎么看?

以下是将 spec 工具集成到 ring/http 库中的初始示例,使用 spec-tools。目前,需要将规范包装到 spec 记录中来启用 3 参数一致性。这是我希望看到移除的样板代码。通过此更改,所有(第三方)规范都应能直接工作。


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

;;Enable 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?) ;;Enable 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])))
定义了一个名为 pizza 的规范,要求必填字段为 ::id, 以及必选的非可选字段 ::name、::size、::origin,可选的非可选字段为 ::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 投票
by

评论者:ikitommi

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

0 投票
by

评论人:marco.m

你好,有什么新消息吗?

0 投票
by
_评论人:alexmiller_

我们将至少等到完成下一批实现更改后才考虑这个问题。我依然认为我们很可能不采纳这个请求。
...