Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

Human-optimized error messages for clojure.spec

License

NotificationsYou must be signed in to change notification settings

bhb/expound

Repository files navigation

Clojars Projectcljdoc badgeCircleCIGitpod ready-to-code

Expound formatsclojure.spec error messages in a way that is optimized for humans to read.

For example, Expound will replace aclojure.spec error message like:

val: {} fails spec: :example/place predicate: (contains? % :city)val: {} fails spec: :example/place predicate: (contains? % :state)

with

-- Spec failed --------------------  {}should contain keys: :city, :state| key    | spec    ||========+=========|| :city  | string? ||--------+---------|| :state | string? |

Comparison withclojure.spec error messages

Expound is in alpha whileclojure.spec is in alpha.

Expound is supported byClojurists Together. If you find this project useful, please consider making a monthly donation to Clojurists Together (or ask your employer to do so).

Installation

If you are using recent versions of ClojureScript, please check thecompatibility guide

Leiningen/Boot

[expound "0.9.0"]

deps.edn

expound/expound {:mvn/version "0.9.0"}

Lumo

npm install @bbrinck/expound

Usage

API docs

Quick start viaclj

> brew install clojure/tools/clojure> clojure -Sdeps '{:deps {friendly/friendly {:git/url "https://gist.github.com/bhb/2686b023d074ac052dbc21f12f324f18" :sha "d532662414376900c13bed9c920181651e1efeff"}}}' -X friendly/runuser=> (require '[expound.alpha :as expound])niluser=> (expound/expound string? 1)nil-- Spec failed --------------------  1should satisfy  string?-------------------------Detected 1 erroruser=>

expound

Replace calls toclojure.spec.alpha/explain withexpound.alpha/expound and toclojure.spec.alpha/explain-str withexpound.alpha/expound-str.

(require '[clojure.spec.alpha:as s])(require '[expound.alpha:as expound])(s/def:example.place/citystring?)(s/def:example.place/statestring?)(s/def:example/place (s/keys:req-un [:example.place/city:example.place/state]))(expound/expound:example/place {:city"Denver",:state:CO} {:print-specs?false});; -- Spec failed --------------------;;   {:city ..., :state :CO};;                      ^^^;; should satisfy;;   string?;; -------------------------;; Detected 1 error

*explain-out*

To use other Spec functions, setclojure.spec.alpha/*explain-out* toexpound/printer.

(require '[clojure.spec.alpha:as s])(require '[expound.alpha:as expound])(s/def:example.place/citystring?)(s/def:example.place/statestring?);;  Use `assert`(s/check-assertstrue); enable asserts;; Set var in the scope of 'binding'(binding [s/*explain-out* expound/printer]  (s/assert:example.place/city1))(set! s/*explain-out* expound/printer);; (or alter-var-root - see doc/faq.md)(s/assert:example.place/city1);; Use `instrument`(require '[clojure.spec.test.alpha:as st])(s/fdef pr-loc:args (s/cat:city:example.place/city:state:example.place/state))(defnpr-loc [city state]  (str city"," state))(st/instrument `pr-loc)(pr-loc"denver":CO);; You can use `explain` without converting to expound(s/explain:example.place/city123)

ClojureScript considerations

Due to the way that macros are expanded in ClojureScript, you'll need to configure Expound inClojure to use Expound during macro-expansion. This does not apply to self-hosted ClojureScript. Note the-e arg when starting ClojureScript:

clj -Srepro -Sdeps '{:deps {expound {:mvn/version "0.9.0"} org.clojure/test.check {:mvn/version "0.9.0"} org.clojure/clojurescript {:mvn/version "1.10.520"}}}' -e "(require '[expound.alpha :as expound]) (set! clojure.spec.alpha/*explain-out* expound.alpha/printer)" -m cljs.main -re node

As ofthis commit, ClojureScript instrumentation errors only contain data and leave formatting/printing errors to the edge of the system e.g. the REPL. To format errors in the browser, you must set up some global handler to catch errors and callrepl/error->str. For instance, here is a custom formatter for Chrome devtools that uses Expound:

(require '[cljs.repl :as repl])(require '[clojure.spec.alpha :as s])(require '[expound.alpha :as expound])(set! s/*explain-out* expound/printer)(def devtools-error-formatter  "Uses cljs.repl utilities to format ExceptionInfo objects in Chrome devtools console."  #js{:header      (fn [object _config]        (when (instance? ExceptionInfo object)          (let [message (some->> (repl/error->str object)                                 (re-find #"[^\n]+"))]            #js["span" message])))      :hasBody (constantly true)      :body (fn [object _config]              #js["div" (repl/error->str object)])})(defonce _         (some-> js/window.devtoolsFormatters                 (.unshift devtools-error-formatter)))

Seethis ticket for other solutions in the browser.

Printing results forcheck

Re-bindings/*explain-out* has no effect on the results ofclojure.spec.test.alpha/summarize-results, but Expound provides the functionexpound/explain-results to print the results fromclojure.spec.test.alpha/check.

(require '[expound.alpha:as expound]         '[clojure.spec.test.alpha:as st]         '[clojure.spec.alpha:as s]         '[clojure.test.check])(s/fdef ranged-rand:args (s/and (s/cat:start int?:end int?)               #(< (:start %) (:end %))):ret int?:fn (s/and #(>= (:ret %) (-> %:args:start))             #(< (:ret %) (-> %:args:end))))(defnranged-rand"Returns random int in range start <= rand < end"  [start end]  (+ start (long (rand (- start end)))))(set! s/*explain-out* expound/printer);; (or alter-var-root - see doc/faq.md)(expound/explain-results (st/check `ranged-rand));;== Checked user/ranged-rand =================;;;;-- Function spec failed -----------;;;;  (user/ranged-rand -3 0);;;;failed spec. Function arguments and return value;;;;  {:args {:start -3, :end 0}, :ret -5};;;;should satisfy;;;;  (fn;;   [%];;   (>= (:ret %) (-> % :args :start)))

Error messages for specs

Adding error messages

If a value fails to satisfy a predicate, Expound will print the name of the function (or<anonymous function> if the function has no name). To improve the error message, you can useexpound.alpha/defmsg to add a human-readable error message to the spec.

(s/def:ex/namestring?)(expound/defmsg:ex/name"should be a string")(expound/expound:ex/name:bob);; -- Spec failed --------------------;;;; :bob;;;; should be a string

You can also reuse messages by defining specs in terms of other specs:

(s/def:ex/stringstring?)(expound/defmsg:ex/string"should be a string")(s/def:ex/city:ex/string)(expound/expound:ex/city:denver);; -- Spec failed --------------------;;;;   :denver;; should be a string

Built-in predicates with error messages

Expound provides a default set of type-like predicates with error messages. For example:

(expound/expound:expound.specs/pos-int-1);; -- Spec failed --------------------;;;; -1;;;; should be a positive integer

You can see the full list of available specs withexpound.specs/public-specs.

Printer options

expound andexpound-str can be configured with options:

(expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false :theme :figwheel-theme})

or, to configure the global printer:

(set! s/*explain-out* (expound/custom-printer {:show-valid-values?true:print-specs?false:theme:figwheel-theme}));; (or alter-var-root - see doc/faq.md)
namespecdefaultdescription
:show-valid-values?boolean?falseIffalse, replaces valid values with... (example below)
:value-str-fnifn?provided functionFunction to print bad values (example below)
:print-specs?boolean?trueIf true, display "Relevant specs" section. Otherwise, omit that section.
:theme#{:figwheel-theme :none}:noneEnables color theme.

:show-valid-values?

By default,printer will omit valid values and replace them with...

(set! s/*explain-out* expound/printer);; (or alter-var-root - see doc/faq.md)(s/explain:example/place {:city"Denver":state:CO:country"USA"});; -- Spec failed --------------------;;;;   {:city ..., :state :CO, :country ...};;                      ^^^;;;; should satisfy;;;;   string?

You can configure Expound to show valid values:

(set! s/*explain-out* (expound/custom-printer {:show-valid-values?true}));; (or alter-var-root - see doc/faq.md)(s/explain:example/place {:city"Denver":state:CO:country"USA"});; -- Spec failed --------------------;;;; {:city "Denver", :state :CO, :country "USA"};;                         ^^^;;;; should satisfy;;;;   string?
:value-str-fn

You can provide your own function to display the invalid value.

;; Your implementation should meet the following spec:(s/fdef my-value-str:args (s/cat:spec-name (s/nilable #{:args:fn:ret}):form any?:path:expound/path:value any?):ret string?)(defnmy-value-str [_spec-name form path value]  (str"In context:" (pr-str form)"\n""Invalid value:" (pr-str value)))(set! s/*explain-out* (expound/custom-printer {:value-str-fn my-value-str}));; (or alter-var-root - see doc/faq.md)(s/explain:example/place {:city"Denver":state:CO:country"USA"});; -- Spec failed --------------------;;;;   In context: {:city "Denver", :state :CO, :country "USA"};;   Invalid value: :CO;;;; should satisfy;;;;   string?

Manual clojure.test/report override

Clojure test allows you to declare a custom multi-method for itsclojure.test/report function. This is particularly useful in ClojureScript, where a test runner can take care of the boilerplate code:

(nspkg.test-runner  (:require [clojure.spec.alpha:as s]            [clojure.test:as test:refer-macros [run-tests]]            [expound.alpha:as expound];; require your namespaces here            [pkg.namespace-test]))(enable-console-print!)(set! s/*explain-out* expound/printer);; (or alter-var-root - see doc/faq.md);; We try to preserve the clojure.test output format(defmethodtest/report [:cljs.test/default:error] [m]  (test/inc-report-counter!:error)  (println"\nERROR in" (test/testing-vars-str m))  (when (seq (:testing-contexts (test/get-current-env)))    (println (test/testing-contexts-str)))  (when-let [message (:message m)] (println message))  (let [actual (:actual m)        ex-data (ex-data actual)]    (if (:clojure.spec.alpha/failure ex-data)      (do (println"expected:" (pr-str (:expected m)))          (print"  actual:\n")          (print (.-message actual)))      (test/print-comparison m))));; run tests, (stest/instrument) either here or in the individual test files.(run-tests 'pkg.namespace-test)

Using Expound as printer for Orchestra

UseOrchestra with Expound to get human-optimized error messages when checking your:ret and:fn specs.

(require '[orchestra.spec.test:as st])(s/fdef location:args (s/cat:city:example.place/city:state:example.place/state):ret string?)(defnlocation [city state];; incorrect implementationnil)(st/instrument)(set! s/*explain-out* expound/printer);; (or alter-var-root - see doc/faq.md)(location"Seattle""WA");;ExceptionInfo Call to #'user/location did not conform to spec:;; form-init3240528896421126128.clj:1;;;; -- Spec failed --------------------;;;; Return value;;;; nil;;;; should satisfy;;;; string?;;;; -------------------------;; Detected 1 error

Conformers

Expound will not give helpful errors (and in some cases, will throw an exception) if you use conformers to transform values. Although using conformers in this way is fairly common, my understanding is that this is not anintended use case.

If you want to use Expound with conformers, you'll need to write a custom printer. See "Printer options" above.

Related work

  • Inspectable - Tools to explore specs and spec failures at the REPL
  • Pretty-Spec - Pretty printer for specs
  • Phrase - Use specs to create error messages for users
  • Pinpointer - spec error reporter based on a precise error analysis

Prior Art

Contributing

Pull requests are welcome, although please open an issue first to discuss the proposed change. I also answer questions on the #expound channel onclojurians Slack.

If you are working on the code, please read theDevelopment Guide

License

Copyright © 2017-2022 Ben Brinckerhoff

Distributed under the Eclipse Public License version 1.0, just like Clojure.

About

Human-optimized error messages for clojure.spec

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages


[8]ページ先頭

©2009-2025 Movatter.jp