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

欢迎!请参阅关于页面以获取更多关于如何使用这个网站的信息。

0
Spec
h3. 问题

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

h3. 详细说明

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

示例

* 一个{{keyword?}}规范
** 使用EDN,不应进行强制转换(它可以表示keywords)
** 使用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)


要使用此工具,您需要手动为所有不同的交换格式创建包含不同合规器的新的边界规范。未限定关键词可以映射到sh/keys中(例如,{{::title}}=>{{::title$JSON}}),但如果在边界暴露了完全限定键(例如示例中的{{::id}}),则此方法不会起作用 - 您不能使用相同名称注册不同合规版本的规范。

h3. 建议

建议在规范协议中支持选型合规,使用新的3参数{{conform*}}和{{clojure.spec/conform}},两者都接受一个额外的用户提供的回调/访问者函数。如果提供了回调,则从Spec的{{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-arity {{conform}},并在所有当前Spec中添加它的实现。目前这是不可能的。

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

16 个答案

0
by

评论由: alexmiller

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

0
by

评论由: ikitommi

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

0
by

评论由: kenrestivo

我可以看出为什么Clojure核心开发者可能不希望Spec支持这类强制转换,但事实的实用现实是,有人将不得不这么做。如果不是在Spec本身中,它将不得不在建立在其上的库中进行,例如Tommi的库。

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

0
by

评论由: 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-tools将规范集成到ring/http库中的示例。目前,需要将规范包装到spec记录中来启用3-arity conform。我希望这部分内容可以被移除。有了这个改动,所有(第三方)规范都应该能够即开即用。


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

;; 启用3-arity conform
(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 conform
(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中实现了该功能。无论如何,现在有了这个说明

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
https://dev.clojure.org/jira/browse/CLJ-2251

评论由: ikitommi

答曰

0
答曰

回复者:marco.m

你好,有任何新的消息吗?

0
答曰
回复者:alexmiller

除非我们完成了下一批实现变更,否则我们不会考虑这个问题。我仍然认为我们很可能会拒绝这个请求。
...