:pre中有关Clojure.Spec验证的有意义的错误消息

Meaningful error message for Clojure.Spec validation in :pre

我用了最后的日子来深入研究Clojure和ClojureScript中的clojure.spec。

到目前为止,我发现它最有用,它是在依赖某种格式数据的公共函数中的:pre:post中使用规范作为防护。

1
2
3
4
(defn person-name [person]
  {:pre [(s/valid? ::person person)]
   :post [(s/valid? string? %)]}
  (str (::first-name person)"" (::last-name person)))

这种方法的问题是,我得到一个java.lang.AssertionError: Assert failed: (s/valid? ::person person),而没有任何关于完全不符合规范的信息。

有谁知道如何在:pre:post防护中获得更好的错误消息?

我知道conformexplain*,但这对那些:pre:post防护没有帮助。


在较新的Alpha中,现在提供了s/assert,可用于断言输入或返回值与规范匹配。如果有效,则返回原始值。如果无效,则将引发断言错误以及解释结果。断言可以打开或关闭,甚至可以选择完全从编译的代码中省略,以产生0的影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(s/def ::first-name string?)
(s/def ::last-name string?)
(s/def ::person (s/keys :req [::first-name ::last-name]))
(defn person-name [person]
  (s/assert ::person person)
  (s/assert string? (str (::first-name person)"" (::last-name person))))

(s/check-asserts true)

(person-name 10)
=> CompilerException clojure.lang.ExceptionInfo: Spec assertion failed
val: 10 fails predicate: map?
:clojure.spec/failure  :assertion-failed
 #:clojure.spec{:problems [{:path [], :pred map?, :val 10, :via [], :in []}], :failure :assertion-failed}


我认为这个想法是您使用spec/instrument来验证函数的输入和输出,而不是之前和之后的条件。

在此博客文章的底部有一个很好的例子:http://gigasquidsoftware.com/blog/2016/05/29/one-fish-spec-fish/。快速总结:您可以使用:args和:ret键定义函数的规范,包括输入值和返回值(从而替换前置条件和后置条件),并使用spec/fdef对其进行仪表化,并获得类似于以下内容的输出如果explain不符合规格,请使用。

从该链接派生的最小示例:

1
2
3
4
5
6
(spec/fdef your-func
    :args even?
    :ret  string?)


(spec/instrument #'your-func)

这等效于将函数具有整数参数作为前提,将函数返回一个字符串作为后置条件。就像您在寻找一样,除了得到更多有用的错误外。

官方指南中的更多详细信息:https://clojure.org/guides/spec ---参见"规范功能"标题下的内容。


不考虑是否应该使用前置条件和后置条件来验证函数参数,而是通过用clojure.test/is包装谓词来从前置条件和后置条件中打印一些更清晰的消息,如以下答案所示:

如何获取Clojure:pre和:post报告其失败值?

因此,您的代码可能如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(ns pre-post-messages.core
  (:require [clojure.spec :as s]
            [clojure.test :as t]))

(defn person-name [person]
  {:pre [(t/is (s/valid? ::person person))]
   :post [(t/is (s/valid? string? %))]}
  (str (::first-name person)"" (::last-name person)))

(def try-1
  {:first-name"Anna Vissi"})

(def try-2
  {::first-name"Anna"
   ::last-name"Vissi"
   ::email"[email protected]"})

(s/def ::person (s/keys :req [::first-name ::last-name ::email]))

评估

1
pre-post-messages.core> (person-name  try-2)

会产生

1
"Anna Vissi"

和评估

1
pre-post-messages.core> (person-name  try-1)

会产生

1
2
3
4
5
6
7
FAIL in () (core.clj:6)

expected: (s/valid? :pre-post-messages.core/person person)

  actual: (not (s/valid? :pre-post-messages.core/person {:first-name"Anna Vissi"}))

AssertionError Assert failed: (t/is (s/valid? :pre-post-messages.core/person person))  pre-post-messages.core/person-name (core.clj:5)


当您不想使用s/assert或无法启用s/check-assserts时,此功能很有用。改善MicSokoli的答案:

:pre只是在乎返回的值都是真实的,因此我们可以将返回值"Success!\
"
转换为true(出于严格性考虑),并且将throw与说明和输入数据一起转换为错误,以防输出不正确。成功。

1
2
3
4
5
6
(defn validate [spec input]
    (let [explanation (s/explain-str spec input)]
        (if (= explanation"Success!\
")
            true
            (throw (ex-info explanation {:input input}))))

可以是这种形式的变体,但是它将运行两次规范:

1
2
3
4
(defn validate [spec input]
    (if (s/valid? spec input)
        true
        (throw (ex-info (s/explain spec input) {:input input}))))

用法:

1
2
3
(defn person-name [person]
  {:pre [(validate ::person person)]}
  (str (::first-name person)"" (::last-name person)))