Meaningful error message for Clojure.Spec validation in :pre

自古美人都是妖i 提交于 2019-12-04 10:07:36

问题


I used the last days to dig deeper into clojure.spec in Clojure and ClojureScript.

Until now I find it most useful, to use specs as guards in :pre and :post in public functions that rely on data in a certain format.

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

The issue with that approach is, that I get a java.lang.AssertionError: Assert failed: (s/valid? ::person person) without any information about what exactly did not met the specification.

Has anyone an idea how to get a better error message in :pre or :post guards?

I know about conform and explain*, but that does not help in those :pre or :post guards.


回答1:


In newer alphas, there is now s/assert which can be used to assert that an input or return value matches a spec. If valid, the original value is returned. If invalid, an assertion error is thrown with the explain result. Assertions can be turned on or off and can even optionally be omitted from the compiled code entirely to have 0 production impact.

(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}



回答2:


I think the idea is that you use spec/instrument to validate function input and output rather than pre and post conditions.

There's a good example toward the bottom of this blog post: http://gigasquidsoftware.com/blog/2016/05/29/one-fish-spec-fish/ . Quick summary: you can define a spec for a function, including both input and return values using the :args and :ret keys (thus replacing both pre and post conditions), with spec/fdef, instrument it, and you get output similar to using explain when it fails to meet spec.

Minimal example derived from that link:

(spec/fdef your-func
    :args even?
    :ret  string?)


(spec/instrument #'your-func)

And that's equivalent to putting a precondition that the function has an integer argument and a postcondition that it returns a string. Except you get much more useful errors, just like you're looking for.

More details in the official guide: https://clojure.org/guides/spec ---see under the heading "Spec'ing functions".




回答3:


Without taking into account if you should use pre and post conditions to validate function arguments, there is a way to print somewhat clearer messages from pre and post conditions by wrapping your predicate with clojure.test/is, as suggested in the answer below:

How can I get Clojure :pre & :post to report their failing value?

So then your code could look like this:

(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 "Anna@Vissi.com"})

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

Evaluating

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

would produce

"Anna Vissi"

and evaluating

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

would produce

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)



回答4:


This is useful when you don't want to use s/assert, or can not enable s/check-assserts. Improving on MicSokoli's answer:

:pre simply cares that the values returned are all truthy, so we can convert the return value "Success!\n" to true (for strictness) and throw an error with the explanation and the input data in case the output is not successful.

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

A variation of this could be this one, but it would run the spec twice:

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

Usage:

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


来源:https://stackoverflow.com/questions/37885542/meaningful-error-message-for-clojure-spec-validation-in-pre

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!