All Projects → bhb → Expound

bhb / Expound

Licence: epl-1.0
Human-optimized error messages for clojure.spec

Programming Languages

clojure
4091 projects
clojurescript
191 projects

Projects that are alternatives of or similar to Expound

Analog
PHP logging library that is highly extendable and simple to use.
Stars: ✭ 314 (-61.04%)
Mutual labels:  errors
Rollbar.js
Error tracking and logging from JS to Rollbar
Stars: ✭ 479 (-40.57%)
Mutual labels:  errors
Tracerr
Golang errors with stack trace and source fragments.
Stars: ✭ 646 (-19.85%)
Mutual labels:  errors
Multierr
Combine one or more Go errors together
Stars: ✭ 329 (-59.18%)
Mutual labels:  errors
Collision
💥 Collision is a beautiful error reporting tool for command-line applications
Stars: ✭ 3,993 (+395.41%)
Mutual labels:  errors
Matrix Doc
Matrix Documentation (including The Spec)
Stars: ✭ 536 (-33.5%)
Mutual labels:  spec
Error Tailor
🦄 Making sure your tailor-made error solution is seamless!
Stars: ✭ 303 (-62.41%)
Mutual labels:  errors
Bugsnag Laravel
Bugsnag notifier for the Laravel PHP framework. Monitor and report Laravel errors.
Stars: ✭ 746 (-7.44%)
Mutual labels:  errors
Bugsnag Php
Bugsnag error monitoring and crash reporting tool for PHP apps
Stars: ✭ 475 (-41.07%)
Mutual labels:  errors
Orchestra
Complete instrumentation for clojure.spec
Stars: ✭ 573 (-28.91%)
Mutual labels:  spec
Laravel Json Api Paginate
A paginator that plays nice with the JSON API spec
Stars: ✭ 351 (-56.45%)
Mutual labels:  spec
Openmicroservices.org
NOT MAINTAINED https://medium.com/@iopeak/open-microservices-specification-1abd8262ad0e
Stars: ✭ 383 (-52.48%)
Mutual labels:  spec
Ghostwheel
Hassle-free inline clojure.spec with semi-automatic generative testing and side effect detection
Stars: ✭ 556 (-31.02%)
Mutual labels:  spec
Tide
A General Toolbox for Identifying Object Detection Errors
Stars: ✭ 309 (-61.66%)
Mutual labels:  errors
Errorx
A comprehensive error handling library for Go
Stars: ✭ 712 (-11.66%)
Mutual labels:  errors
Swiftproject
swift project that brings together some demos, componented, Target-Action, use Swift
Stars: ✭ 310 (-61.54%)
Mutual labels:  spec
Traceback with variables
Adds variables to python traceback. Simple, lightweight, controllable. Debug reasons of exceptions by logging or pretty printing colorful variable contexts for each frame in a stacktrace, showing every value. Dump locals environments after errors to console, files, and loggers. Works in Jupyter and IPython. Install with pip or conda.
Stars: ✭ 509 (-36.85%)
Mutual labels:  errors
Eris
eris provides a better way to handle, trace, and log errors in Go 🎆
Stars: ✭ 758 (-5.96%)
Mutual labels:  errors
Spected
Validation library
Stars: ✭ 717 (-11.04%)
Mutual labels:  spec
Reattempt
🤞 Give your functions another chance
Stars: ✭ 570 (-29.28%)
Mutual labels:  errors

Expound

Clojars Project cljdoc badge CircleCI

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

For example, Expound will replace a clojure.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 with clojure.spec error messages

Expound is in alpha while clojure.spec is in alpha.

Expound is supported by Clojurists 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 the compatibility guide

Leiningen/Boot

[expound "0.8.9"]

deps.edn

expound {:mvn/version "0.8.9"}

Lumo

npm install @bbrinck/expound

Usage

API docs

Quick start via clj

> brew install clojure
> clj -Sdeps '{:deps {friendly {:git/url "https://gist.github.com/bhb/2686b023d074ac052dbc21f12f324f18" :sha "bb5806bd655d743f3b48b36ce83c0085a8d7c54a"}}}' -m friendly
user=> (require '[expound.alpha :as expound])
nil
user=> (expound/expound string? 1)
nil
-- Spec failed --------------------

  1

should satisfy

  string?

-------------------------
Detected 1 error
user=>

expound

Replace calls to clojure.spec.alpha/explain with expound.alpha/expound and to clojure.spec.alpha/explain-str with expound.alpha/expound-str.

(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])

(s/def :example.place/city string?)
(s/def :example.place/state string?)
(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, set clojure.spec.alpha/*explain-out* to expound/printer.

(require '[clojure.spec.alpha :as s])
(require '[expound.alpha :as expound])

(s/def :example.place/city string?)
(s/def :example.place/state string?)

;;  Use `assert`
(s/check-asserts true) ; enable asserts

;; Set var in the scope of 'binding'
(binding [s/*explain-out* expound/printer]
  (s/assert :example.place/city 1))

(set! s/*explain-out* expound/printer)
;; (or alter-var-root - see doc/faq.md)
(s/assert :example.place/city 1)

;; Use `instrument`
(require '[clojure.spec.test.alpha :as st])

(s/fdef pr-loc :args (s/cat :city :example.place/city
                            :state :example.place/state))
(defn pr-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/city 123)

ClojureScript considerations

Due to the way that macros are expanded in ClojureScript, you'll need to configure Expound in Clojure 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.8.9"} 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 of this 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 call repl/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)))

See this ticket for other solutions in the browser.

Printing results for check

Re-binding s/*explain-out* has no effect on the results of clojure.spec.test.alpha/summarize-results, but Expound provides the function expound/explain-results to print the results from clojure.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))))
(defn ranged-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 predicates

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 use expound.alpha/defmsg to add a human-readable error message to the spec.

(s/def :ex/name string?)
(expound/defmsg :ex/name "should be a string")
(expound/expound :ex/name :bob)
;; -- Spec failed --------------------
;;
;; :bob
;;
;; 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 with expound.specs/public-specs.

Printer options

expound and expound-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)
name spec default description
:show-valid-values? boolean? false If false, replaces valid values with ... (example below)
:value-str-fn ifn? provided function Function to print bad values (example below)
:print-specs? boolean? true If true, display "Relevant specs" section. Otherwise, omit that section.
:theme #{:figwheel-theme :none} :none Enables 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?)
(defn my-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 its clojure.test/report function. This is particularly useful in ClojureScript, where a test runner can take care of the boilerplate code:

(ns pkg.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
(defmethod test/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

Use Orchestra 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?)
(defn location [city state]
  ;; incorrect implementation
  nil)

(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 an intended 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 on clojurians Slack.

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

License

Copyright © 2017-2020 Ben Brinckerhoff

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

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].