├── .circleci └── config.yml ├── .clj-kondo ├── config.edn └── hooks │ └── checking.clj ├── .gitignore ├── .gitpod.dockerfile ├── .gitpod.yml ├── .joker ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SHASUMS256.txt.asc ├── _config.yml ├── bin ├── bb_ex.clj ├── bb_test.clj ├── build_comparison_doc.sh ├── check-for-warnings.sh ├── comparison.clj ├── deps.clj ├── deps.edn ├── deps_test.sh ├── find-unused-clj.sh ├── golden_master_test.sh ├── inconsistent-aliases ├── install-bb ├── install-clojure ├── kaocha ├── lint ├── sample.clj ├── sample.cljs ├── test-datomic └── tests ├── deps.edn ├── dev.cljs.edn ├── dev └── user.clj ├── doc ├── arch │ ├── adr-000.md │ ├── adr-001.md │ ├── adr-002.md │ └── adr-003.md ├── cljdoc.edn ├── cljtogether │ ├── project_update1.md │ ├── project_update2.md │ ├── project_update3.md │ ├── project_update4.md │ ├── project_update5.md │ └── project_update6.md ├── comparison.md ├── compatibility.md ├── development.md ├── faq.md ├── prior_art.md └── spec_problems.md ├── expected-jar-contents.txt ├── figwheel-main.edn ├── karma.conf.js ├── package-lock.json ├── package.json ├── project.clj ├── resources └── public │ └── index.html ├── src └── expound │ ├── alpha.cljc │ ├── ansi.cljc │ ├── paths.cljc │ ├── printer.cljc │ ├── problems.cljc │ ├── specs.cljc │ └── util.cljc ├── test ├── cljs_test.cljs ├── expected_deps.txt ├── expected_sample_out.txt └── expound │ ├── alpha_test.cljc │ ├── paths_test.cljc │ ├── print_length_test.cljc │ ├── printer_test.cljc │ ├── problems_test.cljc │ ├── spec_gen.cljc │ ├── specs_test.cljc │ ├── spell_spec_test.cljc │ ├── test_runner.cljs │ ├── test_utils.cljc │ └── util_test.cljc └── tests.edn /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Clojure CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: cimg/clojure:1.11.1-browsers 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/postgres:9.4 16 | 17 | working_directory: ~/repo 18 | 19 | environment: 20 | LEIN_ROOT: "true" 21 | # Customize the JVM maximum heap limit 22 | JVM_OPTS: -Xmx3200m 23 | 24 | steps: 25 | - checkout 26 | 27 | # Download and cache dependencies 28 | - restore_cache: 29 | keys: 30 | - v1-dependencies-{{ checksum "project.clj" }} 31 | # fallback to using the latest cache if no exact match is found 32 | - v1-dependencies- 33 | 34 | - run: lein with-profile +test-common,+test-web,+dev deps 35 | 36 | - save_cache: 37 | paths: 38 | - ~/.m2 39 | key: v1-dependencies-{{ checksum "project.clj" }} 40 | 41 | test_current: 42 | docker: 43 | # specify the version you desire here 44 | - image: cimg/clojure:1.11.1-browsers 45 | 46 | # Specify service dependencies here if necessary 47 | # CircleCI maintains a library of pre-built images 48 | # documented at https://circleci.com/docs/2.0/circleci-images/ 49 | # - image: circleci/postgres:9.4 50 | 51 | working_directory: ~/repo 52 | 53 | environment: 54 | LEIN_ROOT: "true" 55 | # Customize the JVM maximum heap limit 56 | JVM_OPTS: -Xmx3200m 57 | 58 | steps: 59 | - checkout 60 | 61 | # Download and cache dependencies 62 | - restore_cache: 63 | keys: 64 | - v1-dependencies-{{ checksum "project.clj" }} 65 | # fallback to using the latest cache if no exact match is found 66 | - v1-dependencies- 67 | 68 | - run: bin/install-clojure 69 | - run: bin/install-bb 70 | - run: npm install 71 | - run: lein check 72 | - run: 73 | name: 'Check artifact isolation + lack of reflection/boxing warnings' 74 | # skip the first warning, which I believe is in leiningen itself 75 | command: lein with-profile -dev,+check check 2>&1 | grep -v "form-init.*clj" | bin/check-for-warnings.sh 76 | - run: TEST_CHECK_FACTOR=20 lein with-profile test-common test 77 | - run: lein clean 78 | - run: lein with-profile test-web cljsbuild once test 2> >(tee -a stderr.log >&2) 79 | - run: cat stderr.log | grep -v "Options passed to ClojureScript compiler" | grep -v "Use of undeclared Var goog.math.Long" | bin/check-for-warnings.sh 80 | - run: bin/tests 81 | - run: bb bin/bb_ex.clj 2> >(tee -a stderr2.log >&2) 82 | - run: cat stderr2.log | bin/check-for-warnings.sh 83 | - run: bb bin/bb_test.clj 84 | - run: bin/golden_master_test.sh 85 | - run: bin/deps_test.sh 86 | - run: bin/build_comparison_doc.sh 87 | - run: git diff --exit-code -- doc/comparison.md # make sure above step does not modify examples 88 | - run: lein jar && diff -u <(jar tf target/*.jar | sort) <(cat expected-jar-contents.txt | sort) 89 | 90 | test_cljs_old: 91 | docker: 92 | # specify the version you desire here 93 | - image: cimg/clojure:1.11.1-browsers 94 | 95 | # Specify service dependencies here if necessary 96 | # CircleCI maintains a library of pre-built images 97 | # documented at https://circleci.com/docs/2.0/circleci-images/ 98 | # - image: circleci/postgres:9.4 99 | 100 | working_directory: ~/repo 101 | 102 | environment: 103 | LEIN_ROOT: "true" 104 | # Customize the JVM maximum heap limit 105 | JVM_OPTS: -Xmx3200m 106 | 107 | steps: 108 | - checkout 109 | 110 | # Download and cache dependencies 111 | - restore_cache: 112 | keys: 113 | - v1-dependencies-{{ checksum "project.clj" }} 114 | # fallback to using the latest cache if no exact match is found 115 | - v1-dependencies- 116 | - run: bin/install-clojure 117 | - run: npm install 118 | # The version number (1.10.439) is important - it's the last version in which CLJS itself 119 | # did NOT include 'goog.string.format' so it's the last version that will reproduce https://github.com/bhb/expound/issues/183 120 | - run: clojure -Sdeps '{:deps {org.clojure/clojurescript {:mvn/version "1.10.439"}}}' -m cljs.main -re node -i test/cljs_test.cljs 121 | - run: lein clean && lein with-profile test-web,clj-1.9.0,cljs-1.10.238,orch-2020.07.12-1 cljsbuild once test && bin/tests 122 | - run: lein clean && lein with-profile test-web,clj-1.9.0,cljs-1.10.339,orch-2020.07.12-1 cljsbuild once test && bin/tests 123 | - run: lein clean && lein with-profile test-web,clj-1.10.0,cljs-1.10.439,orch-2020.07.12-1 cljsbuild once test && bin/tests 124 | - run: lein clean && lein with-profile test-web,clj-1.10.0,cljs-1.10.597 cljsbuild once test && bin/tests 125 | - run: lein clean && lein with-profile test-web,orch-2019.02.06-1 cljsbuild once test && bin/tests 126 | 127 | test_clj_old: 128 | docker: 129 | # specify the version you desire here 130 | - image: cimg/clojure:1.11.1-browsers 131 | 132 | # Specify service dependencies here if necessary 133 | # CircleCI maintains a library of pre-built images 134 | # documented at https://circleci.com/docs/2.0/circleci-images/ 135 | # - image: circleci/postgres:9.4 136 | 137 | working_directory: ~/repo 138 | 139 | environment: 140 | LEIN_ROOT: "true" 141 | # Customize the JVM maximum heap limit 142 | JVM_OPTS: -Xmx3200m 143 | 144 | steps: 145 | - checkout 146 | 147 | # Download and cache dependencies 148 | - restore_cache: 149 | keys: 150 | - v1-dependencies-{{ checksum "project.clj" }} 151 | # fallback to using the latest cache if no exact match is found 152 | - v1-dependencies- 153 | - run: lein with-profile test-common,clj-1.9.0,spec-0.2.168 test 154 | - run: lein with-profile test-common,clj-1.10.0,spec-0.2.176 test 155 | - run: lein with-profile test-common,clj-1.10.0,orch-2019.02.06-1 test 156 | 157 | workflows: 158 | version: 2 159 | build_test: 160 | jobs: 161 | - build 162 | - test_current: 163 | requires: 164 | - build 165 | - test_cljs_old: 166 | requires: 167 | - build 168 | - test_clj_old: 169 | requires: 170 | - build 171 | -------------------------------------------------------------------------------- /.clj-kondo/config.edn: -------------------------------------------------------------------------------- 1 | {:linters {:deprecated-var {:exclude {expound.alpha/def {:namespaces [".*-test$", "expound.spec-gen"]}}}} 2 | :hooks {:analyze-call {com.gfredericks.test.chuck.clojure-test/checking hooks.checking/checking}}} 3 | -------------------------------------------------------------------------------- /.clj-kondo/hooks/checking.clj: -------------------------------------------------------------------------------- 1 | (ns hooks.checking 2 | (:require [clj-kondo.hooks-api :as api])) 3 | 4 | (defn checking [{:keys [:node]}] 5 | (let [[name num-tests binding-vec & body] (rest (:children node)) 6 | bindings (->> (:children binding-vec) 7 | (partition 2) 8 | (mapcat (fn [[k v]] 9 | (if (contains? #{:let 10 | :parallel} (:k k)) 11 | (:children v) 12 | [k v]))) 13 | vec) 14 | new-node (api/list-node 15 | (list* 16 | (api/token-node 'let*) 17 | (api/vector-node bindings) 18 | name 19 | num-tests 20 | body))] 21 | {:node new-node})) 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | figwheel_server.log 13 | node_modules/ 14 | resources/ 15 | .idea 16 | expound.iml 17 | .nightlight.edn 18 | repl-deps 19 | test-deps 20 | .cpcache 21 | nashorn_code_cache 22 | .rebel_readline_history 23 | .clj-kondo 24 | sqlite.db -------------------------------------------------------------------------------- /.gitpod.dockerfile: -------------------------------------------------------------------------------- 1 | FROM gitpod/workspace-full 2 | 3 | RUN brew install leiningen 4 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: 2 | file: .gitpod.dockerfile 3 | 4 | # List the start up tasks. Learn more https://www.gitpod.io/docs/config-start-tasks/ 5 | tasks: 6 | - name: Install deps 7 | init: | 8 | lein deps 9 | lein classpath 10 | bin/kaocha 11 | - name: Run tests 12 | command: bin/kaocha --watch 13 | 14 | github: 15 | prebuilds: 16 | # enable for the default branch (defaults to true) 17 | master: true 18 | # enable for all branches in this repo (defaults to false) 19 | branches: true 20 | # enable for pull requests coming from this repo (defaults to true) 21 | pullRequests: true 22 | # enable for pull requests coming from forks (defaults to false) 23 | pullRequestsFromForks: true 24 | # add a check to pull requests (defaults to true) 25 | addCheck: true 26 | 27 | vscode: 28 | extensions: 29 | - betterthantomorrow.calva 30 | -------------------------------------------------------------------------------- /.joker: -------------------------------------------------------------------------------- 1 | {:known-macros [clojure.spec.alpha/fdef 2 | com.gfredericks.test.chuck.clojure-test/checking 3 | clojure.test.check.generators/let] 4 | :rules {:if-without-else true} 5 | :ignored-unused-namespaces [clojure.core.specs.alpha]} 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [Unreleased] 5 | 6 | ## [0.9.0] - 2022-01-13 7 | 8 | ### Fixed 9 | - [Bug with `keys` specs that used `and`/`or`](https://github.com/bhb/expound/issues/229) 10 | 11 | ### Added 12 | 13 | - Ability to reuse messages by defining specs in terms of other specs. See "Adding error messages" in the README for examples. Thanks [`@mpenet`](https://github.com/mpenet) for the implementation! 14 | - New `undefmsg` function 15 | 16 | ## [0.8.10] - 2021-09-06 17 | 18 | ### Fixed 19 | 20 | - [Bug with displaying errors when `*print-length*` or `*print-level*` is very small](https://github.com/bhb/expound/issues/217). Thanks [`@vemv`](https://github.com/vemv) for the fix! 21 | - [Bug with finding specs for unqualified keywords in `keys` spec](https://github.com/bhb/expound/issues/215) 22 | 23 | ## [0.8.9] - 2021-02-11 24 | 25 | ### Fixed 26 | 27 | - [Bug with displaying non-data specced values (e.g. Datomic DB) when `:show-valid-values?` is `true`](https://github.com/bhb/expound/issues/209) 28 | 29 | ## [0.8.8] - 2021-02-11 30 | 31 | ### Fixed 32 | 33 | - Warnings when Expound is required in Babashka 34 | 35 | ## [0.8.7] - 2020-12-05 36 | 37 | ### Fixed 38 | 39 | - [Bug with walking all specced values unnecessarily](https://github.com/bhb/expound/issues/205) 40 | 41 | ## [0.8.6] - 2020-10-01 42 | 43 | ### Fixed 44 | 45 | - [Bug with printing specs that are undefined in ClojureScript](https://github.com/bhb/expound/issues/198) 46 | 47 | ### Added 48 | 49 | - [Support for Orchestra 2020.07.12-1](https://github.com/bhb/expound/issues/199) 50 | 51 | ## [0.8.5] - 2020-06-17 52 | 53 | ### Fixed 54 | 55 | - [Invalid spec for 'printer' function](https://github.com/bhb/expound/issues/191) 56 | 57 | ## [0.8.4] - 2019-12-31 58 | 59 | ### Fixed 60 | 61 | - [Regression in formatting code for ClojureScript](https://github.com/bhb/expound/issues/183) 62 | 63 | ## [0.8.3] - 2019-12-29 64 | 65 | ### Fixed 66 | 67 | - Several boxed math warnings. Thanks [`@vemv`](https://github.com/vemv) for the fix! 68 | 69 | ### Added 70 | 71 | - Instructions on how to handle instrumentation exceptions in the browser. Thanks [`@mhuebert`](https://github.com/mhuebert) for the example code! 72 | 73 | 74 | ## [0.8.2] - 2019-12-11 75 | 76 | This release was made possible by [Clojurists Together](https://www.clojuriststogether.org/news/q4-2019-funding-announcement/) 77 | 78 | ### Fixed 79 | 80 | - [Bug with printing failures for `multi-spec`s](https://github.com/bhb/expound/issues/122). Unfortunately, I had to remove some [output that was useful but not reliable in all cases](https://github.com/bhb/expound/blob/946f9268c8ed8db72a521b8077aa0926febf7916/src/expound/alpha.cljc#L240-L261) 81 | - [Bug with registering message for set-based specs](https://github.com/bhb/expound/issues/101) 82 | - [Bug with duplicate custom messages in `alt` or `or` specs](https://github.com/bhb/expound/issues/135) 83 | 84 | ## [0.8.1] - 2019-11-30 85 | 86 | ### Fixed 87 | 88 | - [Bug with unnecessary dependency](https://github.com/bhb/expound/issues/171) 89 | 90 | ### Added 91 | 92 | - New documentation comparing `clojure.spec` messages to Expound messages 93 | 94 | ## [0.8.0] - 2019-11-24 95 | 96 | ### Fixed 97 | - [Bug printing specs with several `or` branches](https://github.com/bhb/expound/issues/93) 98 | - [Bug with over-expanding spec forms inside a table of keys](https://github.com/bhb/expound/issues/155) 99 | - [Bug with incorrectly grouping independent problems in a `keys` spec](https://github.com/bhb/expound/issues/165). Thanks [`@kelvinqian00`](https://github.com/kelvinqian00) for the fix! 100 | 101 | ### Added 102 | - `expound` and `expound-str` now (optionally) accept the same options as `custom-printer` (e.g. `(expound int? "" {:theme :figwheel-theme})`) 103 | 104 | ### Changed 105 | - New format for table of missing keys 106 | 107 | ## [0.7.2] - 2018-12-18 108 | 109 | ### Fixed 110 | - Bugs with exceptions on specs that conform e.g. [`s/keys*`](https://github.com/bhb/expound/issues/112) and [`s/and`](https://github.com/bhb/expound/issues/102). Expound also now [print messages for specs that use `conform` for coercion](https://github.com/bhb/expound/issues/78). 111 | - [ClassCastException when checking spec against sorted map](https://github.com/bhb/expound/issues/136) 112 | - Incompatibilities with the new `explain-data` format introduced in ClojureScript 1.10.439 and `clojure.spec.alpha` 0.2.176 113 | 114 | 115 | ### Changed 116 | - Replaced Codox docs with [cljdoc](https://cljdoc.xyz/) for [API docs](https://cljdoc.xyz/d/expound/expound) 117 | - `clojure.spec` dependency updated to 0.2.168 118 | - Dropped support for ClojureScript 1.9.562 and 1.9.946 119 | - Deprecated `def` macro (use `defmsg` function instead) 120 | 121 | ## [0.7.1] - 2018-06-25 122 | 123 | ### Fixed 124 | - [Bug with printing alternatives in 'or' or 'alt' specs](https://github.com/bhb/expound/issues/73) 125 | - [Bug with missing elements in `cat` specs](https://github.com/bhb/expound/issues/79) 126 | 127 | ### Changed 128 | - If a problem has a value for `:expound.spec.problem/type` key, `expound.alpha/problem-group-str` must be implemented for that value or Expound will throw an error. 129 | 130 | ## [0.7.0] - 2018-05-28 131 | 132 | ### Fixed 133 | - [Use existing value of `ansi/*enable-color*`](https://github.com/bhb/expound/pull/98) 134 | 135 | ### Added 136 | - Specs and docstrings for public API 137 | - Codox site for documentation 138 | - [3rd-party libraries can extend Expound by setting `:expound.spec.problem/type` on each `clojure.spec` problem and declaring a `defmethod` to implement custom printing](https://github.com/bhb/expound/pull/97/) 139 | 140 | ## [0.6.0] - 2018-04-26 141 | 142 | ### Fixed 143 | - Bug with extra whitespace when "Revelant specs" are not printed 144 | 145 | ### Added 146 | - [Optional colorized output i.e. "themes"](https://github.com/bhb/expound/issues/44) 147 | - [`explain-results` and `explain-results-str` functions print human-optimized output for `clojure.spec.test.alpha/check` results](https://github.com/bhb/expound/issues/72) 148 | 149 | ## [0.5.0] - 2018-02-06 150 | 151 | ### Fixed 152 | - [Bug with displaying errors for `s/or` specs](https://github.com/bhb/expound/issues/64) 153 | - Bug where "should be one of..." values didn't display correctly 154 | 155 | ### Added 156 | - Optional error messages for predicates 157 | 158 | ## [0.4.0] - 2017-12-16 159 | 160 | ### Fixed 161 | - [Bug with including development HTML page in JAR](https://github.com/bhb/expound/issues/60) 162 | 163 | ### Added 164 | - Table of keywords and specs for missing keys 165 | 166 | ### Changed 167 | - Better error message for compound key clauses like `:req-un [(or ::foo ::bar)]` 168 | - Better error message for failures in `cat` specs 169 | 170 | ## [0.3.4] - 2017-11-19 171 | 172 | ### Fixed 173 | 174 | - [Bug with composing multi-specs in other specs](https://github.com/bhb/expound/issues/24) 175 | - [Bug with explaining failures with NaN values](https://github.com/bhb/expound/issues/48) 176 | - [Bug with printing specs which are anonymous functions](https://github.com/bhb/expound/issues/50) 177 | - [Bug with set predicates on different branches of 'alt' or 'or' specs](https://github.com/bhb/expound/issues/36) 178 | 179 | ## [0.3.3] - 2017-11-02 180 | 181 | ### Fixed 182 | - [Bug with non-function values that can be treated as functions](https://github.com/bhb/expound/issues/41) 183 | - Multiple bugs when reporting assertion failures 184 | 185 | ## [0.3.2] - 2017-10-29 186 | 187 | ### Fixed 188 | - Bug where duplicate predicates were printed twice 189 | - [Bug with `fspec` specs](https://github.com/bhb/expound/issues/25) 190 | 191 | ## [0.3.1] - 2017-09-26 192 | 193 | ### Fixed 194 | - [Bug with nested `map-of` or `key` specs](https://github.com/bhb/expound/issues/27) 195 | - [Bug with `(coll-of)` specs with set values](https://github.com/bhb/expound/issues/31) 196 | 197 | ## [0.3.0] - 2017-09-05 198 | 199 | ### Added 200 | - Configurable printers 201 | 202 | ### Fixed 203 | - [Bug with using predicates as specs](https://github.com/bhb/expound/issues/20) 204 | 205 | ## [0.2.1] - 2017-08-16 206 | 207 | ### Fixed 208 | - [Bug with including extraneous compiled Javascript in JAR file](https://github.com/bhb/expound/issues/16) 209 | 210 | ## [0.2.0] - 2017-08-14 211 | 212 | ### Added 213 | - Support for [Orchestra](https://github.com/jeaye/orchestra) instrumentation 214 | 215 | ### Changed 216 | - Pretty-print predicates 217 | - Omit `clojure.core` and `cljs.core` prefix when printing predicates 218 | 219 | ### Fixed 220 | - Append [newline to expound output](https://github.com/bhb/expound/issues/8) 221 | 222 | ## [0.1.2] - 2017-07-22 223 | 224 | ### Added 225 | - [Support for instrumentation](https://github.com/bhb/expound/issues/4) 226 | - [Support for Spec asserts](https://github.com/bhb/expound/issues/5) 227 | 228 | ## [0.1.1] - 2017-07-17 229 | 230 | ### Fixed 231 | - [Bug with loading goog.string/format](https://github.com/bhb/expound/issues/3) 232 | 233 | ## 0.1.0 - 2017-07-12 234 | 235 | ### Added 236 | - `expound` and `expound-str` functions. 237 | 238 | [Unreleased]: https://github.com/bhb/expound/compare/v0.9.0...HEAD 239 | [0.9.0]: https://github.com/bhb/expound/compare/v0.8.10...v0.9.0 240 | [0.8.10]: https://github.com/bhb/expound/compare/v0.8.9...v0.8.10 241 | [0.8.9]: https://github.com/bhb/expound/compare/v0.8.8...v0.8.9 242 | [0.8.8]: https://github.com/bhb/expound/compare/v0.8.7...v0.8.8 243 | [0.8.7]: https://github.com/bhb/expound/compare/v0.8.6...v0.8.7 244 | [0.8.6]: https://github.com/bhb/expound/compare/v0.8.5...v0.8.6 245 | [0.8.5]: https://github.com/bhb/expound/compare/v0.8.4...v0.8.5 246 | [0.8.4]: https://github.com/bhb/expound/compare/v0.8.3...v0.8.4 247 | [0.8.3]: https://github.com/bhb/expound/compare/v0.8.2...v0.8.3 248 | [0.8.2]: https://github.com/bhb/expound/compare/v0.8.1...v0.8.2 249 | [0.8.1]: https://github.com/bhb/expound/compare/v0.8.0...v0.8.1 250 | [0.8.0]: https://github.com/bhb/expound/compare/v0.7.2...v0.8.0 251 | [0.7.2]: https://github.com/bhb/expound/compare/v0.7.0...v0.7.2 252 | [0.7.1]: https://github.com/bhb/expound/compare/v0.7.0...v0.7.1 253 | [0.7.0]: https://github.com/bhb/expound/compare/v0.6.0...v0.7.0 254 | [0.6.0]: https://github.com/bhb/expound/compare/v0.5.0...v0.6.0 255 | [0.5.0]: https://github.com/bhb/expound/compare/v0.4.0...v0.5.0 256 | [0.4.0]: https://github.com/bhb/expound/compare/v0.3.4...v0.4.0 257 | [0.3.4]: https://github.com/bhb/expound/compare/v0.3.3...v0.3.4 258 | [0.3.3]: https://github.com/bhb/expound/compare/v0.3.2...v0.3.3 259 | [0.3.2]: https://github.com/bhb/expound/compare/v0.3.1...v0.3.2 260 | [0.3.1]: https://github.com/bhb/expound/compare/v0.3.0...v0.3.1 261 | [0.3.0]: https://github.com/bhb/expound/compare/v0.2.1...v0.3.0 262 | [0.2.1]: https://github.com/bhb/expound/compare/v0.2.0...v0.2.1 263 | [0.2.0]: https://github.com/bhb/expound/compare/v0.1.2...v0.2.0 264 | [0.1.2]: https://github.com/bhb/expound/compare/v0.1.1...v0.1.2 265 | [0.1.1]: https://github.com/bhb/expound/compare/v0.1.0...v0.1.1 266 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor to control, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expound 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/expound.svg)](https://clojars.org/expound) 4 | [![cljdoc badge](https://cljdoc.org/badge/expound/expound)](https://cljdoc.org/d/expound/expound/CURRENT) 5 | [![CircleCI](https://circleci.com/gh/bhb/expound.svg?style=shield)](https://circleci.com/gh/bhb/expound) 6 | [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/bhb/expound) 7 | 8 | Expound formats `clojure.spec` error messages in a way that is optimized for humans to read. 9 | 10 | For example, Expound will replace a `clojure.spec` error message like: 11 | 12 | ``` 13 | val: {} fails spec: :example/place predicate: (contains? % :city) 14 | val: {} fails spec: :example/place predicate: (contains? % :state) 15 | ``` 16 | 17 | with 18 | 19 | ``` 20 | -- Spec failed -------------------- 21 | 22 | {} 23 | 24 | should contain keys: :city, :state 25 | 26 | | key | spec | 27 | |========+=========| 28 | | :city | string? | 29 | |--------+---------| 30 | | :state | string? | 31 | ``` 32 | 33 | [Comparison with `clojure.spec` error messages](doc/comparison.md) 34 | 35 | Expound is in alpha while `clojure.spec` is in alpha. 36 | 37 | **Expound is supported by [Clojurists Together](https://www.clojuriststogether.org/). If you find this project useful, please consider making a monthly donation to Clojurists Together (or ask your employer to do so).** 38 | 39 | ## Installation 40 | 41 | **If you are using recent versions of ClojureScript, please check the [compatibility guide](doc/compatibility.md)** 42 | 43 | ### Leiningen/Boot 44 | 45 | `[expound "0.9.0"]` 46 | 47 | #### deps.edn 48 | 49 | `expound/expound {:mvn/version "0.9.0"}` 50 | 51 | ### Lumo 52 | 53 | `npm install @bbrinck/expound` 54 | 55 | ## Usage 56 | 57 | [API docs](https://cljdoc.xyz/d/expound/expound/CURRENT) 58 | 59 | ### Quick start via `clj` 60 | 61 | ``` 62 | > brew install clojure/tools/clojure 63 | > clojure -Sdeps '{:deps {friendly/friendly {:git/url "https://gist.github.com/bhb/2686b023d074ac052dbc21f12f324f18" :sha "d532662414376900c13bed9c920181651e1efeff"}}}' -X friendly/run 64 | user=> (require '[expound.alpha :as expound]) 65 | nil 66 | user=> (expound/expound string? 1) 67 | nil 68 | -- Spec failed -------------------- 69 | 70 | 1 71 | 72 | should satisfy 73 | 74 | string? 75 | 76 | ------------------------- 77 | Detected 1 error 78 | user=> 79 | ``` 80 | 81 | ### `expound` 82 | 83 | Replace calls to `clojure.spec.alpha/explain` with `expound.alpha/expound` and to `clojure.spec.alpha/explain-str` with `expound.alpha/expound-str`. 84 | 85 | ```clojure 86 | (require '[clojure.spec.alpha :as s]) 87 | (require '[expound.alpha :as expound]) 88 | 89 | (s/def :example.place/city string?) 90 | (s/def :example.place/state string?) 91 | (s/def :example/place (s/keys :req-un [:example.place/city :example.place/state])) 92 | (expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false}) 93 | ;; -- Spec failed -------------------- 94 | 95 | ;; {:city ..., :state :CO} 96 | ;; ^^^ 97 | 98 | ;; should satisfy 99 | 100 | ;; string? 101 | 102 | ;; ------------------------- 103 | ;; Detected 1 error 104 | ``` 105 | 106 | ### `*explain-out*` 107 | 108 | To use other Spec functions, set `clojure.spec.alpha/*explain-out*` to `expound/printer`. 109 | 110 | ```clojure 111 | (require '[clojure.spec.alpha :as s]) 112 | (require '[expound.alpha :as expound]) 113 | 114 | (s/def :example.place/city string?) 115 | (s/def :example.place/state string?) 116 | 117 | ;; Use `assert` 118 | (s/check-asserts true) ; enable asserts 119 | 120 | ;; Set var in the scope of 'binding' 121 | (binding [s/*explain-out* expound/printer] 122 | (s/assert :example.place/city 1)) 123 | 124 | (set! s/*explain-out* expound/printer) 125 | ;; (or alter-var-root - see doc/faq.md) 126 | (s/assert :example.place/city 1) 127 | 128 | ;; Use `instrument` 129 | (require '[clojure.spec.test.alpha :as st]) 130 | 131 | (s/fdef pr-loc :args (s/cat :city :example.place/city 132 | :state :example.place/state)) 133 | (defn pr-loc [city state] 134 | (str city ", " state)) 135 | 136 | (st/instrument `pr-loc) 137 | (pr-loc "denver" :CO) 138 | 139 | ;; You can use `explain` without converting to expound 140 | (s/explain :example.place/city 123) 141 | ``` 142 | 143 | #### ClojureScript considerations 144 | 145 | 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: 146 | 147 | `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` 148 | 149 | As of [this commit](https://github.com/clojure/clojurescript/commit/5f0fabc65ae7ba201b32cc513a1e5931a80a2bf7#diff-c62a39b0b4b1d81ebd2ed6b5d265e8cf), 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: 150 | 151 | ``` 152 | (require '[cljs.repl :as repl]) 153 | (require '[clojure.spec.alpha :as s]) 154 | (require '[expound.alpha :as expound]) 155 | (set! s/*explain-out* expound/printer) 156 | 157 | (def devtools-error-formatter 158 | "Uses cljs.repl utilities to format ExceptionInfo objects in Chrome devtools console." 159 | #js{:header 160 | (fn [object _config] 161 | (when (instance? ExceptionInfo object) 162 | (let [message (some->> (repl/error->str object) 163 | (re-find #"[^\n]+"))] 164 | #js["span" message]))) 165 | :hasBody (constantly true) 166 | :body (fn [object _config] 167 | #js["div" (repl/error->str object)])}) 168 | (defonce _ 169 | (some-> js/window.devtoolsFormatters 170 | (.unshift devtools-error-formatter))) 171 | ``` 172 | 173 | See [this ticket](https://github.com/bhb/expound/issues/152) for other solutions in the browser. 174 | 175 | ### Printing results for `check` 176 | 177 | 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`. 178 | 179 | ```clojure 180 | (require '[expound.alpha :as expound] 181 | '[clojure.spec.test.alpha :as st] 182 | '[clojure.spec.alpha :as s] 183 | '[clojure.test.check]) 184 | 185 | (s/fdef ranged-rand 186 | :args (s/and (s/cat :start int? :end int?) 187 | #(< (:start %) (:end %))) 188 | :ret int? 189 | :fn (s/and #(>= (:ret %) (-> % :args :start)) 190 | #(< (:ret %) (-> % :args :end)))) 191 | (defn ranged-rand 192 | "Returns random int in range start <= rand < end" 193 | [start end] 194 | (+ start (long (rand (- start end))))) 195 | 196 | (set! s/*explain-out* expound/printer) 197 | ;; (or alter-var-root - see doc/faq.md) 198 | (expound/explain-results (st/check `ranged-rand)) 199 | ;;== Checked user/ranged-rand ================= 200 | ;; 201 | ;;-- Function spec failed ----------- 202 | ;; 203 | ;; (user/ranged-rand -3 0) 204 | ;; 205 | ;;failed spec. Function arguments and return value 206 | ;; 207 | ;; {:args {:start -3, :end 0}, :ret -5} 208 | ;; 209 | ;;should satisfy 210 | ;; 211 | ;; (fn 212 | ;; [%] 213 | ;; (>= (:ret %) (-> % :args :start))) 214 | ``` 215 | 216 | ### Error messages for specs 217 | 218 | #### Adding error messages 219 | 220 | If a value fails to satisfy a predicate, Expound will print the name of the function (or `` 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. 221 | 222 | ```clojure 223 | (s/def :ex/name string?) 224 | (expound/defmsg :ex/name "should be a string") 225 | (expound/expound :ex/name :bob) 226 | ;; -- Spec failed -------------------- 227 | ;; 228 | ;; :bob 229 | ;; 230 | ;; should be a string 231 | ``` 232 | 233 | You can also reuse messages by defining specs in terms of other specs: 234 | 235 | ```clojure 236 | (s/def :ex/string string?) 237 | (expound/defmsg :ex/string "should be a string") 238 | (s/def :ex/city :ex/string) 239 | (expound/expound :ex/city :denver) 240 | ;; -- Spec failed -------------------- 241 | ;; 242 | ;; :denver 243 | 244 | ;; should be a string 245 | ``` 246 | 247 | #### Built-in predicates with error messages 248 | 249 | Expound provides a default set of type-like predicates with error messages. For example: 250 | 251 | ```clojure 252 | (expound/expound :expound.specs/pos-int -1) 253 | ;; -- Spec failed -------------------- 254 | ;; 255 | ;; -1 256 | ;; 257 | ;; should be a positive integer 258 | ``` 259 | 260 | You can see the full list of available specs with `expound.specs/public-specs`. 261 | 262 | ### Printer options 263 | 264 | `expound` and `expound-str` can be configured with options: 265 | 266 | ``` 267 | (expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false :theme :figwheel-theme}) 268 | ``` 269 | 270 | or, to configure the global printer: 271 | 272 | ```clojure 273 | (set! s/*explain-out* (expound/custom-printer {:show-valid-values? true :print-specs? false :theme :figwheel-theme})) 274 | ;; (or alter-var-root - see doc/faq.md) 275 | ``` 276 | 277 | | name | spec | default | description | 278 | |------|------|----------|-------------| 279 | | `:show-valid-values?` | `boolean?` | `false` | If `false`, replaces valid values with `...` (example below) | 280 | | `:value-str-fn` | `ifn?` | provided function | Function to print bad values (example below) | 281 | | `:print-specs?` | `boolean?` | `true` | If true, display "Relevant specs" section. Otherwise, omit that section. | 282 | | `:theme` | `#{:figwheel-theme :none}` | `:none` | Enables color theme. | 283 | 284 | 285 | #### `:show-valid-values?` 286 | 287 | By default, `printer` will omit valid values and replace them with `...` 288 | 289 | ```clojure 290 | (set! s/*explain-out* expound/printer) 291 | ;; (or alter-var-root - see doc/faq.md) 292 | (s/explain :example/place {:city "Denver" :state :CO :country "USA"}) 293 | 294 | ;; -- Spec failed -------------------- 295 | ;; 296 | ;; {:city ..., :state :CO, :country ...} 297 | ;; ^^^ 298 | ;; 299 | ;; should satisfy 300 | ;; 301 | ;; string? 302 | ``` 303 | 304 | You can configure Expound to show valid values: 305 | 306 | ```clojure 307 | (set! s/*explain-out* (expound/custom-printer {:show-valid-values? true})) 308 | ;; (or alter-var-root - see doc/faq.md) 309 | (s/explain :example/place {:city "Denver" :state :CO :country "USA"}) 310 | 311 | ;; -- Spec failed -------------------- 312 | ;; 313 | ;; {:city "Denver", :state :CO, :country "USA"} 314 | ;; ^^^ 315 | ;; 316 | ;; should satisfy 317 | ;; 318 | ;; string? 319 | ``` 320 | 321 | ##### `:value-str-fn` 322 | 323 | You can provide your own function to display the invalid value. 324 | 325 | ```clojure 326 | ;; Your implementation should meet the following spec: 327 | (s/fdef my-value-str 328 | :args (s/cat 329 | :spec-name (s/nilable #{:args :fn :ret}) 330 | :form any? 331 | :path :expound/path 332 | :value any?) 333 | :ret string?) 334 | (defn my-value-str [_spec-name form path value] 335 | (str "In context: " (pr-str form) "\n" 336 | "Invalid value: " (pr-str value))) 337 | 338 | (set! s/*explain-out* (expound/custom-printer {:value-str-fn my-value-str})) 339 | ;; (or alter-var-root - see doc/faq.md) 340 | (s/explain :example/place {:city "Denver" :state :CO :country "USA"}) 341 | 342 | ;; -- Spec failed -------------------- 343 | ;; 344 | ;; In context: {:city "Denver", :state :CO, :country "USA"} 345 | ;; Invalid value: :CO 346 | ;; 347 | ;; should satisfy 348 | ;; 349 | ;; string? 350 | ``` 351 | 352 | ### Manual clojure.test/report override 353 | 354 | 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: 355 | 356 | ```clojure 357 | (ns pkg.test-runner 358 | (:require [clojure.spec.alpha :as s] 359 | [clojure.test :as test :refer-macros [run-tests]] 360 | [expound.alpha :as expound] 361 | ;; require your namespaces here 362 | [pkg.namespace-test])) 363 | 364 | (enable-console-print!) 365 | 366 | (set! s/*explain-out* expound/printer) 367 | ;; (or alter-var-root - see doc/faq.md) 368 | 369 | ;; We try to preserve the clojure.test output format 370 | (defmethod test/report [:cljs.test/default :error] [m] 371 | (test/inc-report-counter! :error) 372 | (println "\nERROR in" (test/testing-vars-str m)) 373 | (when (seq (:testing-contexts (test/get-current-env))) 374 | (println (test/testing-contexts-str))) 375 | (when-let [message (:message m)] (println message)) 376 | (let [actual (:actual m) 377 | ex-data (ex-data actual)] 378 | (if (:clojure.spec.alpha/failure ex-data) 379 | (do (println "expected:" (pr-str (:expected m))) 380 | (print " actual:\n") 381 | (print (.-message actual))) 382 | (test/print-comparison m)))) 383 | 384 | ;; run tests, (stest/instrument) either here or in the individual test files. 385 | (run-tests 'pkg.namespace-test) 386 | ``` 387 | 388 | ### Using Expound as printer for Orchestra 389 | 390 | Use [Orchestra](https://github.com/jeaye/orchestra) with Expound to get human-optimized error messages when checking your `:ret` and `:fn` specs. 391 | 392 | ```clojure 393 | (require '[orchestra.spec.test :as st]) 394 | 395 | (s/fdef location 396 | :args (s/cat :city :example.place/city 397 | :state :example.place/state) 398 | :ret string?) 399 | (defn location [city state] 400 | ;; incorrect implementation 401 | nil) 402 | 403 | (st/instrument) 404 | (set! s/*explain-out* expound/printer) 405 | ;; (or alter-var-root - see doc/faq.md) 406 | (location "Seattle" "WA") 407 | 408 | ;;ExceptionInfo Call to #'user/location did not conform to spec: 409 | ;; form-init3240528896421126128.clj:1 410 | ;; 411 | ;; -- Spec failed -------------------- 412 | ;; 413 | ;; Return value 414 | ;; 415 | ;; nil 416 | ;; 417 | ;; should satisfy 418 | ;; 419 | ;; string? 420 | ;; 421 | ;; ------------------------- 422 | ;; Detected 1 error 423 | ``` 424 | ## Conformers 425 | 426 | 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](https://dev.clojure.org/jira/browse/CLJ-2116). 427 | 428 | If you want to use Expound with conformers, you'll need to write a custom printer. See "Printer options" above. 429 | 430 | ## Related work 431 | 432 | - [Inspectable](https://github.com/jpmonettas/inspectable) - Tools to explore specs and spec failures at the REPL 433 | - [Pretty-Spec](https://github.com/jpmonettas/pretty-spec) - Pretty printer for specs 434 | - [Phrase](https://github.com/alexanderkiel/phrase) - Use specs to create error messages for users 435 | - [Pinpointer](https://github.com/athos/Pinpointer) - spec error reporter based on a precise error analysis 436 | 437 | ## Prior Art 438 | 439 | * Error messages in [Elm](http://elm-lang.org/), in particular the [error messages catalog](https://github.com/elm-lang/error-message-catalog) 440 | * Error messages in [Figwheel](https://github.com/bhauman/lein-figwheel), in particular the config error messages generated from [strictly-specking](https://github.com/bhauman/strictly-specking) 441 | * [Clojure Error Message Catalog](https://github.com/yogthos/clojure-error-message-catalog) 442 | * [The Usability of beginner-oriented Clojure error messages](http://wiki.science.ru.nl/tfpie/images/6/6e/TFPIE16-slides-emachkasova.pdf) 443 | * ["Illuminated Macros" - Chris Houser / Jonathan Claggett](https://www.youtube.com/watch?v=o75g9ZRoLaw) 444 | * [seqex](https://github.com/jclaggett/seqex) 445 | * ["Improving Clojure's Error Messages with Grammars" - Colin Fleming](https://www.youtube.com/watch?v=kt4haSH2xcs) 446 | 447 | ## Contributing 448 | 449 | 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](http://clojurians.net/). 450 | 451 | If you are working on the code, please read the [Development Guide](doc/development.md) 452 | 453 | ## License 454 | 455 | Copyright © 2017-2022 Ben Brinckerhoff 456 | 457 | Distributed under the Eclipse Public License version 1.0, just like Clojure. 458 | -------------------------------------------------------------------------------- /SHASUMS256.txt.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNED MESSAGE----- 2 | Hash: SHA256 3 | 4 | 9b4461a1af6d6fc012db5c580ece88bb72aaf33307974fe7736e83ec4bca1788 node-v14.2.0-aix-ppc64.tar.gz 5 | 2447241aefe71dea8ba1552549e4df2e894d1ac12203630db3af63d4ae35c016 node-v14.2.0-darwin-x64.tar.gz 6 | 3caec491f8f8a46c0c88eeebfff6616c7fdbca9695b1e74cb70354507ac3bfd4 node-v14.2.0-darwin-x64.tar.xz 7 | 16158ba804b9d4877624477b706a82796c5895a1f130eada3546dd6070f76b73 node-v14.2.0-headers.tar.gz 8 | f6ca257360e99cb8158ef9cd432f7620aba4f8635dcf9fe0a9c5da1747fe2614 node-v14.2.0-headers.tar.xz 9 | 2fd9bf7b3fc8a0e72ec27f1d274b8eedd1c81e8af3f82739c787ddc288037a4c node-v14.2.0-linux-arm64.tar.gz 10 | 4587d2c52cd348094bd46ee4ee8cdfeb549462ead9b4aadc9cfc3c5fc3ba7215 node-v14.2.0-linux-arm64.tar.xz 11 | 5d898328e8985cd2714a56800766e27d5dbecf3c7ba953e1df9d155328b3ee76 node-v14.2.0-linux-armv7l.tar.gz 12 | 530df44de700ced3ee09f77c84a9ec75f4b6e2842ae970a71f6082874f84e966 node-v14.2.0-linux-armv7l.tar.xz 13 | 45c6a4edfd3179e9f53fc76faa0bd3c255022e6491d9961d9ff0caca9947bd98 node-v14.2.0-linux-ppc64le.tar.gz 14 | 426aad83e3399c9bb9c5972781eba2536cc2244013ee293bbecd7f15830f76b6 node-v14.2.0-linux-ppc64le.tar.xz 15 | 47843ea36678a898679b934347f2ab4471b227cc088f57a53afd502d37009cf6 node-v14.2.0-linux-s390x.tar.gz 16 | 936acec34a3225c27cea055cd55d775f9b0bfa4c87f8f184c93932058908094d node-v14.2.0-linux-s390x.tar.xz 17 | 3307d8b95014e78b43f85242a03fe3b28edfb90cc15e1d26393dcbbc51d05c8e node-v14.2.0-linux-x64.tar.gz 18 | 468cbd92271da8c0cacaa3fa432a73a332e398bade8ad7359a94aa8ab3cc3cca node-v14.2.0-linux-x64.tar.xz 19 | ade90531fb98d5ba5fb58df42e0e1aebd8c11ae1e67c3c720135887a3431adea node-v14.2.0.pkg 20 | 8c9aa909567589e97a22b2df1cf6a8d61e0a546b4c784703e6722f13da259493 node-v14.2.0.tar.gz 21 | 8efdcc3ae381909cc9c4bd08644481a594e08b5a6a7d05814e1c32b1279e16cf node-v14.2.0.tar.xz 22 | 51cc7f4a712cb969a4153ca5c2ebfe8c052987fa4e025d3d98b1c7b1240f06f2 node-v14.2.0-win-x64.7z 23 | 99085f45a894e257123d7c729113cc00ed1413df432dbdce5fe53867e7c53b11 node-v14.2.0-win-x64.zip 24 | 9d616effae140f8f53b5659f07bb0dd5bc3af00b06dfd649401403416ea0e5b3 node-v14.2.0-win-x86.7z 25 | ec5a318016e91a6bb38adb95f9890a483f70e522f4bf97229fe85eb19cd0dd2e node-v14.2.0-win-x86.zip 26 | f855ba61fad5ba16756b47038a1e4c5cb50685dbe2f5a1876c05fbc7300e6ea9 node-v14.2.0-x64.msi 27 | 82a93917b3025575ce5436c5cba7aead7876a89289f3ed189444a065d8b57324 node-v14.2.0-x86.msi 28 | fc3481a669f071e6a1977ff348ae072f324610dc0a92d051d772b594c6988638 win-x64/node.exe 29 | 0084f3d15cc6ca50db917c684941a85f8c4c901f726e1c74bbe57431a1479211 win-x64/node.lib 30 | 6ef164e08b2edd08240bb3a465726fb801e766166293355153c641a55d815768 win-x64/node_pdb.7z 31 | 8a49eed2e4a93f290d874009a1d96c377495c931b159895bdcadc7fd4554b411 win-x64/node_pdb.zip 32 | 02900d6f56eb7820df1c75400f7bc839df50fe70f326dfa2621055c13bd4a725 win-x86/node.exe 33 | b4287d2e5632595de8078815d3b7cd63396c8674146896e17c736c9fead23eae win-x86/node.lib 34 | 8a9a50dc90a1ec43e0e9ecc890a52441c8395fd14a264808ec71e7e6c848fed3 win-x86/node_pdb.7z 35 | 9b25070a4c10b289fb6cab40a25078ad198d6fbf13e483768387fbdc12d98a51 win-x86/node_pdb.zip 36 | -----BEGIN PGP SIGNATURE----- 37 | 38 | iQIzBAEBCAAdFiEEj8yhP+8dDC6RAI4Jdw96mlrhVgAFAl6xr94ACgkQdw96mlrh 39 | VgAGIA/7BfKeeB0R7n03c+NsoL5XoPzdGVA/ZGJbLrWB0gawIIcYeFv4+AHiSSLy 40 | wmECt/whwgsGUBdtfjXa7uBhD0Cq56sybQfQrIPFIt5xJO5HH/hFaRe0qqHuL0tb 41 | GWzjaNGZawgfS5DXka1fJ1AnqBWMkeaJKQB8owy5Jrzmphd37Pee6Wn+Ik5xSx3h 42 | swNTNn1JyagtorfHdHsaAA2kectcyeUJQ9TOzEdxhbrGBehtGyPYGI1gq+seQsTi 43 | f2IvftJNH8I4b+gwBI8hLE6kuZ6spC+VtWSV8K1LTboDgkroPcdQ6oOhiQW10WV6 44 | XQzwDtFvok03O7yWtaA5mvb1fVgffXCXlytCET3ibiaEtyMblnVnOul7x98MHf6E 45 | p1S2OLuu5Xl63yTXTwmvypppTVRMG+kNCJOWMwf+wz+c2nyfDKmXpaDFsACBKCQm 46 | 92c0vwoTZKLK2xB/cNrnHm2QmfAqGIiDaTURO6XNKtHTDEwlHfAvM9DzzVnA9WDL 47 | q9bGAiAllUExnA4vuTMSsFHrUSnEfoF3+4xpeyZqy3+QWNbnwn5q8XmJWE/jWtLb 48 | 7lmG0DL39kY6IyKupTWjoBtZVLFLGTT/1Xt9KVKbQRhCCRp26aRchVzqKe/neYDq 49 | roC9JLqdcLeE+UEXCGBR+WL8FrAmKZmeRN0mGJwVdTJJZXOOkKE= 50 | =uF86 51 | -----END PGP SIGNATURE----- 52 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /bin/bb_ex.clj: -------------------------------------------------------------------------------- 1 | (ns bb 2 | (:require [babashka.deps :as deps])) 3 | 4 | (deps/add-deps 5 | '{:deps {borkdude/spartan.spec {:git/url "https://github.com/borkdude/spartan.spec" 6 | :sha "d3b4e98ec2b8504868e5a6193515c5d23df15264"} 7 | expound/expound {:local/root "."}}}) 8 | 9 | (require 'spartan.spec 10 | '[expound.alpha :as expound]) 11 | 12 | (expound/expound int? "1") 13 | -------------------------------------------------------------------------------- /bin/build_comparison_doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o xtrace 4 | set -o nounset 5 | set -euo pipefail 6 | 7 | pushd bin && clojure comparison.clj > ../doc/comparison.md 8 | popd 9 | -------------------------------------------------------------------------------- /bin/check-for-warnings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | stdin=$(cat -) 6 | 7 | # ignore expected warning 8 | warnings=$(echo "$stdin") 9 | 10 | if grep -q -i "warning" <(echo $warnings); then 11 | echo "$warnings" | grep -i "warning" 12 | exit 1 13 | else 14 | exit 0 15 | fi 16 | -------------------------------------------------------------------------------- /bin/comparison.clj: -------------------------------------------------------------------------------- 1 | (ns expound.comparison 2 | "Generate markdown for comparison doc" 3 | (:require [clojure.spec.alpha :as s] 4 | [expound.alpha :as expound] 5 | [marge.core :as marge] 6 | [clojure.string :as string])) 7 | 8 | (def examples 9 | [ 10 | { 11 | :header "Nested data structures" 12 | :description "If the invalid value is nested, Expound will help locate the problem" 13 | :specs `((s/def :db/id pos-int?) 14 | (s/def :db/ids (s/coll-of :db/id)) 15 | (s/def :app/request (s/keys :req-un [:db/ids])) 16 | ) 17 | :spec :app/request 18 | :value {:ids [123 "456" 789]} 19 | } 20 | { 21 | :header "Missing keys" 22 | :description "If a key is missing from a map, Expound will display the associated spec" 23 | :specs `((s/def :address.west-coast/city string?) 24 | (s/def :address.west-coast/state #{"CA" "OR" "WA"}) 25 | (s/def :app/address (s/keys :req-un [:address.west-coast/city :address.west-coast/state])) 26 | ) 27 | :spec :app/address 28 | :value {} 29 | } 30 | { 31 | :header "Set-based specs" 32 | :description "If a value doesn't match a set-based spec, Expound will list the possible values" 33 | :specs `((s/def :address.west-coast/city string?) 34 | (s/def :address.west-coast/state #{"CA" "OR" "WA"}) 35 | (s/def :app/address (s/keys :req-un [:address.west-coast/city :address.west-coast/state])) 36 | ) 37 | :spec :app/address 38 | :value {:city "Seattle" :state "ID"} 39 | } 40 | { 41 | :header "Grouping" 42 | :description "Expound will group alternatives" 43 | :specs `((s/def :address.west-coast/zip (s/or :str string? :num pos-int?)) 44 | ) 45 | :spec :address.west-coast/zip 46 | :value :98109 47 | } 48 | { 49 | :header "Predicate descriptions" 50 | :description "If you provide a predicate description, Expound will display them" 51 | :specs '((def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") 52 | (defn valid-email? [s] (re-matches email-regex s)) 53 | (s/def :app.user/email (s/and string? valid-email?)) 54 | (expound/defmsg :app.user/email "should be a valid email address") 55 | ) 56 | :spec :app.user/email 57 | :value "@example.com" 58 | } 59 | 60 | { 61 | :header "Too few elements in a sequence" 62 | :description "If you are missing elements, Expound will describe what must come next" 63 | :specs `( 64 | (s/def :app/ingredient (s/cat :quantity number? :unit keyword?)) 65 | ) 66 | :spec :app/ingredient 67 | :value [100] 68 | } 69 | 70 | { 71 | :header "Too many elements in a sequence" 72 | :description "If you have extra elements, Expound will point out which elements should be removed" 73 | :specs `( 74 | (s/def :app/ingredient (s/cat :quantity number? :unit keyword?)) 75 | ) 76 | :spec :app/ingredient 77 | :value [100 :teaspoon :sugar] 78 | } 79 | ] 80 | ) 81 | 82 | (defn example-with-output [example] 83 | (doseq [form (:specs example)] 84 | (eval form) 85 | ) 86 | (merge 87 | example 88 | { 89 | :spec-output (s/explain-str (:spec example) (:value example)) 90 | :expound-output (expound/expound-str (:spec example) (:value example) {:print-specs? false}) 91 | }) 92 | ) 93 | 94 | (defn code-block [forms] 95 | [:code 96 | {:clojure 97 | (str (->> forms 98 | (map pr-str ) 99 | (map #(string/replace % "clojure.spec.alpha" "s")) 100 | (string/join "\n") 101 | ) 102 | )}]) 103 | 104 | (defn markdown-all [sections] 105 | (->> sections 106 | (mapcat identity) 107 | (map marge/markdown) 108 | (string/join "\n\n"))) 109 | 110 | (defn formatted-example [example] 111 | (let [{:keys [spec-output expound-output specs value header description]} (example-with-output example)] 112 | [ 113 | [:hr] 114 | [:h3 header] 115 | [:p description] 116 | 117 | [:h4 "Specs"] 118 | (code-block specs) 119 | 120 | [:h4 "Value"] 121 | (code-block [value]) 122 | 123 | [:h4 "`clojure.spec` message"] 124 | [:code (string/trim-newline spec-output)] 125 | 126 | [:h4 "Expound message"] 127 | [:code (string/trim-newline expound-output)]])) 128 | 129 | (println 130 | (str 131 | (marge/markdown [:h1 "Comparison"]) 132 | "\n" 133 | (marge/markdown 134 | [:p "Expound's error messages are more verbose than the `clojure.spec` messages, which can help you quickly determine why a value fails a spec. Here are some examples."] 135 | ) 136 | "\n\n" 137 | (markdown-all (map formatted-example examples)))) 138 | -------------------------------------------------------------------------------- /bin/deps.clj: -------------------------------------------------------------------------------- 1 | (require '[clojure.xml :as xml] ) 2 | 3 | (let [deps (->> (xml/parse "../pom.xml") ; read pom 4 | :content ; get top-level tags 5 | (filter #(= (:tag %) :dependencies)) ; find dependencies 6 | first ; get tag 7 | :content ; get children 8 | (map :content) ; get children's children 9 | (remove (fn [dep-tags] ; find anything not in 'test' scope 10 | (some #(and (= (:tag %) :scope) 11 | (= (:content %) ["test"])) dep-tags))) 12 | (map (fn [dep-tags] ; pull out group/name of dependency 13 | [(:content 14 | (first 15 | (filter #(= (:tag %) :groupId) dep-tags))) 16 | (:content 17 | (first 18 | (filter #(= (:tag %) :artifactId) dep-tags)))])) 19 | 20 | sort ; sort 21 | )] 22 | (doseq [dep deps] 23 | (prn dep))) 24 | -------------------------------------------------------------------------------- /bin/deps.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {expound/expound {:local/root ".."} 3 | orchestra/orchestra {:mvn/version "2019.02.06-1"} 4 | org.clojure/test.check {:mvn/version "0.9.0"} 5 | org.clojure/clojure {:mvn/version "1.10.1-beta1"} 6 | marge/marge {:mvn/version "0.16.0"} 7 | }} 8 | -------------------------------------------------------------------------------- /bin/deps_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o xtrace 4 | set -o nounset 5 | set -euo pipefail 6 | 7 | mydir=$(mktemp -d "${TMPDIR:-/tmp/}$(basename $0).XXXXXXXXXXXX") 8 | 9 | lein install 10 | pushd bin && clojure deps.clj > "$mydir/actual_deps.txt" 11 | popd 12 | 13 | diff -u <(cat ./test/expected_deps.txt | perl -pe 's/__\d+\#//g') \ 14 | <(cat "$mydir/actual_deps.txt" | perl -pe 's/__\d+\#//g') 15 | -------------------------------------------------------------------------------- /bin/find-unused-clj.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | for f in $(egrep -o -R "defn?-?( \^\:private)? [^ ]*" * --include '*.cljc' --exclude-dir resources | sed 's/ \^\:private//g' | cut -d \ -f 2 | sort | uniq); do 6 | echo $f $(grep -R --include '*.cljc' --exclude-dir resources -- "$f" * | wc -l); 7 | done | grep " 1$" 8 | -------------------------------------------------------------------------------- /bin/golden_master_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o xtrace 4 | set -o nounset 5 | set -euo pipefail 6 | 7 | mydir=$(mktemp -d "${TMPDIR:-/tmp/}$(basename $0).XXXXXXXXXXXX") 8 | 9 | pushd bin && clojure sample.clj > "$mydir/output.txt" 10 | popd 11 | 12 | diff -u <(cat ./test/expected_sample_out.txt | perl -pe 's/__\d+\#//g') \ 13 | <(cat "$mydir/output.txt" | perl -pe 's/__\d+\#//g') 14 | -------------------------------------------------------------------------------- /bin/inconsistent-aliases: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lumo 2 | 3 | (require '[clojure.string :as string]) 4 | 5 | (def process (js/require "child_process")) 6 | 7 | (process.exec 8 | "ack -ho '\\[.*:as.*\\]' src | sort | uniq" 9 | (fn [err stdout stderr] 10 | (let [msgs (->> stdout 11 | string/split-lines 12 | (group-by (fn [s] 13 | (second (re-find #"\[(.*) :as" s)))) 14 | (filter (fn [[k v]] 15 | (< 1 (count v)))) 16 | (map (fn [[k v]] 17 | (str k " has multiple aliases:\n" 18 | (string/join ", " v)))))] 19 | (doseq [msg msgs] 20 | (println msg))))) 21 | -------------------------------------------------------------------------------- /bin/install-bb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -O https://raw.githubusercontent.com/babashka/babashka/master/install 4 | chmod +x install 5 | sudo ./install 6 | -------------------------------------------------------------------------------- /bin/install-clojure: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | curl -O https://download.clojure.org/install/linux-install-1.9.0.394.sh 4 | chmod +x linux-install-1.9.0.394.sh 5 | sudo ./linux-install-1.9.0.394.sh -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | lein kaocha "$@" -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lumo 2 | 3 | (def process (js/require "child_process")) 4 | 5 | (require '[clojure.string :as string]) 6 | 7 | (process.exec 8 | "git ls-files src | xargs joker --lint" 9 | (fn [err stdout stderr] 10 | (let [msgs (->> stderr 11 | string/split-lines 12 | (remove (fn [s] 13 | (or 14 | (re-find #"Unable to resolve symbol: \*print-namespace-maps\*" s) 15 | (re-find #"unused binding: _" s)))))] 16 | (doseq [msg msgs] 17 | (println msg))))) 18 | -------------------------------------------------------------------------------- /bin/sample.clj: -------------------------------------------------------------------------------- 1 | ;; Usage: cd bin; clj sample.clj --check-results | less -R 2 | ;; or 3 | ;; cd bin; clj sample.clj | less -R 4 | 5 | (ns expound.sample 6 | "Tests are great, but sometimes just skimming output is useful 7 | for seeing how output appears in practice" 8 | (:require [clojure.spec.alpha :as s] 9 | [clojure.spec.test.alpha :as st] 10 | [clojure.main :as main] 11 | [expound.alpha :as expound] 12 | [orchestra.spec.test :as orch.st])) 13 | 14 | (defmacro display-explain [spec value] 15 | `(do 16 | (println "===== " '(s/explain ~spec ~value) " =========") 17 | (s/explain ~spec ~value) 18 | (println "\n\n"))) 19 | 20 | (defmacro display-try [form] 21 | `(try 22 | (println "===== " '~form " =========") 23 | ~form 24 | (catch Exception e# 25 | (println (.getMessage e#))))) 26 | 27 | (defmacro format-spec-err [form] 28 | `(try 29 | ~form 30 | (catch Exception e# 31 | (throw (Exception. (main/err->msg e#)))))) 32 | 33 | (defn go [check-results?] 34 | (set! s/*explain-out* (expound/custom-printer {:theme :figwheel-theme})) 35 | (st/instrument) 36 | (s/check-asserts true) 37 | 38 | (display-explain string? 1) 39 | 40 | (s/def :simple-type-based-spec/str string?) 41 | 42 | (s/def :set-based-spec/tag #{:foo :bar}) 43 | (s/def :set-based-spec/nilable-tag (s/nilable :set-based-spec/tag)) 44 | (s/def :set-based-spec/set-of-one #{:foobar}) 45 | 46 | (s/def :set-based-spec/one-or-two (s/or 47 | :one (s/cat :a #{:one}) 48 | :two (s/cat :b #{:two}))) 49 | 50 | (display-explain :set-based-spec/tag :baz) 51 | (display-explain :set-based-spec/one-or-two [:three]) 52 | (display-explain :set-based-spec/nilable-tag :baz) 53 | (display-explain :set-based-spec/set-of-one :baz) 54 | 55 | (s/def :nested-type-based-spec/str string?) 56 | (s/def :nested-type-based-spec/strs (s/coll-of :nested-type-based-spec/str)) 57 | 58 | (display-explain :nested-type-based-spec/strs ["one" "two" 33]) 59 | 60 | (s/def :nested-type-based-spec-special-summary-string/int int?) 61 | (s/def :nested-type-based-spec-special-summary-string/ints (s/coll-of :nested-type-based-spec-special-summary-string/int)) 62 | 63 | (display-explain :nested-type-based-spec-special-summary-string/ints [1 2 "..."]) 64 | 65 | (s/def :or-spec/str-or-int (s/or :int int? :str string?)) 66 | (s/def :or-spec/vals (s/coll-of :or-spec/str-or-int)) 67 | 68 | (s/def :or-spec/str string?) 69 | (s/def :or-spec/int int?) 70 | (s/def :or-spec/m-with-str (s/keys :req [:or-spec/str])) 71 | (s/def :or-spec/m-with-int (s/keys :req [:or-spec/int])) 72 | (s/def :or-spec/m-with-str-or-int (s/or :m-with-str :or-spec/m-with-str 73 | :m-with-int :or-spec/m-with-int)) 74 | 75 | (display-explain :or-spec/str-or-int :kw) 76 | (display-explain :or-spec/vals [0 "hi" :kw "bye"]) 77 | (display-explain (s/or 78 | :strs (s/coll-of string?) 79 | :ints (s/coll-of int?)) 80 | 50) 81 | 82 | (display-explain 83 | (s/or 84 | :letters #{"a" "b"} 85 | :ints #{1 2}) 86 | 50) 87 | 88 | (display-explain :or-spec/m-with-str-or-int {}) 89 | 90 | (display-explain (s/or :m-with-str1 (s/keys :req [:or-spec/str]) 91 | :m-with-int2 (s/keys :req [:or-spec/str])) {}) (s/def :and-spec/name (s/and string? #(pos? (count %)))) 92 | (s/def :and-spec/names (s/coll-of :and-spec/name)) 93 | 94 | (display-explain :and-spec/name "") 95 | 96 | (display-explain :and-spec/names ["bob" "sally" "" 1]) 97 | 98 | (s/def :coll-of-spec/big-int-coll (s/coll-of int? :min-count 10)) 99 | 100 | (display-explain :coll-of-spec/big-int-coll []) 101 | 102 | (s/def :cat-spec/kw (s/cat :k keyword? :v any?)) 103 | (s/def :cat-spec/set (s/cat :type #{:foo :bar} :str string?)) 104 | (s/def :cat-spec/alt* (s/alt :s string? :i int?)) 105 | (s/def :cat-spec/alt (s/+ :cat-spec/alt*)) 106 | (s/def :cat-spec/alt-inline (s/+ (s/alt :s string? :i int?))) 107 | (s/def :cat-spec/any (s/cat :x (s/+ any?))) ;; Not a useful spec, but worth testing 108 | 109 | (display-explain :cat-spec/kw []) 110 | 111 | (display-explain :cat-spec/set []) 112 | 113 | (display-explain :cat-spec/alt []) 114 | 115 | (display-explain :cat-spec/alt-inline []) 116 | 117 | (display-explain :cat-spec/any []) 118 | 119 | (display-explain :cat-spec/kw [:foo 1 :bar :baz]) 120 | 121 | (s/def :keys-spec/name string?) 122 | (s/def :keys-spec/age int?) 123 | (s/def :keys-spec/user (s/keys :req [:keys-spec/name] 124 | :req-un [:keys-spec/age])) 125 | 126 | (s/def :key-spec/state string?) 127 | (s/def :key-spec/city string?) 128 | (s/def :key-spec/zip pos-int?) 129 | 130 | (s/def :keys-spec/user2 (s/keys :req [(and :keys-spec/name 131 | :keys-spec/age)] 132 | :req-un [(or 133 | :key-spec/zip 134 | (and 135 | :key-spec/state 136 | :key-spec/city))])) 137 | 138 | (s/def :keys-spec/user3 (s/keys :req-un [(or 139 | :key-spec/zip 140 | (and 141 | :key-spec/state 142 | :key-spec/city))])) 143 | 144 | (display-explain :keys-spec/user {}) 145 | 146 | (display-explain :keys-spec/user2 {}) 147 | 148 | (display-explain :keys-spec/user3 {}) 149 | (display-explain (s/keys :req-un [:keys-spec/name :keys-spec/age]) {}) 150 | 151 | (display-explain :keys-spec/user {:age 1 :keys-spec/name :bob}) 152 | 153 | (s/def :multi-spec/value string?) 154 | (s/def :multi-spec/children vector?) 155 | (defmulti el-type :multi-spec/el-type) 156 | (defmethod el-type :text [x] 157 | (s/keys :req [:multi-spec/value])) 158 | (defmethod el-type :group [x] 159 | (s/keys :req [:multi-spec/children])) 160 | (s/def :multi-spec/el (s/multi-spec el-type :multi-spec/el-type)) 161 | 162 | (display-explain :multi-spec/el {}) 163 | (display-explain :multi-spec/el {:multi-spec/el-type :image}) 164 | 165 | (display-explain :multi-spec/el {:multi-spec/el-type :text}) 166 | 167 | (s/def :recursive-spec/tag #{:text :group}) 168 | (s/def :recursive-spec/on-tap (s/coll-of map? :kind vector?)) 169 | (s/def :recursive-spec/props (s/keys :opt-un [:recursive-spec/on-tap])) 170 | (s/def :recursive-spec/el (s/keys :req-un [:recursive-spec/tag] 171 | :opt-un [:recursive-spec/props :recursive-spec/children])) 172 | (s/def :recursive-spec/children (s/coll-of (s/nilable :recursive-spec/el) :kind vector?)) 173 | 174 | (display-explain 175 | :recursive-spec/el 176 | {:tag :group 177 | :children [{:tag :group 178 | :children [{:tag :group 179 | :props {:on-tap {}}}]}]}) 180 | 181 | (s/def :cat-wrapped-in-or-spec/kv (s/and 182 | sequential? 183 | (s/cat :k keyword? :v any?))) 184 | (s/def :cat-wrapped-in-or-spec/type #{:text}) 185 | (s/def :cat-wrapped-in-or-spec/kv-or-string (s/or 186 | :map (s/keys :req [:cat-wrapped-in-or-spec/type]) 187 | :kv :cat-wrapped-in-or-spec/kv)) 188 | 189 | (display-explain :cat-wrapped-in-or-spec/kv-or-string {"foo" "hi"}) 190 | 191 | (s/def :test-assert/name string?) 192 | 193 | (display-try 194 | (s/assert :test-assert/name :hello)) 195 | 196 | (s/fdef test-instrument-adder 197 | :args (s/cat :x int? :y int?) 198 | :fn #(> (:ret %) (-> % :args :x)) 199 | :ret pos-int?) 200 | (defn test-instrument-adder [x y] 201 | (+ x y)) 202 | 203 | (st/instrument `test-instrument-adder) 204 | 205 | (display-try 206 | (format-spec-err 207 | (test-instrument-adder "" :x))) 208 | 209 | (orch.st/instrument `test-instrument-adder) 210 | 211 | (display-try 212 | (test-instrument-adder "" :x)) 213 | 214 | (display-try 215 | (test-instrument-adder 1)) 216 | 217 | (display-try 218 | (test-instrument-adder -1 -2)) 219 | 220 | (display-try 221 | (test-instrument-adder 1 0)) (s/def :alt-spec/int-or-str (s/alt :int int? :string string?)) 222 | (display-explain :alt-spec/int-or-str [:hi]) 223 | 224 | (s/def :duplicate-preds/str-or-str (s/or 225 | ;; Use anonymous functions to assure 226 | ;; non-equality 227 | :str1 #(string? %) 228 | :str2 #(string? %))) 229 | 230 | (display-explain :duplicate-preds/str-or-str 1) 231 | 232 | (s/def :fspec-test/div (s/fspec 233 | :args (s/cat :x int? :y pos-int?))) 234 | 235 | (defn my-div [x y] 236 | (assert (not (zero? (/ x y))))) 237 | 238 | (display-explain :fspec-test/div my-div) 239 | 240 | (display-explain (s/coll-of :fspec-test/div) [my-div]) 241 | 242 | (s/def :fspec-ret-test/my-int pos-int?) 243 | (s/def :fspec-ret-test/plus (s/fspec 244 | :args (s/cat :x int? :y pos-int?) 245 | :ret :fspec-ret-test/my-int)) 246 | 247 | (defn my-plus [x y] 248 | (+ x y)) 249 | 250 | (display-explain :fspec-ret-test/plus my-plus) 251 | 252 | (display-explain (s/coll-of :fspec-ret-test/plus) [my-plus]) 253 | 254 | (s/def :fspec-fn-test/minus (s/fspec 255 | :args (s/cat :x int? :y int?) 256 | :fn (s/and 257 | #(< (:ret %) (-> % :args :x)) 258 | #(< (:ret %) (-> % :args :y))))) 259 | 260 | (defn my-minus [x y] 261 | (- x y)) (display-explain :fspec-fn-test/minus my-minus) 262 | 263 | (display-explain (s/coll-of :fspec-fn-test/minus) [my-minus]) 264 | 265 | (display-explain (s/coll-of (s/fspec :args (s/cat :x int?) :ret int?)) [:foo]) 266 | 267 | (display-explain (s/coll-of (s/fspec :args (s/cat :x int?) :ret int?)) [#{}]) 268 | 269 | (display-explain (s/coll-of (s/fspec :args (s/cat :x int?) :ret int?)) [[]]) (defmulti pet :pet/type) 270 | (defmethod pet :dog [_] 271 | (s/keys)) 272 | (defmethod pet :cat [_] 273 | (s/keys)) 274 | 275 | (defmulti animal :animal/type) 276 | (defmethod animal :dog [_] 277 | (s/keys)) 278 | (defmethod animal :cat [_] 279 | (s/keys)) 280 | 281 | (s/def :multispec-in-compound-spec/pet1 (s/and 282 | map? 283 | (s/multi-spec pet :pet/type))) 284 | 285 | (s/def :multispec-in-compound-spec/pet2 (s/or 286 | :map1 (s/multi-spec pet :pet/type) 287 | :map2 (s/multi-spec animal :animal/type))) 288 | 289 | (display-explain :multispec-in-compound-spec/pet1 {:pet/type :fish}) 290 | 291 | (display-explain :multispec-in-compound-spec/pet2 {:pet/type :fish}) 292 | 293 | (expound/def :predicate-messages/string string? "should be a string") 294 | (expound/def :predicate-messages/vector vector? "should be a vector") 295 | 296 | (display-explain :predicate-messages/string :hello) 297 | 298 | (display-explain (s/or :s :predicate-messages/string 299 | :v :predicate-messages/vector) 1) 300 | 301 | (display-explain (s/or :p pos-int? 302 | :s :predicate-messages/string 303 | :v vector?) 'foo) 304 | (def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") 305 | 306 | (expound/def :predicate-messages/email (s/and string? #(re-matches email-regex %)) "should be a valid email address") 307 | (expound/def :predicate-messages/score (s/int-in 0 100) "should be between 0 and 100") 308 | 309 | (display-explain 310 | :predicate-messages/email 311 | "sally@") 312 | 313 | (display-explain 314 | :predicate-messages/score 315 | 101) 316 | 317 | (println "----- check results -----") 318 | 319 | (when check-results? 320 | (doseq [sym-to-check (st/checkable-syms)] 321 | (println "trying to check" sym-to-check "...") 322 | (try 323 | (st/with-instrument-disabled 324 | (orch.st/with-instrument-disabled 325 | (expound/explain-results (st/check sym-to-check {:clojure.spec.test.check/opts {:num-tests 5}})))) 326 | (catch Exception e 327 | (println "caught exception: " (.getMessage e)))))) 328 | 329 | (s/fdef some-func 330 | :args (s/cat :x int?)) 331 | 332 | (st/with-instrument-disabled 333 | (orch.st/with-instrument-disabled 334 | (expound/explain-results (st/check `some-func)))) (s/fdef results-str-fn1 335 | :args (s/cat :x nat-int? :y nat-int?) 336 | :ret pos-int?) 337 | (defn results-str-fn1 [x y] 338 | (+ x y)) 339 | 340 | (s/fdef results-str-fn2 341 | :args (s/cat :x nat-int? :y nat-int?) 342 | :fn #(let [x (-> % :args :x) 343 | y (-> % :args :y) 344 | ret (-> % :ret)] 345 | (< x ret))) 346 | (defn results-str-fn2 [x y] 347 | (+ x y)) 348 | 349 | (expound/explain-result (st/check-fn `resultsf-str-fn1 (s/spec `results-str-fn2))) 350 | 351 | (s/def ::sorted-pair (s/and (s/cat :x int? :y int?) #(< (-> % :x) (-> % :y)))) 352 | (s/explain ::sorted-pair [1 0]) 353 | ) 354 | 355 | 356 | (go (= "--check-results" (first *command-line-args*))) 357 | (shutdown-agents) 358 | -------------------------------------------------------------------------------- /bin/sample.cljs: -------------------------------------------------------------------------------- 1 | (ns sample 2 | (:require [expound.alpha :as expound :include-macros true])) 3 | 4 | (expound/def ::foo int? "some kind of integer") 5 | 6 | (expound/expound ::foo "test str") 7 | -------------------------------------------------------------------------------- /bin/test-datomic: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #_( 4 | #_DEPS is same format as deps.edn. Multiline is okay. 5 | DEPS=' 6 | {:deps {com.datomic/dev-local {:mvn/version "0.9.225"} 7 | expound/expound {:local/root ".."} 8 | }} 9 | ' 10 | #_You can put other options here 11 | OPTS=' 12 | ' 13 | 14 | exec clojure $OPTS -Sdeps "$DEPS" -M "$0" "$@" 15 | ) 16 | 17 | ;; I can't add this to the normal test suite since I cannot 18 | ;; add the datomic dev-local jar to the github repo (due to licensing issues) 19 | 20 | ;; To run this, you need to get a copy of datomic dev tools. 21 | ;; Instructions here: 22 | ;; https://docs.datomic.com/cloud/dev-local.html#getting-started 23 | ;; 1. Get email 24 | ;; 2. Run ./install within zip 25 | ;; 3. Set up ~/.datomic/dev-local.edn (as described in doc above) 26 | 27 | (require '[expound.alpha :as expound] 28 | '[datomic.client.api :as d] 29 | '[clojure.test :as ct :refer [is testing deftest use-fixtures]] 30 | '[clojure.spec.alpha :as s]) 31 | 32 | (s/def ::foo (s/or :i int? :s string?)) 33 | 34 | (deftest datomic-integration 35 | (let [args {:server-type :dev-local 36 | :system "test" 37 | :db-name "movies" 38 | } 39 | 40 | client (d/client args) 41 | _ (d/create-database client args) 42 | conn (d/connect client args) 43 | db (d/db conn)] 44 | 45 | ;; DB implements map, but can't be walked 46 | (is (= true (map? db))) 47 | (is (= "Success!\n" 48 | (expound/expound-str some? db))) 49 | (is (= "-- Spec failed -------------------- 50 | 51 | [#datomic.core.db.Db{:id \"movies\", :basisT 21, :indexBasisT 0, :index-root-id nil, :asOfT nil, :sinceT nil, :raw nil}] 52 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 53 | 54 | should contain key: :user/foo 55 | 56 | | key | spec | 57 | |===========+=========================| 58 | | :user/foo | (or :i int? :s string?) | 59 | 60 | ------------------------- 61 | Detected 1 error 62 | " 63 | (expound/expound-str (s/cat :db (s/keys 64 | :req [::foo])) [db]))) 65 | 66 | (s/def ::id int?) 67 | (is (= "-- Spec failed -------------------- 68 | 69 | [#datomic.core.db.Db{:id \"movies\", :basisT 21, :indexBasisT 0, :index-root-id nil, :asOfT nil, :sinceT nil, :raw nil}] 70 | ^^^^^^^^ 71 | 72 | should satisfy 73 | 74 | int? 75 | 76 | -- Relevant specs ------- 77 | 78 | :user/id: 79 | clojure.core/int? 80 | 81 | ------------------------- 82 | Detected 1 error\n" 83 | (expound/expound-str 84 | (s/cat :db (s/keys 85 | :req-un [::id])) 86 | [db] 87 | { 88 | :show-valid-values? true 89 | }))) 90 | 91 | (s/def ::id int?) 92 | (is (= "-- Spec failed -------------------- 93 | 94 | [{:indexBasisT ..., 95 | :birth-level ..., 96 | :mid-index ..., 97 | :since ..., 98 | :schema-level ..., 99 | :index ..., 100 | :indexed-datoms ..., 101 | :raw ..., 102 | :basisT ..., 103 | :system-eids ..., 104 | :history ..., 105 | :ids ..., 106 | :indexing ..., 107 | :history? ..., 108 | :elements ..., 109 | :index-rev ..., 110 | :memidx ..., 111 | :keys ..., 112 | :as-of ..., 113 | :id \"movies\", 114 | ^^^^^^^^ 115 | :nextEidx ..., 116 | :sinceT ..., 117 | :t ..., 118 | :index-root-id ..., 119 | :filt ..., 120 | :asOfT ...}] 121 | 122 | should satisfy 123 | 124 | int? 125 | 126 | -- Relevant specs ------- 127 | 128 | :user/id: 129 | clojure.core/int? 130 | 131 | ------------------------- 132 | Detected 1 error 133 | " 134 | (expound/expound-str 135 | (s/cat :db (s/keys 136 | :req-un [::id])) 137 | [db] 138 | { 139 | :show-valid-values? false 140 | }))))) 141 | (ct/run-tests) 142 | (shutdown-agents) 143 | -------------------------------------------------------------------------------- /bin/tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | compile_status="${1:-No status}" 6 | 7 | # FIXME - enable when we fix 8 | # https://github.com/bhb/expound/issues/123 9 | # echo "Running isolated CLJS test" 10 | 11 | # cd bin && clojure -Sdeps '{:deps {org.clojure/clojurescript {:mvn/version "1.10.339"} expound/expound {:local/root ".."}}}' --main cljs.main --repl-env nashorn sample.cljs 12 | 13 | # popd 14 | 15 | echo "Compilation status: $compile_status" 16 | ./node_modules/karma/bin/karma start --single-run 17 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :aliases {;; clj -Atest 3 | :test {:extra-paths ["test"] 4 | :extra-deps {com.cognitect/test-runner {:git/url "https://github.com/cognitect-labs/test-runner.git" 5 | :sha "3cb0a9daf1cb746259dc8309b218f9211ad3b33b"} 6 | org.clojure/test.check {:mvn/version "0.10.0-alpha3"} 7 | org.clojure/clojurescript {:mvn/version "1.10.439"} 8 | com.gfredericks/test.chuck {:mvn/version "0.2.8"} 9 | orchestra/orchestra {:mvn/version "2018.08.19-1"} 10 | org.clojure/core.specs.alpha {:mvn/version "0.1.24"} 11 | ring/ring-core {:mvn/version "1.6.3"} ; required to make ring-spec work, may cause issues with figwheel? 12 | ring/ring-spec {:mvn/version "0.0.4"} ; to test specs 13 | metosin/spec-tools {:mvn/version "0.8.2"} 14 | com.bhauman/spell-spec {:mvn/version "0.1.1"}} 15 | ;; Taken from https://github.com/boot-clj/boot/wiki/Improving-startup-time 16 | :jvm-opts ["-client " "-XX:+TieredCompilation" "-XX:TieredStopAtLevel=1" "-Xmx2g" "-XX:+UseConcMarkSweepGC" "-XX:+CMSClassUnloadingEnabled" "-Xverify:none"] 17 | :main-opts ["-m" "cognitect.test-runner"]} 18 | ;; clj -A:lint:lint/fix 19 | :lint {:extra-deps {com.jameslaverack/cljfmt-runner 20 | {:git/url "https://github.com/JamesLaverack/cljfmt-runner" 21 | :sha "97960e9a6464935534b5a6bab529e063d0027128"}} 22 | :main-opts ["-m" "cljfmt-runner.check"]} 23 | :lint/fix {:main-opts ["-m" "cljfmt-runner.fix"]} 24 | 25 | ;; clojure -A:carve --opts '{:paths ["src" "test"] :api-namespaces [expound.alpha expound.specs]}' 26 | :carve {:extra-deps {borkdude/carve {:git/url "https://github.com/borkdude/carve" 27 | :sha "4b5010a09e030dbd998faff718d12400748ab3b9"}} 28 | :main-opts ["-m" "carve.main"]} 29 | 30 | ;; clojure -A:test:figwheel-repl 31 | ;; open http://localhost:9500/figwheel-extra-main/auto-testing 32 | :figwheel-repl {:extra-paths ["resources" "target"] 33 | :extra-deps {com.bhauman/figwheel-main {:mvn/version "0.1.9"} 34 | com.bhauman/rebel-readline-cljs {:mvn/version "0.1.4"} 35 | ;;;;;;;;;;; test deps ;;;;;;;;;;;;;; 36 | ;; not necessary for tests, but just for legacy karma set up 37 | ;; until I remove it 38 | karma-reporter/karma-reporter {:mvn/version "3.1.0"}} 39 | :main-opts ["-m" "figwheel.main" 40 | "-b" "dev" 41 | "-r"]}}} 42 | -------------------------------------------------------------------------------- /dev.cljs.edn: -------------------------------------------------------------------------------- 1 | {:main expound.alpha} 2 | -------------------------------------------------------------------------------- /dev/user.clj: -------------------------------------------------------------------------------- 1 | (ns user 2 | (:require [clojure.spec.alpha :as s] 3 | [orchestra.spec.test :as st] 4 | [expound.alpha :as expound])) 5 | 6 | (defn setup [] 7 | (set! s/*explain-out* expound/printer) 8 | (st/instrument)) 9 | 10 | (comment 11 | (setup)) 12 | -------------------------------------------------------------------------------- /doc/arch/adr-000.md: -------------------------------------------------------------------------------- 1 | # ADR 000: Using Architecure Decision Records 2 | 3 | ## Context 4 | 5 | Architecture decisions often require thought over a longer period of time. It is useful to document various options and tradeoffs, both as a mechanism for exploring/discussing the solution space, but also as a record of decisions for review at later times. 6 | 7 | [More motivation in the original blog post](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) 8 | 9 | The process of carefully weighing tradeoffs of various options is summarized in [a post](https://data-sorcery.org/2010/12/29/hammock-driven-dev/) on Hammock Driven Development. 10 | 11 | ## Decision 12 | 13 | We will maintain a series of Architecture Decision Records. 14 | 15 | From the ADR blog post: 16 | 17 | > ADRs will be numbered sequentially and monotonically. Numbers will not be reused. 18 | 19 | > If a decision is reversed, we will keep the old one around, but mark it as superseded. (It's still relevant to know that it was the decision, but is no longer the decision.) 20 | 21 | We will use the following outline 22 | 23 | * Title 24 | * Problem Statement 25 | * Context 26 | * Related work (optional) 27 | * Possible solutions (for each solution, list pros/cons) 28 | * Decision 29 | * Status 30 | * Consequences 31 | 32 | ## Status 33 | 34 | Accepted 35 | -------------------------------------------------------------------------------- /doc/arch/adr-001.md: -------------------------------------------------------------------------------- 1 | # ADR 001: Configurable value printers 2 | 3 | ## Problem 4 | 5 | The default value printer omits irrelevant values. While this compacts large values and reduces noise, it can also obscure the location of data by omitting helpful context about the "siblings" of the bad data. 6 | 7 | ## Context 8 | 9 | https://github.com/bhb/expound/issues/18 10 | 11 | More generally, there is some demand for completely different output that would be suitable for showing an outside user (say, a consumer of an API) 12 | 13 | https://clojurians-log.clojureverse.org/clojure-spec/2017-07-17.html#inst-2017-07-17T18:09:19.667294Z 14 | 15 | The current printer was built for finding problems in large data structures coming over the wire. However, in the case of instrumentation, values are often not as deeply nested or as large. 16 | 17 | ## Possible solutions 18 | 19 | ### Make default value printer smarter 20 | 21 | We could only omit data in certain circumstances, based on factors like the nesting level of bad data, and the length of sibling data. 22 | 23 | #### Tradeoffs 24 | * \+ No configuration necessary for users 25 | * \+ If done correctly, adds value for all users 26 | * \- Very hard to get right in all cases 27 | * \- No way to override heuristics 28 | * \- Complicated mental model for users 29 | * \- Printer may change seemingly "at random" if user makes minor change to data 30 | * \- Does not allow configuration of other aspects of `printer` 31 | 32 | ### Use dynamic var 33 | 34 | A `*value-str*` could be set by clients. Expound could provide one or more implementations of this function. 35 | 36 | ##### Tradeoffs 37 | * \+ Easy to implement. Build a new printer that does not omit values, call the new function stored in the dynamic var, add documentation to switch the printer. 38 | * \+ Extensible. Users can easily provide their own implementation 39 | * \+ Easy. Users don't have to understand entire Expound API to change this common configuration point. 40 | * \- `set!` only works if it has already been bound with `binding`, which will not be true of this dynamic var, so users cannot set this globally, only within a `binding`. [Explanation](https://github.com/bhb/expound/issues/19#issuecomment-324507107) 41 | * \- Does not allow more extensive configuration of other aspects of `printer` 42 | * \- Possible "long tail" of many dynamic vars for other configuration aspects 43 | 44 | #### Provide several different `printer` functions 45 | 46 | Expound currently only exposes a single `printer` function. We could expose a new function named `instrumentation-printer` which is better suited for instrumentation. 47 | 48 | ##### Tradeoffs 49 | * \+ Relatively easy to implement. Build a new value printer, pass down the configuration variable. 50 | * \+ Easy - just pick a different printer 51 | * \+ There may be other changes we want to make for the instrumentation printer beyond just changing the value printer 52 | * \+ Users don't need to understand the Expound internals 53 | * \+ `*explain-printer*` works with `set!` as well as `binding` in dev-time contexts like REPL 54 | * \- All changes must be taken together, no "a la carte" configuration 55 | * \- Assumes that this value printer is only useful for instrumentation, when it may vary independently. And naming becomes hard if we just name it based on the feature: `no-omitted-values-printer?`. Or a function that builds a printer (after taking some configuration values)? 56 | 57 | #### Refactor to allow users to construct their own `printer` functions 58 | 59 | We could refactor Expound to include some `core` namespace that would allow users to build their own printers out of the parts. 60 | 61 | ##### Tradeoffs 62 | * \+ Users can modify any part of the message 63 | * \+ Forces simplification and clarification of parts 64 | * \+ Avoids "long tail" of configuration 65 | * \+ Allows easier writing of a number of default printers for various cases 66 | * \+ Easy to set globally, since we are still using `*explain-out` dynamic var that is already bound. 67 | * \- "Simple but not easy" - if a user just wants to not omit values, they have to learn a whole API 68 | * \- Not future-proof. New printers effectively fork the default Expound printer, so they won't get new improvements to Expound printer by default 69 | * \- Bigger public API requires more careful maintainence 70 | * \- Time consuming to build new API that is well-factored and hopefully stable 71 | 72 | #### Add a new "printer builder" function 73 | 74 | Make `printer` (or a new function) take args and return a new printer. 75 | 76 | ##### Tradeoffs 77 | * \+ Still uses global `*explain-out*` function 78 | * \+ Simple: common configuration options can be supported via named args 79 | * \+ Relatively easy to implement 80 | * \+ Users don't need to understand internals of Expound 81 | * \+ Upgrade path is smoother - always set a single dynamic var, but we give you simple way to configure default one 82 | * \+ Multiple configurable printers are possible, each with own options 83 | * \- Might have "long tail" of configuration for default printer 84 | 85 | ## Decision 86 | 87 | Add a new "printer builder" function. 88 | 89 | ## Status 90 | 91 | Accepted 92 | 93 | -------------------------------------------------------------------------------- /doc/arch/adr-002.md: -------------------------------------------------------------------------------- 1 | # ADR 002: Locating non-conforming values in context (with conformers) 2 | 3 | ## Context 4 | 5 | Expound currently has code to highlight the non-conforming value (NCV) within a larger value (the "context"). For instance, given the context `{:city "Denver", :state :CO}` with a spec requiring both city and state to be strings, Expound will print out: 6 | 7 | ``` 8 | {:city ..., :state :CO} 9 | ^^^ 10 | 11 | should satisfy 12 | 13 | string? 14 | ``` 15 | 16 | However, the current solution requires that the non-conforming value exists atomically in the context. This assumption is not true if the spec uses a `conformer` to modify the value. 17 | 18 | For instance, it is possible to use a conformer to verify a string with a regex spec: 19 | 20 | ```clojure 21 | (s/def ::string-AB-seq (s/cat :a #{\A} :b #{\B})) 22 | 23 | (s/def ::string-AB 24 | (s/and 25 | ;; conform as sequence (seq function) 26 | (s/conformer seq) 27 | ;; re-use previous sequence spec 28 | ::string-AB-seq)) 29 | 30 | (s/conform ::string-AB "AC") 31 | ``` 32 | 33 | In this case, the NCV is `\C` but context will not actually contain the character as an atomic value. 34 | 35 | ### How are conformers used in practice? 36 | 37 | Since conformers are [not intended to be used for coercion](https://dev.clojure.org/jira/browse/CLJ-2116?focusedCommentId=45123&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-45123), how do users use conformers in practice? 38 | 39 | - As mentioned above, treating string as regex 40 | - Treating string as collection of values, e.g. collection of pos-ints (this is less powerful, but perhaps more succinct than a regex) 41 | - A string which actually represents a simple int (no collection) 42 | - An int which should be string (e.g. an ID) 43 | - [Converting any collection into a set](https://dev.clojure.org/jira/browse/CLJS-1919) 44 | - [A string that really should be treated as a UUID](http://cjohansen.no/a-unified-specification/) 45 | 46 | It's worth noting that the spec authors don't intend conformers to be used in this way. 47 | 48 | > "The guide page intentionally does not mention conformers as we consider them to be primarily useful for writing new custom composite spec types (not for general data transformation)" - Alex Miller, #clojure-spec, Clojurians slack 49 | 50 | ### Implementation notes 51 | 52 | At least in Clojure (not sure about CLJS), we do get access to the conformer via the 'pred', e.g. 53 | 54 | `(clojure.spec.alpha/conformer expound.problems/numberify clojure.core/str)` 55 | 56 | but this is only in the case where the conformer is included in the pred that fails - if the conforming happens at a level "above" the final failing predicate, we don't see it (see the example specs above) 57 | 58 | 59 | ```clojure 60 | (s/explain-data ::string-AB "AC") 61 | 62 | ;; #:clojure.spec.alpha{:problems [{:path [:b], :pred #{\B}, :val \C, :via [:expound.problems/string-AB :expound.problems/string-AB-seq], :in [1]}], :spec :expound.problems/string-AB, :value "AC"} 63 | ``` 64 | 65 | ### Existing solution 66 | 67 | We recursively walk the context data structure, using the "annotated" path (one with various records like `KeyPathSegment` and `KeyValuePathSegment` to indicate when, for example, the key of a map is invalid). When we reach the end of the path (i.e. there are no more path segments to traverse), we mark the value as "relevant" meaning it is the NCV. 68 | 69 | ## Possible solutions 70 | 71 | ### Modify the existing algorithm 72 | 73 | We could modify the current algorithm to allow extensions to the `summary-form` function such that values could be walked in a custom way e.g. strings could be further walked by index of char. 74 | 75 | This extension point is a bit tricky, since it's not enough to just mark the bad value, we also need to understand the position of the bad value in the context. 76 | 77 | **Tradeoffs** 78 | 79 | - \+ Minimal changes 80 | - \+ We don't have a good sense of how conformers are used, so this defers work until we get more data 81 | - \+ Works for treating single characters as invalid 82 | - \- Premature generalization? I'm not sure this will always work 83 | - \- Not sure it would work for treating groups of characters as invalid 84 | - \- Error message is confusing in the collection->set case (or int to string) case because the NCV won't match the type at all. 85 | - \- Error message is also confusing in some cases e.g. of course "a" is not an int, it's a string, but the issue is that's it's not an int when passed through conformer 'numberify' 86 | 87 | ### Always walk the conformed value 88 | 89 | We could conform the value first, then use the path instead of "in" 90 | 91 | **Tradeoffs** 92 | 93 | - \+ Easier to walk data structure, since it's in the right structure 94 | - \- Harder to understand error, since data structure does not match what was entered 95 | - \- Very verbose in common cases like "alt" and "or", or regex operations. 96 | 97 | ### Don't support transformations via conformers, but at least give a useful error message 98 | 99 | If we can't follow a path segment into a value, we could at least give a better error message. 100 | 101 | **Tradeoffs** 102 | 103 | - \+ Easier to understand than current error message 104 | - \+ Avoids trying to support an unsupported feature 105 | - \- Using conformers for transformation is pretty common, and I'd prefer to allow these users to use Expound 106 | - \- No middle ground - users can't use Expound at all 107 | 108 | ### Don't support transformations via conformers out of the box, but provide a multi-method if users want to enable this 109 | 110 | I think we could check a multimethod if no other patterns match - the multimethod could take the value and the index 111 | 112 | **Tradeoffs** 113 | 114 | - \+ Allows users who want to use conformers a path to work around the issue 115 | - \- Adds implementation complexity in Expound 116 | - \- If the value and the index are not unique (and they may not be, especially if multiple libraries are using Expound), then this strategy won't work. We'd also need some unique identifier like the spec name or something 117 | 118 | ### Just make sure the specific cases I know about don't crash 119 | 120 | Hard-code solutions for things like treating strings like regex, etc, and not support a general mechanism 121 | 122 | **Tradeoffs** 123 | 124 | - \+ Solves the problem for the use-cases I know about 125 | - \+ Allows those users to use Expound without changes 126 | - \- Possible long tail of one-off bugs for an unsupported use case, although I can push back on specific instances 127 | - \- Implmentation complexity 128 | 129 | ### Throw a more helpful exception, provide an example replacement for 'printer' 130 | 131 | `expound/custom-printer` allows users to specify a `:value-str-fn`. I could throw a more useful exception when the default `value-str-fn` fails and provide an example of a custom implementation that works with specific conformers. 132 | 133 | **Tradeoffs** 134 | 135 | - \+ Gives users a more understandable error out of the box 136 | - \+ Users who want to adapt to their custom use case can, and can share solutions 137 | - \- May require a breaking change to `value-str-fn` signature to include all the relevant information (probably just want to pass the annotated problem) 138 | 139 | ## Decision 140 | 141 | I'm going with the solution "Throw a more helpful exception, provide an example replacement for 'printer'". 142 | 143 | 144 | ## Status 145 | 146 | Accepted 147 | -------------------------------------------------------------------------------- /doc/arch/adr-003.md: -------------------------------------------------------------------------------- 1 | # ADR 003: Easier editing of error messages 2 | 3 | ## Context 4 | 5 | ### Expound only returns strings 6 | 7 | Besides a fixed set of [printer options](https://github.com/bhb/expound#printer-options), users can only manipulate error messages by parsing and manipulating the string returned by `expound-str`. 8 | 9 | ### Expound locates "problem" values within a "context" value 10 | 11 | Expound error messages display the "problem" value within the entire "context" value. For instance, if `"456"` should be an integer within `{:ids [123 "456" 789]}`, Expound will print: 12 | 13 | ``` 14 | {:ids [... "456" ...]} 15 | ^^^^^ 16 | 17 | should satisfy 18 | 19 | int? 20 | ``` 21 | 22 | The entire context value is invalid because the problem value is invalid. 23 | 24 | ## Problem Statement 25 | 26 | Manipulating Expound error messages by parsing strings is error prone and brittle. Expound does not consider the format of the error messages to be part of the API and so changes that break client string-parsing code is likely to break at some point. 27 | 28 | Why might a user want to manipulate the error message? 29 | 30 | * Shrinking the context value to create shorter error messages 31 | * Shrinking the problem value to create shorter error messages 32 | * Adding data to the context value to help locate the problem value 33 | * [Adding additional information](https://github.com/bhb/expound/issues/148) 34 | 35 | ### Specific use cases 36 | 37 | #### [#129](https://github.com/bhb/expound/issues/129) 38 | 39 | When a map matches some user-defined pattern (e.g. contains some keys) and/or when the problem is of a certain problem type (e.g. the "missing keys" type, which currently shows a table of missing keys), Expound should map information that is not relevant. 40 | 41 | For instance, it's possible the following error message is unnecessarily long: 42 | 43 | ``` 44 | {:tag ..., 45 | :children [{:tag ..., :children [{:tag :group, :props {:on-tap {}}}]}]} 46 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 47 | 48 | should satisfy 49 | 50 | nil? 51 | ``` 52 | 53 | since the nested value `:on-tap {}` adds no information about the non-nilness of `{:tag :group, :props {:on-tap {}}}` 54 | 55 | Considerations: 56 | 57 | * For some problem types, hiding the number of keys could hide relevant information - e.g. a predicate that checked length of map 58 | * Some information about the value may not just be used to understand why it failed predicate, but also to locate the value. For instance, consider an array with many maps, in which every map contains an `:id` key - we may not want to hide the `:id` value if it helps user locate the invalid value. 59 | 60 | #### [#110](https://github.com/bhb/expound/issues/110) 61 | 62 | When sequences are long, the error message is long due to repeated "..." values. For instance, 63 | 64 | ``` 65 | [... 66 | ... 67 | ... 68 | ... 69 | ... 70 | ... 71 | ... 72 | ... 73 | 33 ] 74 | ^^ 75 | 76 | should satisfy 77 | 78 | string? 79 | ``` 80 | 81 | could potentially be minimized to: 82 | 83 | ``` 84 | [... 85 | < 7 more > 86 | 33 ] 87 | ^^ 88 | 89 | should satisfy 90 | 91 | string? 92 | ``` 93 | 94 | Considerations: 95 | 96 | * This depends on value, not necessarily on type of problem 97 | * For certain values with many values that are similar, only showing the failing value is insufficient, but the `print-valid-values` option shows too much data. 98 | * This could be solved via a user-provided whitelist of values to show 99 | * Perhaps add notion of "distance" from problem? Does this depend entirely on type of data? 100 | 101 | #### [#108](https://github.com/bhb/expound/issues/108) 102 | 103 | When value is very large (especially nested values), printing the "upper" problem values creates very long error messages. 104 | 105 | #### [#148](https://github.com/bhb/expound/issues/148) 106 | 107 | Given the values and the error, allow user to inject content between value and the error (or really, anywhere) 108 | 109 | #### "Embed spec errors" (No issue filed) 110 | 111 | > My case: user creates a router, a lot of rules are run (syntax, conflicts, dependencies, route data specs etc.). Spec validation is just one part and… I would like to return enought context information what and where it failed. Currently: 112 | 113 | ``` 114 | (require '[reitit.core :as r]) 115 | (require '[clojure.spec.alpha :as s]) 116 | (require '[expound.alpha :as e]) 117 | 118 | (s/def ::role #{:admin :manager}) 119 | (s/def ::roles (s/coll-of ::role :into #{})) 120 | 121 | (r/router 122 | ["/api" {:handler identity 123 | ::roles #{:adminz}}] 124 | {::rs/explain e/expound-str 125 | :validate rs/validate-spec!}) 126 | ; CompilerException clojure.lang.ExceptionInfo: Invalid route data: 127 | ; 128 | ; -- On route ----------------------- 129 | ; 130 | ; "/api" 131 | ; 132 | ; -- Spec failed -------------------- 133 | ; 134 | ; {:handler ..., :user/roles #{:adminz}} 135 | ; ^^^^^^^ 136 | ; 137 | ; should be one of: `:admin`,`:manager` 138 | ; 139 | ``` 140 | 141 | Given some context, add additional context around the spec. 142 | 143 | #### Customize printing for record or value (No issue filed) 144 | 145 | > Can I prevent Expound from printing a certain record? It generates something like 140k lines of output which is not helpful at all. I defined a `print-method` so that prints something simple with `prn` but that does not seem to affect Expound. 146 | 147 | Given some value (or properties of value), allow user to specify custom output (to shrink size of output). 148 | 149 | ### Non-problems 150 | 151 | * Minor formatting tweaks to error messages e.g. changes in indentation 152 | * No requests to change the specific characters i.e. hide values with ",,," instead of "." or underline with "---" instead of "^^^" 153 | 154 | ## Constraints 155 | 156 | It's not clear that users would use an solution that allowed for easier editing of error messages: they might instead just want more fixes or features that solve the problem automatically. 157 | 158 | If Expound tries to do the right thing in all cases, it will get complex to maintain and to use since users will have to guess how/why it's showing or omitting certain information. 159 | 160 | ## Prior Art 161 | 162 | ### [Hiccup](https://github.com/weavejester/hiccup) 163 | 164 | Hiccup is a data format for representing HTML 165 | 166 | **Tradeoffs in the context of this problem:** 167 | 168 | * \+ Optional second element (map) is a place to add classes and IDs, which add semantic meaning 169 | * \- Not intended to cover aspects like indentation and whitespace, since HTML/CSS is responsible for that 170 | 171 | ### [Fipp](https://github.com/brandonbloom/fipp/blob/master/doc/primitives.md)
 172 | Fipp is a better pretty printer for Clojure and ClojureScript 173 | 174 | **Tradeoffs in the context of this problem:** 175 | 176 | * \+ Focused on pretty printing, so primitives reflect that 177 | * \- No place to add meaning which would allow users to find content based on ID or class 178 | * ? Potentially more general than this particular problem requires 179 | * ? Fipp is very fast, but Expound doesn't require that much speed 180 | 181 | ### [Clojure pretty printer](https://clojure.github.io/clojure/doc/clojure/pprint/PrettyPrinting.html) 182 | 183 | Expound currently uses Clojure's built-in pretty-print to display values. 184 | 185 | > More advanced formats, including formats that don't look like Clojure data at all like XML and JSON, can be rendered by creating custom dispatch functions. 186 | 187 | - 188 | 189 | Flowchart of how to use 190 | 191 | * \- Need slightly different implementation for ClojureScript and Clojure 192 | * \- Seems to only work on types, need a new type for each role in data structure 193 | * \+ Wouldn't have to worry about writing layout engine 194 | * \+ Configurable by users if they didn't like how Expound formats tree 195 | 196 | ## Possible solutions: general approach 197 | 198 | ### Include Fipp 199 | 200 | Expound could depend on Fipp for pretty-printing. All functions that generate a string would return Fipp data structures. 201 | 202 | **Tradeoffs** 203 | 204 | * \+ Don't need to reimplement algorithm 205 | * \+ Can rely on existing documentation for data primitives 206 | * ? Does Fipp provide code to extract/manipulate the primitives? Do we want to provide this so Expound clients can manipulate data structure? 207 | * \- Take on two new deps: Fipp and `org.clojure/core.rrb-vector`. 208 | * Fipp is well-maintained, but there are complex issues like [this](https://github.com/brandonbloom/fipp/issues/60) 209 | * \- Doesn't provide way to add semantic information about elements beyond formatting 210 | * \- Requires a wrapping layer if we don't want clients to be exposed to Fipp internals 211 | 212 | ### Write a custom data description 213 | 214 | I could come up with my own custom data language to describe the errors. 215 | 216 | * \- Need to write more code 217 | * \+ Can make it custom to error messages 218 | * \- Need to write more documentation for format 219 | * \+ Creates a more stable API for changes (format is stable, although content of strings are not) 220 | * \- Creates a more complex API (more functions) 221 | * \- Requires more testing for backwards compatibility 222 | * \+ Speeds up building internal features 223 | * \+ Provides solution for advanced users that doesn't rely on string manipulation 224 | * Encourages libraries to use Expound as dependency 225 | * Provides temporary solution for advanced use cases (can still build configuration if I want, but not necessary for all cases) 226 | * May provide long-term fix for use cases I don't want to move into Expound 227 | 228 | ### Don't expose data description 229 | 230 | I could make an internal data API that is not exposed to users. 231 | 232 | **Tradeoffs** 233 | 234 | * \+ No need to add new functions (which won't be used my most users anyway) 235 | * \+ Still allows faster iteration on internal features 236 | * \- If I want to solve the issues linked above, I'll need to either add complexity to the Expound rules OR add more configuration 237 | * Additional configuration adds potential for bugs as number of combinations grows 238 | * I'm not sure users use the configuration I expose now 239 | * \+ No additional API to test for breaking changes 240 | * \- String format becomes an API that users cannot rely on 241 | * \- Does not encourage tools to use Expound internally 242 | 243 | ## Possible solutions: data langauge for displaying problem and context values 244 | 245 | ### Return original value plus mapping from `:in` path to value 246 | 247 | It's tricky to embed metadata within the original data, so we provide a lookup table that provides information to assist with formatting. 248 | 249 | We also need to pass along the category of failure because we might alter the output. 250 | 251 | The deal-breaker here is that I can't meaningfully traverse the original data structure in order and get relevant metadata. Metadata needs to be included because there's no mechanism to look it up later. 252 | 253 | ### Use Clojure metadata 254 | 255 | [Clojure's built-in metadata](https://clojure.org/reference/metadata) can be added to collections and symbols, whereas problem and context values can be (and contain) scalars, so metadata won't work for this problem. 256 | 257 | ### Wrap all data in custom metadata 258 | 259 | The format is TBD, but the idea is to wrap each piece of data in a map that adds additional attributes that are used to control how it is displayed. 260 | 261 | * "Role": is the data a collection, a key, an index, or a scalar? 262 | * A collection is always printed 263 | * A key is always printed (this helps locate the problem value) 264 | * A index *may* be printed (if it is on the `:in` path of the problem value) 265 | * A scalar will be printed if it is the problem value, otherwise it will not 266 | * "Distance": How "far away" is the value from the problem value? 267 | * Generally values that are closer will be printed to help locate the problem value 268 | * "Depth": How "deep" is the value relative to the problem value? 269 | * When printing the problem value, we may want to only printed nested values to a certain depth 270 | 271 | ## Decision 272 | 273 | ## Status 274 | 275 | Draft 276 | -------------------------------------------------------------------------------- /doc/cljdoc.edn: -------------------------------------------------------------------------------- 1 | {:cljdoc.doc/tree [["Readme" {:file "README.md"}] 2 | ["Changelog" {:file "CHANGELOG.md"}] 3 | ["Compatibility " {:file "doc/compatibility.md"}] 4 | ["Comparison" {:file "doc/comparison.md"}] 5 | ["Prior Art" {:file "doc/prior_art.md"}] 6 | ["FAQ" {:file "doc/faq.md"}]]} 7 | -------------------------------------------------------------------------------- /doc/cljtogether/project_update1.md: -------------------------------------------------------------------------------- 1 | # Project Update 1: 2019-12-01 - 2019-12-15 2 | 3 | I've been focusing on working through the backlog of bugs. I fixed three bugs: 4 | 5 | * Bug with printing failures for multi-specs. 6 | * Bug with registering message for set-based specs 7 | * Bug with duplicate custom messages in `alt` or `or` specs 8 | 9 | These three bugs are the last known bugs in Expound (not including one bug that is blocked on a `clojure.spec` bug). 10 | 11 | I've released Expound 0.8.2 which includes these fixes. -------------------------------------------------------------------------------- /doc/cljtogether/project_update2.md: -------------------------------------------------------------------------------- 1 | # Project Update 2: 2019-12-16 - 2019-12-31 2 | 3 | Over the past year or so, I've received a number of feature requests that fall into the following two categories: 4 | 5 | * users want to embed Expound messages in a larger error message or insert extra information into the Expound message 6 | * users want to modify how the invalid value is printed 7 | 8 | Most of my time has been spent doing some design thinking on how I could support these features. 9 | 10 | In addition, I've completed a few small tasks: 11 | 12 | * fixed several boxed math warnings 13 | * fixed a bug with using Expound in ClojureScript 14 | 15 | I've released Expound 0.8.3 and 0.8.4 which includes these fixes. -------------------------------------------------------------------------------- /doc/cljtogether/project_update3.md: -------------------------------------------------------------------------------- 1 | # Project Update 3: 2020-01-01 - 2020-01-15 2 | 3 | 4 | #### Improving the display of "context" values 5 | 6 | Expound error messages display the specific problem within the context of the entire invalid value. For instance, if `"456"` should be an integer within `{:ids [123 "456" 789]}`, Expound will print: 7 | 8 | ``` 9 | {:ids [... "456" ...]} 10 | ^^^^^ 11 | 12 | should satisfy 13 | 14 | int? 15 | ``` 16 | 17 | There are a number of unresolved issues in Expound that all relate to how Expound prints the "context" value - either by showing too much information (which creates very long error messages) or showing too little (which hides important information about where an invalid value is located). 18 | 19 | I've been doing a lot of design work (writing an ADR and spiking some code) on how I might resolve such issues. 20 | 21 | #### spec-alpha2 support 22 | 23 | Separately, I've been beginning to lay some early foundations for `expound.alpha2` which will support `spec-alpha2` 24 | 25 | * Created a new Expound namespace 26 | * Created very basic test that old and new namespaces can be loaded with the corresponding version of spec 27 | * Read up on `spec-alpha2` documentation 28 | -------------------------------------------------------------------------------- /doc/cljtogether/project_update4.md: -------------------------------------------------------------------------------- 1 | # Project Update 4: 2020-01-15 - 2020-01-31 2 | 3 | 4 | #### Improving the display of "context" values 5 | 6 | I've continued work on the problem of how to allow users to adapt the error messages for their specific use case. 7 | 8 | I drafted an [ADR](https://github.com/bhb/expound/blob/master/doc/arch/adr-003.md) (still a work in progress) and spent time spiking out some solutions. 9 | 10 | This is a fairly tricky problem and I certainly have lots to learn about designing a "data API". If anyone has tips, ideas, or links to prior work, please leave a comment on the [GitHub issue](https://github.com/bhb/expound/issues/189) or start a discussion in the `#expound` channel on [Clojurians Slack](http://clojurians.net). -------------------------------------------------------------------------------- /doc/cljtogether/project_update5.md: -------------------------------------------------------------------------------- 1 | # Project Update 5: 2020-02-01 - 2020-02-15 2 | 3 | 4 | I'm taking a break from designing [a better way to customize Expound error messages](https://github.com/bhb/expound/issues/189) to work on a version of Expound that will work with `clojure/spec-alpha2` AKA `clojure.alpha.spec` AKA `spec2`. 5 | 6 | The new Expound namespace is `expound.alpha2`. This version will only work with `spec2` and will NOT be backwards compatible with `expound.alpha`. Both versions will coexist in the same JAR, so you can use whichever one you want, depending on the version of Spec you use. 7 | 8 | In addition to supporting `spec2`, `expound.alpha2` will include a number of changes (all of which are subject to change): 9 | 10 | * [Hide "Relevant specs" section by default](https://twitter.com/bbrinck/status/1204595098207444993) 11 | * Remove deprecated `expound/def` macro (you can still use `defmsg`) 12 | * Make option names more consistent: one option includes the verb "show" while another includes "print" and I don't think there's any meaningful difference 13 | * Remove headers like "Spec failed" which add almost no information 14 | * Include new API for customizing error messages 15 | * Rework internal multi-methods and namespaces to simplify code 16 | 17 | I'm happy to report that my `spec2` branch has a few passing tests, only 85 or so failing tests to go. 18 | -------------------------------------------------------------------------------- /doc/cljtogether/project_update6.md: -------------------------------------------------------------------------------- 1 | # Project Update 5: 2020-02-16 - 2020-02-29 2 | 3 | I've been working on `expound.alpha2` and I'm happy to report I have an early version that works with `clojure.alpha.spec` AKA `spec2`. 4 | 5 | If you are experimenting with `spec2` and want to use Expound, you can use commits from the [`spec2` branch](https://github.com/bhb/expound/pull/186). I won't be releasing JARs for some time, but you can use `tools.deps` or [`lein-git-down`](https://github.com/reifyhealth/lein-git-down) to depend on specific commits. 6 | 7 | Here's an example using the "movie-times-user" example from the ["Schema and select" wiki page](https://github.com/clojure/spec-alpha2/wiki/Schema-and-select) 8 | 9 | ```clojure 10 | (ns example) 11 | (require '[clojure.alpha.spec :as s]) 12 | (require '[expound.alpha2 :as expound]) 13 | 14 | (s/def ::street string?) 15 | (s/def ::city string?) 16 | (s/def ::state string?) ;; simplified 17 | (s/def ::zip int?) ;; simplified 18 | (s/def ::addr (s/schema [::street ::city ::state ::zip])) 19 | (s/def ::id int?) 20 | (s/def ::first string?) 21 | (s/def ::last string?) 22 | (s/def ::user (s/schema [::id ::first ::last ::addr])) 23 | (s/def ::movie-times-user (s/select ::user [::id ::addr {::addr [::zip]}])) 24 | 25 | (s/explain ::movie-times-user {}) 26 | ;; {} - failed: (fn [m] (contains? m :example/id)) spec: :example/movie-times-user 27 | ;; {} - failed: (fn [m] (contains? m :example/addr)) spec: :example/movie-times-user 28 | 29 | (expound/expound ::movie-times-user {}) 30 | 31 | ;;-- Spec failed -------------------- 32 | ;; 33 | ;; {} 34 | ;; 35 | ;;should satisfy 36 | ;; 37 | ;; (fn [m] (contains? m :example/id)) 38 | ;; 39 | ;;or 40 | ;; 41 | ;; (fn [m] (contains? m :example/addr)) 42 | ;; 43 | ;;-- Relevant specs ------- 44 | ;; 45 | ;;:example/movie-times-user: 46 | ;; (clojure.alpha.spec/select 47 | ;; :example/user 48 | ;; [:example/id :example/addr #:example{:addr [:example/zip]}]) 49 | ;; 50 | ;;———————————— 51 | ;;Detected 1 error 52 | ``` 53 | 54 | As you can see, there are many rough edges, but it should work for common cases. Please feel free to [report issues](https://github.com/bhb/expound/issues)! 55 | 56 | ### Caveats 57 | 58 | * The API for `expound.alpha2` is in flux: breaking changes are expected! 59 | * No ClojureScript support 60 | * `fspec` is currently broken 61 | * Many new features in spec2 may not work, but most of the features ported from spec1 should work fine (except `fspec`, see above) -------------------------------------------------------------------------------- /doc/comparison.md: -------------------------------------------------------------------------------- 1 | # Comparison 2 | 3 | Expound's error messages are more verbose than the `clojure.spec` messages, which can help you quickly determine why a value fails a spec. Here are some examples. 4 | 5 | 6 | --- 7 | 8 | ### Nested data structures 9 | 10 | 11 | If the invalid value is nested, Expound will help locate the problem 12 | 13 | 14 | #### Specs 15 | 16 | 17 | ```clojure 18 | (s/def :db/id clojure.core/pos-int?) 19 | (s/def :db/ids (s/coll-of :db/id)) 20 | (s/def :app/request (s/keys :req-un [:db/ids])) 21 | ``` 22 | 23 | #### Value 24 | 25 | 26 | ```clojure 27 | {:ids [123 "456" 789]} 28 | ``` 29 | 30 | #### `clojure.spec` message 31 | 32 | 33 | ``` 34 | "456" - failed: pos-int? in: [:ids 1] at: [:ids] spec: :db/id 35 | ``` 36 | 37 | #### Expound message 38 | 39 | 40 | ``` 41 | -- Spec failed -------------------- 42 | 43 | {:ids [... "456" ...]} 44 | ^^^^^ 45 | 46 | should satisfy 47 | 48 | pos-int? 49 | 50 | ------------------------- 51 | Detected 1 error 52 | ``` 53 | 54 | --- 55 | 56 | ### Missing keys 57 | 58 | 59 | If a key is missing from a map, Expound will display the associated spec 60 | 61 | 62 | #### Specs 63 | 64 | 65 | ```clojure 66 | (s/def :address.west-coast/city clojure.core/string?) 67 | (s/def :address.west-coast/state #{"CA" "WA" "OR"}) 68 | (s/def :app/address (s/keys :req-un [:address.west-coast/city :address.west-coast/state])) 69 | ``` 70 | 71 | #### Value 72 | 73 | 74 | ```clojure 75 | {} 76 | ``` 77 | 78 | #### `clojure.spec` message 79 | 80 | 81 | ``` 82 | {} - failed: (contains? % :city) spec: :app/address 83 | {} - failed: (contains? % :state) spec: :app/address 84 | ``` 85 | 86 | #### Expound message 87 | 88 | 89 | ``` 90 | -- Spec failed -------------------- 91 | 92 | {} 93 | 94 | should contain keys: :city, :state 95 | 96 | | key | spec | 97 | |========+===================| 98 | | :city | string? | 99 | |--------+-------------------| 100 | | :state | #{"CA" "WA" "OR"} | 101 | 102 | ------------------------- 103 | Detected 1 error 104 | ``` 105 | 106 | --- 107 | 108 | ### Set-based specs 109 | 110 | 111 | If a value doesn't match a set-based spec, Expound will list the possible values 112 | 113 | 114 | #### Specs 115 | 116 | 117 | ```clojure 118 | (s/def :address.west-coast/city clojure.core/string?) 119 | (s/def :address.west-coast/state #{"CA" "WA" "OR"}) 120 | (s/def :app/address (s/keys :req-un [:address.west-coast/city :address.west-coast/state])) 121 | ``` 122 | 123 | #### Value 124 | 125 | 126 | ```clojure 127 | {:city "Seattle", :state "ID"} 128 | ``` 129 | 130 | #### `clojure.spec` message 131 | 132 | 133 | ``` 134 | "ID" - failed: #{"CA" "WA" "OR"} in: [:state] at: [:state] spec: :address.west-coast/state 135 | ``` 136 | 137 | #### Expound message 138 | 139 | 140 | ``` 141 | -- Spec failed -------------------- 142 | 143 | {:city ..., :state "ID"} 144 | ^^^^ 145 | 146 | should be one of: "CA", "OR", "WA" 147 | 148 | ------------------------- 149 | Detected 1 error 150 | ``` 151 | 152 | --- 153 | 154 | ### Grouping 155 | 156 | 157 | Expound will group alternatives 158 | 159 | 160 | #### Specs 161 | 162 | 163 | ```clojure 164 | (s/def :address.west-coast/zip (s/or :str clojure.core/string? :num clojure.core/pos-int?)) 165 | ``` 166 | 167 | #### Value 168 | 169 | 170 | ```clojure 171 | :98109 172 | ``` 173 | 174 | #### `clojure.spec` message 175 | 176 | 177 | ``` 178 | :98109 - failed: string? at: [:str] spec: :address.west-coast/zip 179 | :98109 - failed: pos-int? at: [:num] spec: :address.west-coast/zip 180 | ``` 181 | 182 | #### Expound message 183 | 184 | 185 | ``` 186 | -- Spec failed -------------------- 187 | 188 | :98109 189 | 190 | should satisfy 191 | 192 | string? 193 | 194 | or 195 | 196 | pos-int? 197 | 198 | ------------------------- 199 | Detected 1 error 200 | ``` 201 | 202 | --- 203 | 204 | ### Predicate descriptions 205 | 206 | 207 | If you provide a predicate description, Expound will display them 208 | 209 | 210 | #### Specs 211 | 212 | 213 | ```clojure 214 | (def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") 215 | (defn valid-email? [s] (re-matches email-regex s)) 216 | (s/def :app.user/email (s/and string? valid-email?)) 217 | (expound/defmsg :app.user/email "should be a valid email address") 218 | ``` 219 | 220 | #### Value 221 | 222 | 223 | ```clojure 224 | "@example.com" 225 | ``` 226 | 227 | #### `clojure.spec` message 228 | 229 | 230 | ``` 231 | "@example.com" - failed: valid-email? spec: :app.user/email 232 | ``` 233 | 234 | #### Expound message 235 | 236 | 237 | ``` 238 | -- Spec failed -------------------- 239 | 240 | "@example.com" 241 | 242 | should be a valid email address 243 | 244 | ------------------------- 245 | Detected 1 error 246 | ``` 247 | 248 | --- 249 | 250 | ### Too few elements in a sequence 251 | 252 | 253 | If you are missing elements, Expound will describe what must come next 254 | 255 | 256 | #### Specs 257 | 258 | 259 | ```clojure 260 | (s/def :app/ingredient (s/cat :quantity clojure.core/number? :unit clojure.core/keyword?)) 261 | ``` 262 | 263 | #### Value 264 | 265 | 266 | ```clojure 267 | [100] 268 | ``` 269 | 270 | #### `clojure.spec` message 271 | 272 | 273 | ``` 274 | () - failed: Insufficient input at: [:unit] spec: :app/ingredient 275 | ``` 276 | 277 | #### Expound message 278 | 279 | 280 | ``` 281 | -- Syntax error ------------------- 282 | 283 | [100] 284 | 285 | should have additional elements. The next element ":unit" should satisfy 286 | 287 | keyword? 288 | 289 | ------------------------- 290 | Detected 1 error 291 | ``` 292 | 293 | --- 294 | 295 | ### Too many elements in a sequence 296 | 297 | 298 | If you have extra elements, Expound will point out which elements should be removed 299 | 300 | 301 | #### Specs 302 | 303 | 304 | ```clojure 305 | (s/def :app/ingredient (s/cat :quantity clojure.core/number? :unit clojure.core/keyword?)) 306 | ``` 307 | 308 | #### Value 309 | 310 | 311 | ```clojure 312 | [100 :teaspoon :sugar] 313 | ``` 314 | 315 | #### `clojure.spec` message 316 | 317 | 318 | ``` 319 | (:sugar) - failed: Extra input in: [2] spec: :app/ingredient 320 | ``` 321 | 322 | #### Expound message 323 | 324 | 325 | ``` 326 | -- Syntax error ------------------- 327 | 328 | [... ... :sugar] 329 | ^^^^^^ 330 | 331 | has extra input 332 | 333 | ------------------------- 334 | Detected 1 error 335 | ``` 336 | -------------------------------------------------------------------------------- /doc/compatibility.md: -------------------------------------------------------------------------------- 1 | # Compatibility 2 | 3 | | | clj 1.9 + cljs 1.10.339 | clj 1.10 + cljs 1.10.439 | clj 1.10 + cljs 1.10.516 | clj 1.10 + cljs 1.10.520 4 | |-------------------------------------------|-------------------------|--------------------------|--------------------------|--------------------------| 5 | | `expound` | yes | yes | yes | yes | 6 | | `explain` | yes | yes | yes | yes | 7 | | `explain-results` | yes | yes | yes | yes | 8 | | expound errors for instrumented functions | yes | no | yes | yes | 9 | | expound errors for macros [^1] | yes | yes | yes | yes | 10 | 11 | ## Relevant JIRAs 12 | 13 | * 1.10.439 - https://dev.clojure.org/jira/browse/CLJS-2913 14 | * 1.10.516 - https://dev.clojure.org/jira/browse/CLJS-3050 15 | 16 | [^1]: Due to the way that macros are expanded in ClojureScript, you'll need to configure Expound in *Clojure*. This does not apply to self-hosted ClojureScript. 17 | 18 | (Note the `-e` arg below) 19 | 20 | `clj -Srepro -Sdeps '{:deps {expound/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` 21 | 22 | Now you will get Expound errors during macro-expansion: 23 | 24 | ``` 25 | ClojureScript 1.10.520 26 | cljs.user=> (require '[clojure.core.specs.alpha]) 27 | nil 28 | cljs.user=> (let [x]) 29 | Execution error - invalid arguments to cljs.analyzer/do-macroexpand-check at (analyzer.cljc:3772). 30 | -- Spec failed -------------------- 31 | 32 | ([x]) 33 | ^^^ 34 | 35 | should satisfy 36 | 37 | even-number-of-forms? 38 | 39 | -- Relevant specs ------- 40 | 41 | :cljs.core.specs.alpha/bindings: 42 | (clojure.spec.alpha/and 43 | clojure.core/vector? 44 | cljs.core.specs.alpha/even-number-of-forms? 45 | (clojure.spec.alpha/* :cljs.core.specs.alpha/binding)) 46 | 47 | ------------------------- 48 | Detected 1 error 49 | cljs.user=> 50 | ``` 51 | -------------------------------------------------------------------------------- /doc/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Socket REPL 4 | 5 | `clj -J-Dclojure.server.repl="{:port 5555 :accept clojure.core.server/repl}"` 6 | 7 | Then connect with `inf-clojure-connect` 8 | 9 | ## Clojurescript REPL (node) 10 | 11 | ``` 12 | lein with-profile +test-web,+cljs-repl repl 13 | ``` 14 | 15 | ``` 16 | M-x cider-connect 17 | (require 'cljs.repl.node) 18 | (cider.piggieback/cljs-repl (cljs.repl.node/repl-env)) 19 | ``` 20 | 21 | ## Running CLJS tests 22 | 23 | ### Karma 24 | 25 | `lein with-profile test-web cljsbuild auto test` 26 | `ls ./resources/public/test-web/test.js | entr -s 'sleep 1; bin/tests'` 27 | 28 | ## Running CLJ tests 29 | 30 | `lein with-profile +test-common test` 31 | 32 | or 33 | 34 | `lein with-profile +test-common test-refresh :changes-only` 35 | 36 | or (if you want to save a second or two) 37 | 38 | `clj -Atest:test-deps` 39 | 40 | or 41 | 42 | `bin/kaocha --watch --plugin profiling` 43 | 44 | ## Code coverage 45 | 46 | `bin/kaocha --plugin cloverage --cov-exclude-call expound.alpha/def` 47 | 48 | ## Updating packages 49 | 50 | You must have `lein-ancient` installed in your `~/.lein/profiles.clj` 51 | 52 | e.g. `lein ancient :all` (or in my setup, `lein with-profile +tools ancient :all` 53 | 54 | ## Readability and linting 55 | 56 | `./bin/inconsistent-aliases` shows namespace aliases that are different across the codebase. 57 | 58 | `./bin/lint` lints the code with `joker` 59 | 60 | `lein hiera` generates a graph of namespace dependencies 61 | 62 | ## Release 63 | 64 | 1. Update version in `project.clj` 65 | 1. Update `CHANGELOG.md` (including section for release and links at bottom) 66 | 1. Update version in `README.md` 67 | 1. Update version in `doc/compatibility.md` 68 | 1. Update version in `package.json` 69 | 1. `npm install` 70 | 1. Commit 71 | 1. `git tag -a v0.9.0 -m "version 0.9.0"` 72 | 1. `git push --tags` 73 | 74 | 75 | ### clojars 76 | 77 | Double check version is changed in `project.clj` and `lein deploy clojars` (use deploy token instead of password) 78 | 79 | ### NPM 80 | 81 | Double check version is changed in `package.json` and `npm publish --access=public` 82 | -------------------------------------------------------------------------------- /doc/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## How do I use Expound to print all spec errors? 4 | 5 | ### Using `set!` 6 | 7 | In most cases, you'll want to use `set!` to install the Expound printer: 8 | 9 | ```clojure 10 | (set! s/*explain-out* expound/printer) 11 | ``` 12 | 13 | ### Using `alter-var-root` 14 | 15 | If you using Expound within a REPL, `alter-var-root` will not install the Expound printer properly - you should use `set!` as described above. 16 | 17 | If you using the Expound printer in a non-REPL environment, `set!` will only change s/*explain-out* in the *current thread*. If your program spawns additional threads (e.g. a web server), you can set s/*explain-out* for all threads with: 18 | 19 | 20 | ```clojure 21 | (alter-var-root #'s/*explain-out* (constantly expound/printer)) 22 | ``` 23 | 24 | `set!` will also *not work* within an uberjar. Use `alter-var-root` instead. 25 | 26 | ## Why don't I see Expound errors for instrumentation or macroexpansion errors? 27 | 28 | As of `clojure.spec.alpha` 0.2.176 and ClojureScript 1.10.439 (which contains `cljs.spec.alpha`), spec no longer includes the spec error in the exception message. Instead, the error will now include more data about the spec error, but this isn't printed by default in older REPLs. 29 | 30 | Clojure 1.10.0 updates the REPL to print this new error data. As of this writing, the Clojurescript REPL is in the process of being updated. 31 | 32 | To summarize, spec errors will be printed at the REPL with these combinations: 33 | 34 | * `clojure.spec.alpha` 0.2.168 will work with Clojure 1.9.0 35 | * `clojure.spec.alpha` 0.2.176 will work with Clojure 1.10.0 36 | * Clojurescript 1.10.238 and 1.10.339 -------------------------------------------------------------------------------- /doc/prior_art.md: -------------------------------------------------------------------------------- 1 | # Prior Art 2 | 3 | Notes from reading other articles or blog posts about error messages. 4 | 5 | ## ["Finding and fixing errors" (Urn)](http://urn-lang.com/tutorial/03-finding-errors.html) 6 | 7 | Urn prints line numbers and underlines error. 8 | 9 | Explanantions are printed to the right of `^^^` 10 | 11 | ## [Urn](https://github.com/SquidDev/urn) 12 | 13 | Urn assertions show value of expressions below the expressions themselves 14 | 15 | ``` 16 | > (import test ()) 17 | out = nil 18 | > (affirm (eq? '("foo" "bar" "") 19 | . (string/split "foo-bar" "-"))) 20 | [ERROR] :1 (compile#111{split,temp}:46): Assertion failed 21 | (eq? (quote ("foo" "bar" "")) (string/split "foo-bar" "-")) 22 | | | 23 | | ("foo" "bar") 24 | ("foo" "bar" "") 25 | ``` 26 | 27 | Urn points of syntax errors (underlining good and bad stuff), e.g. 28 | 29 | ``` 30 | > (] 31 | [ERROR] Expected ')', got ']' 32 | => :[1:2 .. 1:2] ("]") 33 | 1 │ (] 34 | │ ^... block opened with '(' 35 | 1 │ (] 36 | │ ^ ']' used here 37 | > 38 | ``` 39 | 40 | ## ["Way, Way, Waaaay Nicer Error Messages!" (ReasonML)](https://reasonml.github.io/blog/2017/08/25/way-nicer-error-messages.html) 41 | 42 | “We’ve four a bug for you!” is friendly but not minimalist. I imagine it would quickly become noise. 43 | 44 | Line number and bad data colorized. 45 | 46 | Prints types and definitions “defined as” e.g. 47 | 48 | `jsPayload (defined as Js.t {* age : int, name : string})` 49 | 50 | Also use color to demarcate regions of errors by colorizing headings 51 | 52 | Tracks error messages in custom [repository](https://github.com/reasonml-community/error-message-improvement/issues). 53 | 54 | ## ["Shape of errors to come" (Rust)](https://blog.rust-lang.org/2016/08/10/Shape-of-errors-to-come.html) 55 | 56 | Rust errors have numbers for more explanation E0499. 57 | 58 | Explanation are inline. Combines source when lines for two different regions overlap. 59 | 60 | Displays other “points of interest” 61 | 62 | “Undefined or not in scope” - that's a good, concise error message. 63 | 64 | Rust explain (based on error number) is not generic, it uses your example in explanation! 65 | 66 | ## ["Measuring the Effectiveness of Error Messages Designed for Novice Programmers" (Racket)](http://cs.brown.edu/~sk/Publications/Papers/Published/mfk-measur-effect-error-msg-novice-sigcse/) 67 | 68 | DrRacket reports only one error at a time. 69 | 70 | In this experiment, they record edits to analyze how effective errors are. 71 | 72 | No analysis in this article of what factors caused bad error messages. 73 | 74 | ## ["Mind Your Language: On Novices' Interactions with Error Messages" (Racket)](http://cs.brown.edu/~sk/Publications/Papers/Published/mfk-mind-lang-novice-inter-error-msg/paper.pdf) 75 | 76 | > Yet, ask any experienced programmer about the quality of error messages in their programming environments, and you will often get an embarrassed laugh. 77 | 78 | Users sense that error must be in highlighted region (although this is not always true!) 79 | 80 | > We also noticed that students tended to look for a recommended course of action in the wording of the error message. 81 | 82 | > For instance, once the error message mentions a missing part, students felt prompted to provide the missing part, though this might not be the correct fix. 83 | 84 | > Error messages should not propose solutions. Even though some errors have likely fixes (missing close parentheses in particular places, for example), those fixes will not cover all cases 85 | 86 | > Error messages should not prompt students towards incorrect edits 87 | 88 | If function call and function don’t match, show both, because either could be source of problem. 89 | 90 | The terms "Expected" and "found" imply that function definition is correct, which may be misleading. 91 | 92 | Could beginner mode ask questions if solution is ambiguous? Or provide multiple solutions? 93 | 94 | > IDE developers should provide guides (not just documentation buried in some help menu) about the semantics of notations such as source highlighting. 95 | 96 | 97 | ## ["Error Message Conventions" (Racket)](https://docs.racket-lang.org/reference/exns.html) 98 | 99 | Racket’s error message convention is to produce error messages with the following shape: 100 | 101 | ``` 102 | ‹srcloc›: ‹name›: ‹message›; 103 | ‹continued-message› ... 104 | ‹field›: ‹detail› 105 | ... 106 | ``` 107 | 108 | ## ["Compilers as Assistants" (Elm)](http://elm-lang.org/blog/compilers-as-assistants) 109 | 110 | Elm can detect likely typos. 111 | 112 | Elm hides unrelated fields in data. 113 | 114 | Elm uses the term “mismatch”, or “does not match” 115 | 116 | Avoids cascading errors using [this approach](https://news.ycombinator.com/item?id=9808317) 117 | 118 | ## ["Compiler Errors for Humans" (Elm)](http://elm-lang.org/blog/compiler-errors-for-humans) 119 | 120 | "When we read code, color is a huge usability improvement” 121 | "When we read prose, layout has a major impact on our experience." 122 | -------------------------------------------------------------------------------- /doc/spec_problems.md: -------------------------------------------------------------------------------- 1 | # Spec problems 2 | 3 | Notes on the format of Spec's problems (as of alpha1). 4 | 5 | Each problem has several keys: 6 | 7 | * `:in` is a vector of "keys" to navigate to the bad value in the original data structure. This is roughly analogous to the arguments to `get-in`, but the "keys" don't exactly match what is expected for `get-in` (in particular, they can locate the key of a map data structure, which `get-in` can't do. `get-in` also doesn't work with lists). 8 | * `:via` is a vector of the specs names (keywords) followed to reach the spec that failed. This may be "missing" specs if they are not named. 9 | * `:path` is a vector of "keys" AND the alternate branch names followed when picking amongst named alternates 10 | * `:pred` is the symbol of the predicate that failed 11 | * `:val` is the bad value 12 | 13 | For example, given: 14 | 15 | ```clojure 16 | (s/def :example/id (s/or :num pos-int? 17 | :uuid uuid? )) 18 | 19 | (s/def :example/entity (s/keys :req-un [:example/id])) 20 | ``` 21 | 22 | When you call `(s/explain-data :example/entity {:id -1})`, you'll get two problems: 23 | 24 | ```clojure 25 | {:clojure.spec.alpha/spec :example/entity, 26 | :clojure.spec.alpha/value {:id -1} 27 | :clojure.spec.alpha/problems 28 | '( 29 | ;; problem 1 30 | {:path [:id :num], 31 | :pred clojure.core/pos-int?, 32 | :val -1, 33 | :via [:example/entity :example/id], 34 | :in [:id]} 35 | ;; problem 2 36 | {:path [:id :uuid], 37 | :pred clojure.core/uuid?, 38 | :val -1, 39 | :via [:example/entity :example/id], 40 | :in [:id]}), 41 | } 42 | ``` 43 | 44 | doc/spec_problems.md -------------------------------------------------------------------------------- /expected-jar-contents.txt: -------------------------------------------------------------------------------- 1 | META-INF/MANIFEST.MF 2 | META-INF/maven/expound/expound/pom.xml 3 | META-INF/leiningen/expound/expound/project.clj 4 | META-INF/leiningen/expound/expound/README.md 5 | META-INF/leiningen/expound/expound/LICENSE 6 | META-INF/ 7 | META-INF/maven/ 8 | META-INF/maven/expound/ 9 | META-INF/maven/expound/expound/ 10 | META-INF/maven/expound/expound/pom.properties 11 | expound/ 12 | expound/util.cljc 13 | expound/paths.cljc 14 | expound/alpha.cljc 15 | expound/problems.cljc 16 | expound/printer.cljc 17 | expound/specs.cljc 18 | expound/ansi.cljc 19 | -------------------------------------------------------------------------------- /figwheel-main.edn: -------------------------------------------------------------------------------- 1 | {:auto-testing true 2 | :watch-dirs ["src" "test"]} 3 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | // same as :output-dir in cljsbuild/builds/test 3 | var output_dir = 'resources/public/test-web/out' 4 | // same as :output-to in cljsbuild/builds/test 5 | var output_to = 'resources/public/test-web/test.js' 6 | config.set({ 7 | frameworks: ['cljs-test'], 8 | 9 | files: [ 10 | output_dir + '/goog/base.js', 11 | output_dir + '/cljs_deps.js', 12 | output_to, 13 | {pattern: output_dir + '/*.js', included: false}, 14 | {pattern: output_dir + '/**/*.js', included: false} 15 | ], 16 | 17 | client: {args: ['expound.test_runner.run_all'], 18 | captureConsole: true}, 19 | 20 | // singleRun set to false does not work! 21 | singleRun: true, 22 | browsers: ['ChromeHeadless'], 23 | autoWatch: false, 24 | logLevel: config.LOG_INFO, 25 | concurrency: 1, 26 | // Workarounds for timeout 27 | browserNoActivityTimeout: 20000, 28 | transports: ['polling'], 29 | browserDisconnectTolerance: 5 30 | }) 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bbrinck/expound", 3 | "version": "0.9.0", 4 | "description": "Human-optimized error messages for clojure.spec", 5 | "author": { 6 | "name": "Ben Brinckerhoff", 7 | "email": "ben@bbrinck.com", 8 | "url": "http://bbrinck.com" 9 | }, 10 | "homepage": "https://github.com/bhb/expound", 11 | "license": "EPL-1.0", 12 | "directories": { 13 | "lib": "src" 14 | }, 15 | "files": [ 16 | "src/*" 17 | ], 18 | "keywords": [ 19 | "clojurescript", 20 | "clojure", 21 | "cljs", 22 | "cljc", 23 | "self-host" 24 | ], 25 | "devDependencies": { 26 | "karma": "^6.3.4", 27 | "karma-chrome-launcher": "^2.2.0", 28 | "karma-cljs-test": "^0.1.0", 29 | "lodash": "^4.17.11", 30 | "set-value": "4.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject expound "0.9.0" 2 | :description "Human-optimized error messages for clojure.spec" 3 | :url "https://github.com/bhb/expound" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :scm {:name "git" :url "https://github.com/bhb/expound"} 7 | :dependencies [[org.clojure/clojure "1.10.3" :scope "provided"] 8 | [org.clojure/clojurescript "1.11.4" :scope "provided"] 9 | [org.clojure/spec.alpha "0.3.218" :scope "provided"]] 10 | :deploy-repositories [["releases" :clojars]] 11 | :jar-exclusions [#"^public/.*"] 12 | :plugins [ 13 | [lein-cljfmt "0.8.0"] 14 | [lein-cljsbuild "1.1.8" :exclusions [[org.clojure/clojure]]] 15 | [lein-hiera "1.1.0"] 16 | ] 17 | :cljsbuild {:builds 18 | [{:id "test" 19 | :source-paths ["src" "test"] 20 | :compiler {;; If you change output-to or output-dir, 21 | ;; you must update karma.conf.js to match 22 | :asset-path "test-web/out" 23 | :output-to "resources/public/test-web/test.js" 24 | :output-dir "resources/public/test-web/out" 25 | :main "expound.test-runner" 26 | :optimizations :none 27 | :verbose true 28 | :compiler-stats true}}]} 29 | :profiles {:dev {:dependencies [ 30 | [binaryage/devtools "1.0.4"] 31 | [cider/piggieback "0.5.3"] 32 | [orchestra "2021.01.01-1"] 33 | [org.clojure/core.specs.alpha "0.2.62"] 34 | [vvvvalvalval/scope-capture "0.3.2"] 35 | [org.clojure/test.check "1.1.1"] 36 | [metosin/spec-tools "0.10.5"] 37 | [ring/ring-core "1.9.4"] 38 | [ring/ring-spec "0.0.4"] ; to test specs 39 | [cider/cider-nrepl "0.27.4"] 40 | ] 41 | :injections [(require 'sc.api)] 42 | :plugins [ 43 | [io.aviso/pretty "1.1.1"] 44 | ;; I am NOT adding cider-nrepl here because 45 | ;; using it as a plugin seems to add it as a top-level dependency 46 | ;; when I build the package! 47 | ] 48 | ;; need to add dev source path here to get user.clj loaded 49 | :source-paths ["src" "dev"] 50 | :repl-options {:nrepl-middleware [cider.piggieback/wrap-cljs-repl 51 | ;; Manually add all middlewear 52 | ;; https://docs.cider.mx/cider-nrepl/usage.html#_via_leiningen 53 | cider.nrepl/wrap-apropos 54 | cider.nrepl/wrap-classpath 55 | cider.nrepl/wrap-clojuredocs 56 | cider.nrepl/wrap-complete 57 | cider.nrepl/wrap-debug 58 | cider.nrepl/wrap-format 59 | cider.nrepl/wrap-info 60 | cider.nrepl/wrap-inspect 61 | cider.nrepl/wrap-macroexpand 62 | cider.nrepl/wrap-ns 63 | cider.nrepl/wrap-spec 64 | cider.nrepl/wrap-profile 65 | cider.nrepl/wrap-refresh 66 | cider.nrepl/wrap-resource 67 | cider.nrepl/wrap-stacktrace 68 | cider.nrepl/wrap-test 69 | cider.nrepl/wrap-trace 70 | cider.nrepl/wrap-out 71 | cider.nrepl/wrap-undef 72 | cider.nrepl/wrap-version 73 | cider.nrepl/wrap-xref 74 | ]} 75 | ;; need to add the compliled assets to the :clean-targets 76 | :clean-targets ^{:protect false} ["resources/public/test-web" 77 | :target-path]} 78 | :check {:global-vars {*unchecked-math* :warn-on-boxed 79 | *warn-on-reflection* true}} 80 | :kaocha [:test-common 81 | {:dependencies [[lambdaisland/kaocha "1.60.977"] 82 | [lambdaisland/kaocha-cloverage "1.0-45"]]}] 83 | :test-common {:dependencies [[org.clojure/test.check "1.1.1"] 84 | [orchestra "2021.01.01-1"] 85 | [pjstadig/humane-test-output "0.11.0"] 86 | [com.gfredericks/test.chuck "0.2.13"] 87 | [io.aviso/pretty "1.1.1"] 88 | [org.clojure/core.specs.alpha "0.2.62"] 89 | [com.stuartsierra/dependency "1.0.0"] 90 | [ring/ring-core "1.9.4"] 91 | [ring/ring-spec "0.0.4"] ; to test specs 92 | [metosin/spec-tools "0.10.5"] 93 | [com.bhauman/spell-spec "0.1.2"]] 94 | :middleware [io.aviso.lein-pretty/inject] 95 | :injections [(require 'pjstadig.humane-test-output) 96 | (pjstadig.humane-test-output/activate!)] 97 | } 98 | :test-web [:test-common 99 | {:source-paths ["test"] 100 | :dependencies [[karma-reporter "3.1.0"]]}] 101 | :cljs-repl {:dependencies [[cider/piggieback "0.5.3"]]} 102 | :clj-1.9.0 {:dependencies [[org.clojure/clojure "1.9.0" :upgrade false] 103 | [metosin/spec-tools "0.7.1" :upgrade false]]} 104 | :clj-1.10.0 {:dependencies [[org.clojure/clojure "1.10.0" :upgrade false] 105 | [metosin/spec-tools "0.7.1" :upgrade false]]} 106 | :cljs-1.10.238 {:dependencies [[org.clojure/clojurescript "1.10.238" :upgrade false]]} 107 | :cljs-1.10.339 {:dependencies [[org.clojure/clojurescript "1.10.339" :upgrade false]]} 108 | :cljs-1.10.439 {:dependencies [[org.clojure/clojurescript "1.10.439" :upgrade false]]} 109 | :cljs-1.10.597 {:dependencies [[org.clojure/clojurescript "1.10.597" :upgrade false]]} 110 | :spec-0.2.168 {:dependencies [[org.clojure/spec.alpha "0.2.168" :upgrade false]]} 111 | :spec-0.2.176 {:dependencies [[org.clojure/spec.alpha "0.2.176" :upgrade false]]} 112 | :orch-2019.02.06-1 {:dependencies [[orchestra "2019.02.06-1" :upgrade false]]} 113 | :orch-2020.07.12-1 {:dependencies [[orchestra "2020.07.12-1" :upgrade false]]} 114 | } 115 | :aliases {"kaocha" ["with-profile" "+kaocha" "run" "-m" "kaocha.runner"] 116 | "run-tests-once" ["with-profile" "test-web" "cljsbuild" "once" "test"] 117 | "run-tests-auto" ["do" 118 | ["with-profile" "test-web" "cljsbuild" "once" "test"] 119 | ["with-profile" "test-web" "cljsbuild" "auto" "test"]]} 120 | :test-refresh {:refresh-dirs ["src" "test"] 121 | :watch-dirs ["src" "test"]}) 122 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

REPL should be connected...

7 | 8 | 9 | -------------------------------------------------------------------------------- /src/expound/ansi.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc expound.ansi 2 | (:require [clojure.string :as string])) 3 | 4 | ;; Copied from strictly-specking, since I see no reason 5 | ;; to deviate from the colors displayed in figwheel 6 | ;; https://github.com/bhauman/strictly-specking/blob/f102c9bd604f0c238a738ac9e2b1f6968fdfd2d8/src/strictly_specking/ansi_util.clj 7 | 8 | (def sgr-code 9 | "Map of symbols to numeric SGR (select graphic rendition) codes." 10 | {:none 0 11 | :bold 1 12 | :underline 3 13 | :blink 5 14 | :reverse 7 15 | :hidden 8 16 | :strike 9 17 | :black 30 18 | :red 31 19 | :green 32 20 | :yellow 33 21 | :blue 34 22 | :magenta 35 23 | :cyan 36 24 | :white 37 25 | :fg-256 38 26 | :fg-reset 39 27 | :bg-black 40 28 | :bg-red 41 29 | :bg-green 42 30 | :bg-yellow 43 31 | :bg-blue 44 32 | :bg-magenta 45 33 | :bg-cyan 46 34 | :bg-white 47 35 | :bg-256 48 36 | :bg-reset 49}) 37 | 38 | (def ^:dynamic *enable-color* false) 39 | 40 | (defn esc 41 | "Returns an ANSI escope string which will apply the given collection of SGR 42 | codes." 43 | [codes] 44 | (let [codes (map sgr-code codes codes) 45 | codes (string/join \; codes)] 46 | (str \u001b \[ codes \m))) 47 | 48 | (defn escape 49 | "Returns an ANSI escope string which will enact the given SGR codes." 50 | [& codes] 51 | (esc codes)) 52 | 53 | (defn sgr 54 | "Wraps the given string with SGR escapes to apply the given codes, then reset 55 | the graphics." 56 | [string & codes] 57 | (str (esc codes) string (escape :none))) 58 | 59 | (def ^:dynamic *print-styles* 60 | {:highlight [:bold] 61 | :good [:green] 62 | :good-pred [:green] 63 | :good-key [:green] 64 | :bad [:red] 65 | :bad-value [:red] 66 | :error-key [:red] 67 | :focus-key [:bold] 68 | :correct-key [:green] 69 | :header [:cyan] 70 | :footer [:cyan] 71 | :warning-key [:bold] 72 | :focus-path [:magenta] 73 | :message [:magenta] 74 | :pointer [:magenta] 75 | :none [:none]}) 76 | 77 | (defn resolve-styles [styles] 78 | (if-let [res (not-empty 79 | (mapcat #(or 80 | (when-let [res (*print-styles* %)] 81 | res) 82 | [%]) 83 | styles))] 84 | res 85 | ;; fall back to bright 86 | [:bold])) 87 | 88 | (defn color [s & styles] 89 | (if *enable-color* 90 | (apply sgr s (resolve-styles styles)) 91 | s)) 92 | -------------------------------------------------------------------------------- /src/expound/paths.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc expound.paths 2 | (:require [clojure.spec.alpha :as s] 3 | [expound.util :as util])) 4 | 5 | ;;;;;; specs ;;;;;; 6 | 7 | (s/def :expound/path (s/nilable sequential?)) 8 | 9 | ;;;;;; types ;;;;;; 10 | 11 | (defrecord KeyPathSegment [key]) 12 | 13 | (defrecord KeyValuePathSegment [idx]) 14 | 15 | ;;;;;;;;;;;;;;;;;;; 16 | 17 | (defn kps? [x] 18 | (instance? KeyPathSegment x)) 19 | 20 | (defn kvps? [x] 21 | (instance? KeyValuePathSegment x)) 22 | 23 | (declare in-with-kps*) 24 | 25 | (defn fn-equal [x y] 26 | (and (fn? x) 27 | (fn? y) 28 | (= (pr-str x) 29 | (pr-str y)))) 30 | 31 | (defn both-nan? [x y] 32 | (and (util/nan? x) 33 | (util/nan? y))) 34 | 35 | (defn equalish? [x y] 36 | (or 37 | (= x y) 38 | (fn-equal x y) 39 | (both-nan? x y))) 40 | 41 | (defn in-with-kps-maps-as-seqs [form val in in'] 42 | (let [[k & rst] in 43 | [idx & rst2] rst] 44 | (cond 45 | (= ::not-found form) 46 | ::not-found 47 | 48 | (and (empty? in) 49 | (equalish? form val)) 50 | in' 51 | 52 | ;; detect a `:in` path that points to a key/value pair in a coll-of spec 53 | (and (map? form) 54 | (nat-int? k) 55 | (< (long k) 56 | (count (seq form)))) 57 | (in-with-kps* (nth (seq form) k) val rst (conj in' (->KeyValuePathSegment k))) 58 | 59 | (and (map? form) 60 | (nat-int? k) 61 | (int? idx) 62 | (< (long k) 63 | (count (seq form))) 64 | (< (long idx) 65 | (count (nth (seq form) k)))) 66 | (in-with-kps* (nth (nth (seq form) k) idx) val rst2 (conj in' (->KeyValuePathSegment k) idx)) 67 | 68 | :else 69 | ::not-found))) 70 | 71 | (defn in-with-kps-fuzzy-match-for-regex-failures [form val in in'] 72 | (if (= form ::not-found) 73 | form 74 | (let [[k & rst] in] 75 | (cond 76 | ;; not enough input 77 | (and (empty? in) 78 | (seqable? form) 79 | (= val '())) 80 | in' 81 | 82 | ;; too much input 83 | (and (empty? in) 84 | (and (seq? val) 85 | (= form 86 | (first val)))) 87 | in' 88 | 89 | (and (nat-int? k) (seqable? form)) 90 | (in-with-kps* (nth (seq form) k ::not-found) val rst (conj in' k)) 91 | 92 | :else 93 | ::not-found)))) 94 | 95 | (defn in-with-kps-ints-are-keys [form val in in'] 96 | (if (= form ::not-found) 97 | form 98 | (let [[k & rst] in] 99 | (cond 100 | (and (empty? in) 101 | (equalish? form val)) 102 | in' 103 | 104 | (associative? form) 105 | (in-with-kps* (get form k ::not-found) val rst (conj in' k)) 106 | 107 | (and (int? k) (seqable? form)) 108 | (in-with-kps* (nth (seq form) k ::not-found) val rst (conj in' k)) 109 | 110 | :else 111 | ::not-found)))) 112 | 113 | (defn in-with-kps-ints-are-key-value-indicators [form val in in'] 114 | (if (= form ::not-found) 115 | form 116 | (let [[k & rst] in 117 | [idx & rst2] rst] 118 | (cond 119 | (and (empty? in) 120 | (equalish? form val)) 121 | in' 122 | 123 | ;; detect a `:in` path that points at a key in a map-of spec 124 | (and (map? form) 125 | (= 0 idx)) 126 | (in-with-kps* k val rst2 (conj in' (->KeyPathSegment k))) 127 | 128 | ;; detect a `:in` path that points at a value in a map-of spec 129 | (and (map? form) 130 | (= 1 idx)) 131 | (in-with-kps* (get form k ::not-found) val rst2 (conj in' k)) 132 | 133 | :else 134 | ::not-found)))) 135 | 136 | (defn in-with-kps* [form val in in'] 137 | (if (fn? form) 138 | in' 139 | (let [br1 (in-with-kps-ints-are-key-value-indicators form val in in')] 140 | (if (not= ::not-found br1) 141 | br1 142 | (let [br2 (in-with-kps-maps-as-seqs form val in in')] 143 | (if (not= ::not-found br2) 144 | br2 145 | (let [br3 (in-with-kps-ints-are-keys form val in in')] 146 | (if (not= ::not-found br3) 147 | br3 148 | (let [br4 (in-with-kps-fuzzy-match-for-regex-failures form val in in')] 149 | (if (not= ::not-found br4) 150 | br4 151 | ::not-found)))))))))) 152 | 153 | (defn paths-to-value [form val path paths] 154 | (cond 155 | (= form val) 156 | (conj paths path) 157 | 158 | (or (sequential? form) 159 | (set? form)) 160 | (reduce 161 | (fn [ps [x i]] 162 | (paths-to-value x val (conj path i) ps)) 163 | paths 164 | (map vector form (range))) 165 | 166 | (map? form) (reduce 167 | (fn [ps [k v]] 168 | (->> ps 169 | (paths-to-value k val (conj path (->KeyPathSegment k))) 170 | (paths-to-value v val (conj path k)))) 171 | paths 172 | form) 173 | 174 | :else paths)) 175 | 176 | (defn in-with-kps [form val in in'] 177 | (let [res (in-with-kps* form val in in')] 178 | (if (= ::not-found res) 179 | nil 180 | res))) 181 | 182 | (declare compare-paths) 183 | 184 | (defn compare-path-segment [x y] 185 | (cond 186 | (and (int? x) (kvps? y)) 187 | (compare x (:idx y)) 188 | 189 | (and (kvps? x) (int? y)) 190 | (compare (:idx x) y) 191 | 192 | (and (kps? x) (not (kps? y))) 193 | -1 194 | 195 | (and (not (kps? x)) (kps? y)) 196 | 1 197 | 198 | (and (vector? x) (vector? y)) 199 | (compare-paths x y) 200 | 201 | :else 202 | (compare x y))) 203 | 204 | (defn compare-paths [path1 path2] 205 | (->> (map compare-path-segment path1 path2) 206 | (remove #{0}) 207 | first)) 208 | 209 | (defn value-in 210 | "Similar to get-in, but works with paths that reference map keys" 211 | [form in] 212 | (if (nil? in) 213 | form 214 | (let [[k & rst] in] 215 | (cond 216 | (empty? in) 217 | form 218 | 219 | (and (map? form) (kps? k)) 220 | (recur (:key k) rst) 221 | 222 | (and (map? form) (kvps? k)) 223 | (recur (nth (seq form) (:idx k)) rst) 224 | 225 | (associative? form) 226 | (recur (get form k) rst) 227 | 228 | (and (int? k) 229 | (seqable? form)) 230 | (recur (nth (seq form) k) rst) 231 | 232 | :else 233 | (throw (ex-info "No value found" 234 | {:form form 235 | :in in})))))) 236 | -------------------------------------------------------------------------------- /src/expound/printer.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc expound.printer 2 | (:require [clojure.string :as string] 3 | [clojure.spec.alpha :as s] 4 | [clojure.pprint :as pprint] 5 | [clojure.set :as set] 6 | [expound.util :as util] 7 | [expound.ansi :as ansi] 8 | [expound.paths :as paths] 9 | [clojure.walk :as walk] 10 | #?(:cljs [goog.string.format]) ; https://github.com/bhb/expound/issues/183 11 | #?(:cljs [goog.string]) ; https://github.com/bhb/expound/issues/183 12 | #?(:clj [clojure.main :as main])) 13 | (:refer-clojure :exclude [format])) 14 | 15 | (def indent-level 2) 16 | (def anon-fn-str "") 17 | 18 | (s/def :expound.spec/spec-conjunction 19 | (s/cat 20 | :op #{'or 'and} 21 | :specs (s/+ :expound.spec/kw-or-conjunction))) 22 | (s/def :expound.spec/kw-or-conjunction 23 | (s/or 24 | :kw qualified-keyword? 25 | :conj :expound.spec/spec-conjunction)) 26 | (s/def :expound.spec/keys-spec 27 | (s/cat :keys #{'clojure.spec.alpha/keys 28 | 'cljs.spec.alpha/keys} 29 | :clauses (s/* 30 | (s/cat :qualifier #{:req-un :req :opt-un :opt} 31 | :specs (s/coll-of :expound.spec/kw-or-conjunction))))) 32 | (s/def :expound.spec/contains-key-pred (s/or 33 | :simple (s/cat 34 | :contains #{`contains? 'contains?} 35 | :arg #{'%} 36 | :kw keyword?) 37 | :compound (s/cat 38 | :op #{`or `and} 39 | :clauses (s/+ :expound.spec/contains-key-pred)))) 40 | (declare format) 41 | 42 | (defn ^:private str-width [lines] 43 | (apply max (map count lines))) 44 | 45 | (defn ^:private max-column-width [rows i] 46 | (apply max 0 (map #(str-width (string/split-lines (str (nth % i)))) rows))) 47 | 48 | (defn ^:private max-row-height [row] 49 | (apply max 0 50 | (map #(count (string/split-lines (str %))) row))) 51 | 52 | (defn ^:private indented-multirows [column-widths multi-rows] 53 | (->> multi-rows 54 | (map 55 | (fn [multi-row] 56 | (map 57 | (fn [row] 58 | (map-indexed 59 | (fn [i v] 60 | (format (str "%-" (nth column-widths i) "s") v)) 61 | row)) 62 | multi-row))))) 63 | 64 | (defn ^:private formatted-row [row edge spacer middle] 65 | (str edge spacer 66 | (string/join (str spacer middle spacer) row) 67 | spacer edge)) 68 | 69 | (defn ^:private table [multirows] 70 | (let [header (first (first multirows)) 71 | columns-dividers (map #(apply str (repeat (count (str %)) "-")) header) 72 | header-columns-dividers (map #(apply str (repeat (count (str %)) "=")) header) 73 | header-divider (formatted-row header-columns-dividers "|" "=" "+") 74 | row-divider (formatted-row columns-dividers "|" "-" "+") 75 | formatted-multirows (->> multirows 76 | (map 77 | (fn [multirow] 78 | (map (fn [row] (formatted-row row "|" " " "|")) multirow))))] 79 | 80 | (->> 81 | (concat [[header-divider]] (repeat [row-divider])) 82 | (mapcat vector formatted-multirows) 83 | (butlast) ;; remove the trailing row-divider 84 | (mapcat seq)))) 85 | 86 | (defn ^:private multirow [row-height row] 87 | (let [split-row-contents (mapv (fn [v] (string/split-lines (str v))) row)] 88 | (for [row-idx (range row-height)] 89 | (for [col-idx (range (count row))] 90 | (get-in split-row-contents [col-idx row-idx] ""))))) 91 | 92 | (defn ^:private multirows [row-heights rows] 93 | (map-indexed (fn [idx row] (multirow (get row-heights idx) row)) rows)) 94 | 95 | (defn ^:private formatted-multirows [column-keys map-rows] 96 | (when-not (empty? map-rows) 97 | (let [rows (into [column-keys] (map #(map % column-keys) map-rows)) 98 | row-heights (mapv max-row-height rows) 99 | column-widths (map-indexed 100 | (fn [i _] (max-column-width rows i)) 101 | (first rows))] 102 | 103 | (->> 104 | rows 105 | (multirows row-heights) 106 | (indented-multirows column-widths))))) 107 | 108 | (defn table-str [column-keys map-rows] 109 | (str 110 | "\n" 111 | (apply str 112 | (map 113 | (fn [line] (str line "\n")) 114 | (table (formatted-multirows column-keys map-rows)))))) 115 | 116 | (s/fdef print-table 117 | :args (s/cat 118 | :columns (s/? (s/coll-of any?)) 119 | :map-rows (s/coll-of map?))) 120 | (defn print-table 121 | ([map-rows] 122 | (print-table (keys (first map-rows)) map-rows)) 123 | ([column-keys map-rows] 124 | (table-str column-keys map-rows))) 125 | 126 | ;;;; private 127 | 128 | (defn keywords [form] 129 | (->> form 130 | (tree-seq coll? seq) 131 | (filter keyword?))) 132 | 133 | (defn singleton? [xs] 134 | (= 1 (count xs))) 135 | 136 | (defn specs-from-form [via] 137 | (let [form (some-> via last s/form) 138 | keys-specs (->> (tree-seq coll? seq form) 139 | (filter #(s/valid? :expound.spec/keys-spec %)))] 140 | (if (empty? keys-specs) 141 | #{} 142 | (->> keys-specs 143 | (map #(s/conform :expound.spec/keys-spec %)) 144 | (mapcat :clauses) 145 | (mapcat :specs) 146 | (tree-seq coll? seq) 147 | (filter 148 | (fn [x] 149 | (and (vector? x) (= :kw (first x))))) 150 | (map second) 151 | set)))) 152 | 153 | (defn key->spec [keys problems] 154 | (doseq [p problems] 155 | (assert (some? (:expound/via p)) util/assert-message)) 156 | (let [vias (map :expound/via problems) 157 | specs (if (every? qualified-keyword? keys) 158 | keys 159 | (if-let [specs (apply set/union (map specs-from-form vias))] 160 | specs 161 | keys))] 162 | (reduce 163 | (fn [m k] 164 | (assoc m 165 | k 166 | (if (qualified-keyword? k) 167 | k 168 | (or (->> specs 169 | (filter #(= (name k) (name %))) 170 | first) 171 | "")))) 172 | {} 173 | keys))) 174 | 175 | (defn summarize-key-clause [[branch match]] 176 | (case branch 177 | :simple 178 | (:kw match) 179 | 180 | :compound 181 | (apply list 182 | (symbol (name (:op match))) 183 | (map summarize-key-clause (:clauses match))))) 184 | 185 | (defn missing-key [form] 186 | (let [[branch match] (s/conform :expound.spec/contains-key-pred (nth form 2))] 187 | (case branch 188 | :simple 189 | (:kw match) 190 | 191 | :compound 192 | (summarize-key-clause [branch match])))) 193 | 194 | ;;;; public 195 | 196 | (defn elide-core-ns [s] 197 | #?(:cljs (-> s 198 | (string/replace "cljs.core/" "") 199 | (string/replace "cljs/core/" "")) 200 | :clj (string/replace s "clojure.core/" ""))) 201 | 202 | (defn elide-spec-ns [s] 203 | #?(:cljs (-> s 204 | (string/replace "cljs.spec.alpha/" "") 205 | (string/replace "cljs/spec/alpha" "")) 206 | :clj (string/replace s "clojure.spec.alpha/" ""))) 207 | 208 | (defn pprint-fn [f] 209 | (-> #?(:clj 210 | (let [[_ ns-n f-n] (re-matches #"(.*)\$(.*?)(__[0-9]+)?" (str f))] 211 | (if (re-matches #"^fn__\d+\@.*$" f-n) 212 | anon-fn-str 213 | (str 214 | (main/demunge ns-n) "/" 215 | (main/demunge f-n)))) 216 | :cljs 217 | (let [fn-parts (string/split (second (re-find 218 | #"object\[([^\( \]]+).*(\n|\])?" 219 | (pr-str f))) 220 | #"\$") 221 | ns-n (string/join "." (butlast fn-parts)) 222 | fn-n (last fn-parts)] 223 | (if (empty? ns-n) 224 | anon-fn-str 225 | (str 226 | (demunge ns-n) "/" 227 | (demunge fn-n))))) 228 | (elide-core-ns) 229 | (string/replace #"--\d+" "") 230 | (string/replace #"@[a-zA-Z0-9]+" ""))) 231 | 232 | #?(:cljs 233 | (defn format [fmt & args] 234 | (apply goog.string/format fmt args)) 235 | :clj (def format clojure.core/format)) 236 | 237 | (s/fdef pprint-str 238 | :args (s/cat :x any?) 239 | :ret string?) 240 | (defn pprint-str 241 | "Returns the pretty-printed string" 242 | [x] 243 | (if (fn? x) 244 | (pprint-fn x) 245 | (pprint/write x :stream nil))) 246 | 247 | (defn expand-spec [spec] 248 | (let [expanded-spec (if (s/get-spec spec) 249 | (s/form spec) 250 | spec)] 251 | (if (string? expanded-spec) 252 | expanded-spec 253 | (pprint-str expanded-spec)))) 254 | 255 | (defn simple-spec-or-name [spec-name] 256 | (let [expanded (expand-spec spec-name) 257 | spec-str (elide-spec-ns (elide-core-ns 258 | (if (nil? expanded) 259 | "nil" 260 | expanded)))] 261 | 262 | spec-str)) 263 | 264 | (defn print-spec-keys* [problems] 265 | (let [keys (keywords (map #(missing-key (:pred %)) problems))] 266 | (if (and (empty? (:expound/via (first problems))) 267 | (some simple-keyword? keys)) 268 | ;; The containing spec is not present in the problems 269 | ;; and at least one key is not namespaced, so we can't figure out 270 | ;; the spec they intended. 271 | nil 272 | 273 | (->> (key->spec keys problems) 274 | (map (fn [[k v]] {"key" k "spec" (simple-spec-or-name v)})) 275 | (sort-by #(get % "key")))))) 276 | 277 | (defn print-spec-keys [problems] 278 | (->> 279 | (print-spec-keys* problems) 280 | (print-table ["key" "spec"]) 281 | string/trim)) 282 | 283 | (defn print-missing-keys [problems] 284 | (let [keys-clauses (distinct (map (comp missing-key :pred) problems))] 285 | (if (every? keyword? keys-clauses) 286 | (string/join ", " (map #(ansi/color % :correct-key) (sort keys-clauses))) 287 | (str "\n\n" 288 | (ansi/color (pprint-str 289 | (if (singleton? keys-clauses) 290 | (first keys-clauses) 291 | (apply list 292 | 'and 293 | keys-clauses))) :correct-key))))) 294 | 295 | (s/fdef no-trailing-whitespace 296 | :args (s/cat :s string?) 297 | :ret string?) 298 | (defn no-trailing-whitespace 299 | "Given an potentially multi-line string, returns that string with all 300 | trailing whitespace removed." 301 | [s] 302 | (let [s' (->> s 303 | string/split-lines 304 | (map string/trimr) 305 | (string/join "\n"))] 306 | (if (= \newline (last s)) 307 | (str s' "\n") 308 | s'))) 309 | 310 | (s/fdef indent 311 | :args (s/cat 312 | :first-line-indent-level (s/? nat-int?) 313 | :indent-level (s/? nat-int?) 314 | :s string?) 315 | :ret string?) 316 | (defn indent 317 | "Given an potentially multi-line string, returns that string indented by 318 | 'indent-level' spaces. Optionally, can indent first line and other lines 319 | different amounts." 320 | ([s] 321 | (indent indent-level s)) 322 | ([indent-level s] 323 | (indent indent-level indent-level s)) 324 | ([first-line-indent rest-lines-indent s] 325 | (let [[line & lines] (string/split-lines (str s))] 326 | (->> lines 327 | (map #(str (apply str (repeat rest-lines-indent " ")) %)) 328 | (into [(str (apply str (repeat first-line-indent " ")) line)]) 329 | (string/join "\n"))))) 330 | 331 | (defn escape-replacement [#?(:clj pattern :cljs _pattern) s] 332 | #?(:clj (if (string? pattern) 333 | s 334 | (string/re-quote-replacement s)) 335 | :cljs (string/replace s #"\$" "$$$$"))) 336 | 337 | (defn blank-form [form] 338 | (cond 339 | (map? form) 340 | (zipmap (keys form) (repeat :expound.problems/irrelevant)) 341 | 342 | (vector? form) 343 | (vec (repeat (count form) :expound.problems/irrelevant)) 344 | 345 | (set? form) 346 | form 347 | 348 | (or (list? form) 349 | (seq? form)) 350 | (apply list (repeat (count form) :expound.problems/irrelevant)) 351 | 352 | :else 353 | :expound.problems/irrelevant)) 354 | 355 | (s/fdef summary-form 356 | :args (s/cat :show-valid-values? boolean? 357 | :form any? 358 | :highlighted-path :expound/path)) 359 | (defn summary-form [show-valid-values? form in] 360 | (let [[k & rst] in 361 | rst (or rst []) 362 | displayed-form (if show-valid-values? form (blank-form form))] 363 | (cond 364 | (empty? in) 365 | :expound.problems/relevant 366 | 367 | (and (map? form) (paths/kps? k)) 368 | (-> displayed-form 369 | (dissoc (:key k)) 370 | (assoc (summary-form show-valid-values? (:key k) rst) 371 | :expound.problems/irrelevant)) 372 | 373 | (and (map? form) (paths/kvps? k)) 374 | (recur show-valid-values? (nth (seq form) (:idx k)) rst) 375 | 376 | (associative? form) 377 | (assoc displayed-form 378 | k 379 | (summary-form show-valid-values? (get form k) rst)) 380 | 381 | (and (int? k) (seq? form)) 382 | (apply list (-> displayed-form 383 | vec 384 | (assoc k (summary-form show-valid-values? (nth form k) rst)))) 385 | 386 | (and (int? k) (set? form)) 387 | (into #{} (-> displayed-form 388 | vec 389 | (assoc k (summary-form show-valid-values? (nth (seq form) k) rst)))) 390 | 391 | (and (int? k) (list? form)) 392 | (into '() (-> displayed-form 393 | vec 394 | (assoc k (summary-form show-valid-values? (nth (seq form) k) rst)))) 395 | 396 | (and (int? k) (string? form)) 397 | (string/join (assoc (vec form) k :expound.problems/relevant)) 398 | 399 | :else 400 | (throw (ex-info "Cannot find path segment in form. This can be caused by using conformers to transform values, which is not supported in Expound" 401 | {:form form 402 | :in in}))))) 403 | 404 | ;; FIXME - this function is not intuitive. 405 | (defn highlight-line 406 | [prefix replacement] 407 | (let [max-width (apply max (map #(count (str %)) (string/split-lines replacement)))] 408 | (indent (count (str prefix)) 409 | (apply str (repeat max-width "^"))))) 410 | 411 | (defn highlighted-value 412 | "Given a problem, returns a pretty printed 413 | string that highlights the problem value" 414 | [opts problem] 415 | (let [{:keys [:expound/form :expound/in]} problem 416 | {:keys [show-valid-values?] :or {show-valid-values? false}} opts 417 | printed-val (pprint-str (paths/value-in form in)) 418 | relevant (str "(" :expound.problems/relevant "|(" :expound.problems/kv-relevant "\\s+" :expound.problems/kv-relevant "))") 419 | regex (re-pattern (str "(.*)" relevant ".*")) 420 | s (binding [*print-namespace-maps* false] 421 | (if (:show-valid-values? opts) 422 | (pprint-str (summary-form show-valid-values? form in)) 423 | (pprint-str (walk/prewalk-replace {:expound.problems/irrelevant '...} (summary-form show-valid-values? form in))))) 424 | [line prefix & _more] (re-find regex s)] 425 | (if-not line ;; can be nil depending on unforeseen *print-length* / *print-level* values: 426 | (str 427 | printed-val 428 | "\n\nin\n\n" 429 | (pprint-str form)) 430 | (let [highlighted-line (-> line 431 | (string/replace (re-pattern relevant) (escape-replacement 432 | (re-pattern relevant) 433 | (indent 0 (count prefix) (ansi/color printed-val :bad-value)))) 434 | (str "\n" (ansi/color (highlight-line prefix printed-val) 435 | :pointer)))] 436 | ;;highlighted-line 437 | (no-trailing-whitespace (string/replace s line (escape-replacement line highlighted-line))))))) 438 | -------------------------------------------------------------------------------- /src/expound/problems.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc expound.problems 2 | (:require [expound.paths :as paths] 3 | [clojure.spec.alpha :as s]) 4 | (:refer-clojure :exclude [type])) 5 | 6 | ;; can simplify when 7 | ;; https://dev.clojure.org/jira/browse/CLJ-2192 or 8 | ;; https://dev.clojure.org/jira/browse/CLJ-2258 are fixed 9 | (defn- adjust-in [form problem] 10 | ;; Three strategies for finding the value... 11 | (let [;; 1. Find the original value 12 | in1 (paths/in-with-kps form (:val problem) (:in problem) []) 13 | 14 | ;; 2. If value is unique, just find that, ignoring the 'in' path 15 | in2 (let [paths (paths/paths-to-value form (:val problem) [] [])] 16 | (if (= 1 (count paths)) 17 | (first paths) 18 | nil)) 19 | 20 | ;; 3. Find the unformed value (if there is an unformer) 21 | in3 (try 22 | #?(:bb false 23 | :clj (paths/in-with-kps form 24 | (s/unform (last (:via problem)) (:val problem)) 25 | (:in problem) []) 26 | :cljs (paths/in-with-kps form 27 | (s/unform (last (:via problem)) (:val problem)) 28 | (:in problem) [])) 29 | ;; The unform fails if there is no unformer 30 | ;; and the unform function could throw any type of 31 | ;; exception (it's provided by user) 32 | (catch #?(:cljs :default 33 | :clj java.lang.Throwable) _e 34 | nil)) 35 | new-in (cond in1 36 | in1 37 | 38 | in2 39 | in2 40 | 41 | in3 42 | in3 43 | 44 | (or (= '(apply fn) (:pred problem)) 45 | (#{:ret} (first (:path problem)))) 46 | (:in problem) 47 | 48 | :else 49 | nil)] 50 | 51 | (assoc problem 52 | :expound/in 53 | new-in))) 54 | 55 | (defn- adjust-path [failure problem] 56 | (assoc problem :expound/path 57 | ;; Orchestra 2019.02.06-1 prefixed the path, but as of 2020.07.12-1, it is not included 58 | (if (and (= :instrument failure) (#{:ret :args} (first (:path problem)))) 59 | (vec (rest (:path problem))) 60 | (:path problem)))) 61 | 62 | (defn- add-spec [spec problem] 63 | (assoc problem :spec spec)) 64 | 65 | ;; via is slightly different when using s/assert 66 | (defn fix-via [spec problem] 67 | (if (= spec (first (:via problem))) 68 | (assoc problem :expound/via (:via problem)) 69 | (assoc problem :expound/via (into [spec] (:via problem))))) 70 | 71 | (defn ^:private missing-spec? [_failure problem] 72 | (= "no method" (:reason problem))) 73 | 74 | (defn ^:private not-in-set? [_failure problem] 75 | (set? (:pred problem))) 76 | 77 | (defn ^:private fspec-exception-failure? [failure problem] 78 | (and (not= :instrument failure) 79 | (not= :check-failed failure) 80 | (= '(apply fn) (:pred problem)))) 81 | 82 | (defn ^:private fspec-ret-failure? [failure problem] 83 | (and 84 | (not= :instrument failure) 85 | (not= :check-failed failure) 86 | (= :ret (last (:path problem))))) 87 | 88 | (defn ^:private fspec-fn-failure? [failure problem] 89 | (and 90 | (not= :instrument failure) 91 | (not= :check-failed failure) 92 | (= :fn (last (:path problem))))) 93 | 94 | (defn ^:private check-ret-failure? [failure problem] 95 | (and 96 | (= :check-failed failure) 97 | (= :ret (last (:path problem))))) 98 | 99 | (defn ^:private check-fn-failure? [failure problem] 100 | (and (= :check-failed failure) 101 | (= :fn (last (:path problem))))) 102 | 103 | (defn ^:private missing-key? [_failure problem] 104 | (let [pred (:pred problem)] 105 | (and (seq? pred) 106 | (< 2 (count pred)) 107 | (s/valid? 108 | :expound.spec/contains-key-pred 109 | (nth pred 2))))) 110 | 111 | (defn ^:private insufficient-input? [_failure problem] 112 | (contains? #{"Insufficient input"} (:reason problem))) 113 | 114 | (defn ^:private extra-input? [_failure problem] 115 | (contains? #{"Extra input"} (:reason problem))) 116 | 117 | (s/fdef ptype 118 | :args (s/cat :failure (s/nilable #{:instrument :check-failed :assertion-failed}) 119 | :problem :expound.spec/problem 120 | :skip-location? boolean?)) 121 | (defn ^:private ptype [failure problem skip-locations?] 122 | (cond 123 | (:expound.spec.problem/type problem) 124 | (:expound.spec.problem/type problem) 125 | 126 | ;; This is really a location of a failure, not a failure type 127 | (and (not skip-locations?) (fspec-ret-failure? failure problem)) 128 | :expound.problem/fspec-ret-failure 129 | 130 | (fspec-exception-failure? failure problem) 131 | :expound.problem/fspec-exception-failure 132 | 133 | ;; This is really a location of a failure, not a failure type 134 | ;; (compare to check-fn-failure, which is also an fn failure, but 135 | ;; at a different location) 136 | (and (not skip-locations?) (fspec-fn-failure? failure problem)) 137 | :expound.problem/fspec-fn-failure 138 | 139 | ;; This is really a location of a failure, not a failure type 140 | (and (not skip-locations?) (check-ret-failure? failure problem)) 141 | :expound.problem/check-ret-failure 142 | 143 | ;; This is really a location of a failure, not a failure type 144 | (and (not skip-locations?) (check-fn-failure? failure problem)) 145 | :expound.problem/check-fn-failure 146 | 147 | (insufficient-input? failure problem) 148 | :expound.problem/insufficient-input 149 | 150 | (extra-input? failure problem) 151 | :expound.problem/extra-input 152 | 153 | (not-in-set? failure problem) 154 | :expound.problem/not-in-set 155 | 156 | (missing-key? failure problem) 157 | :expound.problem/missing-key 158 | 159 | (missing-spec? failure problem) 160 | :expound.problem/missing-spec 161 | 162 | :else 163 | :expound.problem/unknown)) 164 | 165 | ;;;;;;;;;;;;;;;;;;;;;;;;;; public ;;;;;;;;;;;;;;;;;;;;;;;;; 166 | 167 | (defn annotate [explain-data] 168 | (let [{::s/keys [problems value args ret fn failure spec]} explain-data 169 | caller (or (:clojure.spec.test.alpha/caller explain-data) (:orchestra.spec.test/caller explain-data)) 170 | form (if (not= :instrument failure) 171 | value 172 | (cond 173 | (contains? explain-data ::s/ret) ret 174 | (contains? explain-data ::s/args) args 175 | (contains? explain-data ::s/fn) fn 176 | :else (throw (ex-info "Invalid explain-data" {:explain-data explain-data})))) 177 | problems' (map (comp (partial adjust-in form) 178 | (partial adjust-path failure) 179 | (partial add-spec spec) 180 | (partial fix-via spec) 181 | #(assoc % :expound/form form) 182 | #(assoc % :expound.spec.problem/type (ptype failure % false))) 183 | problems)] 184 | 185 | (-> explain-data 186 | (assoc :expound/form form 187 | :expound/caller caller 188 | :expound/problems problems')))) 189 | 190 | (def type ptype) 191 | 192 | ;; Must keep this function here because 193 | ;; spell-spec uses it 194 | ;; https://github.com/bhauman/spell-spec/blob/48ea2ca544f02b04a73dc42a91aa4876dcc5fc95/src/spell_spec/expound.cljc#L20 195 | (def value-in paths/value-in) 196 | -------------------------------------------------------------------------------- /src/expound/specs.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc expound.specs 2 | (:require #?(:cljs [expound.alpha :as ex :include-macros true] 3 | :clj [expound.alpha :as ex]) 4 | [clojure.spec.alpha :as s])) 5 | 6 | ;;;; public specs ;;;;;; 7 | 8 | (s/def ::bool boolean?) 9 | #?(:clj (s/def ::bytes bytes?)) 10 | (s/def ::double double?) 11 | (s/def ::ident ident?) 12 | (s/def ::indexed indexed?) 13 | (s/def ::int int?) 14 | (s/def ::kw keyword?) 15 | (s/def ::map map?) 16 | (s/def ::nat-int nat-int?) 17 | (s/def ::neg-int neg-int?) 18 | (s/def ::pos-int pos-int?) 19 | (s/def ::qualified-ident qualified-ident?) 20 | (s/def ::qualified-kw qualified-keyword?) 21 | (s/def ::qualified-sym qualified-symbol?) 22 | (s/def ::seqable seqable?) 23 | (s/def ::simple-ident simple-ident?) 24 | (s/def ::simple-kw simple-keyword?) 25 | (s/def ::simple-sym simple-symbol?) 26 | (s/def ::str string?) 27 | (s/def ::sym symbol?) 28 | (s/def ::uri uri?) 29 | (s/def ::uuid uuid?) 30 | (s/def ::vec vector?) 31 | 32 | (ex/defmsg ::bool "should be either true or false") 33 | #?(:clj (ex/defmsg ::bytes "should be an array of bytes")) 34 | (ex/defmsg ::double "should be a double") 35 | (ex/defmsg ::ident "should be an identifier (a symbol or keyword)") 36 | (ex/defmsg ::indexed "should be an indexed collection") 37 | (ex/defmsg ::int "should be an integer") 38 | (ex/defmsg ::kw "should be a keyword") 39 | (ex/defmsg ::map "should be a map") 40 | (ex/defmsg ::nat-int "should be an integer equal to, or greater than, zero") 41 | (ex/defmsg ::neg-int "should be a negative integer") 42 | (ex/defmsg ::pos-int "should be a positive integer") 43 | (ex/defmsg ::qualified-ident "should be an identifier (a symbol or keyword) with a namespace") 44 | (ex/defmsg ::qualified-kw "should be a keyword with a namespace") 45 | (ex/defmsg ::qualified-sym "should be a symbol with a namespace") 46 | (ex/defmsg ::seqable "should be a seqable collection") 47 | (ex/defmsg ::simple-ident "should be an identifier (a symbol or keyword) with no namespace") 48 | (ex/defmsg ::simple-kw "should be a keyword with no namespace") 49 | (ex/defmsg ::simple-sym "should be a symbol with no namespace") 50 | (ex/defmsg ::str "should be a string") 51 | (ex/defmsg ::sym "should be a symbol") 52 | (ex/defmsg ::uri "should be a URI") 53 | (ex/defmsg ::uuid "should be a UUID") 54 | (ex/defmsg ::vec "should be a vector") 55 | 56 | (def ^:no-doc public-specs 57 | [::bool #?(:clj ::bytes) ::double ::ident ::indexed ::int ::kw 58 | ::map ::nat-int ::neg-int ::pos-int ::qualified-ident 59 | ::qualified-kw ::qualified-sym ::seqable ::simple-ident 60 | ::simple-kw ::simple-sym ::str ::sym ::uuid ::uri ::vec]) 61 | -------------------------------------------------------------------------------- /src/expound/util.cljc: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc expound.util 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | (def assert-message "Internal Expound assertion failed. Please report this bug at https://github.com/bhb/expound/issues") 5 | 6 | (defn nan? [x] 7 | #?(:clj (and (number? x) (Double/isNaN x)) 8 | :cljs (and (number? x) (js/isNaN x)))) 9 | 10 | (defn- parent-spec 11 | "Look up for the parent spec using the spec hierarchy." 12 | [k] 13 | (when-let [p (some-> k s/get-spec)] 14 | (or (when (qualified-ident? p) p) 15 | (s/form p)))) 16 | 17 | (defn spec-vals 18 | "Returns all spec keys or pred " 19 | ([spec-ident] 20 | (->> spec-ident 21 | (iterate parent-spec) 22 | (take-while some?)))) 23 | -------------------------------------------------------------------------------- /test/cljs_test.cljs: -------------------------------------------------------------------------------- 1 | ;; https://github.com/bhb/expound/issues/183 2 | ;; is easy to miss because if any of the dev/test dependencies 3 | ;; include 'goog.string.format', the tests will happen to pass, but 4 | ;; the bug can still occur for clients. 5 | ;; 6 | ;; To avoid this issue, I added this test that includes no other libraries. 7 | 8 | (require '[clojure.spec.alpha :as s]) 9 | (require '[expound.alpha :as expound]) 10 | 11 | (s/def :example.place/city string?) 12 | (s/def :example.place/state string?) 13 | (s/def :example/place (s/keys :req-un [:example.place/city :example.place/state])) 14 | 15 | (expound/expound :example/place {:city "Denver", :state :CO} {:print-specs? false}) 16 | -------------------------------------------------------------------------------- /test/expected_deps.txt: -------------------------------------------------------------------------------- 1 | [["org.clojure"] ["clojure"]] 2 | [["org.clojure"] ["clojurescript"]] 3 | [["org.clojure"] ["spec.alpha"]] 4 | -------------------------------------------------------------------------------- /test/expound/paths_test.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.paths-test 2 | (:require [clojure.test :as ct :refer [is deftest use-fixtures]] 3 | [clojure.test.check.generators :as gen] 4 | [com.gfredericks.test.chuck.clojure-test :refer [checking]] 5 | [expound.paths :as paths] 6 | [expound.test-utils :as test-utils] 7 | [com.gfredericks.test.chuck :as chuck])) 8 | 9 | (def num-tests 100) 10 | 11 | (use-fixtures :once 12 | test-utils/check-spec-assertions 13 | test-utils/instrument-all) 14 | 15 | (deftest compare-paths-test 16 | (checking 17 | "path to a key comes before a path to a value" 18 | 10 19 | [k gen/simple-type-printable] 20 | (is (= -1 (paths/compare-paths [(paths/->KeyPathSegment k)] [k]))) 21 | (is (= 1 (paths/compare-paths [k] [(paths/->KeyPathSegment k)]))))) 22 | 23 | (defn nth-value [form i] 24 | (let [seq (remove map-entry? (tree-seq coll? seq form))] 25 | (nth seq (mod i (count seq))))) 26 | 27 | (deftest paths-to-value-test 28 | (checking 29 | "value-in is inverse of paths-to-value" 30 | (chuck/times num-tests) 31 | [form test-utils/any-printable-wo-nan 32 | i gen/nat 33 | :let [x (nth-value form i) 34 | paths (paths/paths-to-value form x [] [])]] 35 | (is (seq paths)) 36 | (doseq [path paths] 37 | (is (= x 38 | (paths/value-in form 39 | path)))))) 40 | 41 | -------------------------------------------------------------------------------- /test/expound/print_length_test.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.print-length-test 2 | (:require [clojure.test :as ct :refer [is deftest testing]] 3 | [clojure.spec.alpha :as s] 4 | [expound.alpha] 5 | [clojure.string :as string])) 6 | 7 | (def the-value (range 10)) 8 | ;; Fails on the last element of the range 9 | (def the-spec (s/coll-of #(< % 9))) 10 | (def the-explanation (s/explain-data the-spec the-value)) 11 | 12 | (deftest print-length-test 13 | (testing "Expound works even in face of a low `*print-length*` and `*print-level*`, without throwing exceptions. 14 | See https://github.com/bhb/expound/issues/217" 15 | (doseq [length [1 5 100 *print-length*] 16 | level [1 5 100 *print-level*] 17 | ;; Note that the `is` resides outside of the `binding`. Else test output itself can be affected. 18 | :let [v (binding [*print-length* length 19 | *print-level* level] 20 | (with-out-str 21 | (expound.alpha/printer the-explanation)))]] 22 | ;; Don't make a particularly specific test assertion, since a limited print-length isn't necessarily realistic/usual: 23 | (is (not (string/blank? v)))))) 24 | -------------------------------------------------------------------------------- /test/expound/printer_test.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.printer-test 2 | (:require [clojure.spec.alpha :as s] 3 | [clojure.test :as ct :refer [is deftest use-fixtures testing]] 4 | [expound.printer :as printer] 5 | [clojure.string :as string] 6 | [com.gfredericks.test.chuck.clojure-test :refer [checking]] 7 | [expound.test-utils :as test-utils :refer [contains-nan?]] 8 | [expound.spec-gen :as sg] 9 | [expound.problems :as problems])) 10 | 11 | (def num-tests 5) 12 | 13 | (use-fixtures :once 14 | test-utils/check-spec-assertions 15 | test-utils/instrument-all) 16 | 17 | (defn example-fn []) 18 | (defn get-args [& args] args) 19 | 20 | (deftest pprint-fn 21 | (is (= "string?" 22 | (printer/pprint-fn (::s/spec (s/explain-data string? 1))))) 23 | (is (= "expound.printer-test/example-fn" 24 | (printer/pprint-fn example-fn))) 25 | (is (= "" 26 | (printer/pprint-fn #(inc (inc %))))) 27 | (is (= "" 28 | (printer/pprint-fn (constantly true)))) 29 | (is (= "" 30 | (printer/pprint-fn (comp vec str)))) 31 | (is (= "expound.test-utils/instrument-all" 32 | (printer/pprint-fn test-utils/instrument-all))) 33 | (is (= "expound.test-utils/contains-nan?" 34 | (printer/pprint-fn contains-nan?)))) 35 | 36 | (s/def :print-spec-keys/field1 string?) 37 | (s/def :print-spec-keys/field2 (s/coll-of :print-spec-keys/field1)) 38 | (s/def :print-spec-keys/field3 int?) 39 | (s/def :print-spec-keys/field4 string?) 40 | (s/def :print-spec-keys/field5 string?) 41 | (s/def :print-spec-keys/key-spec (s/keys 42 | :req [:print-spec-keys/field1] 43 | :req-un [:print-spec-keys/field2])) 44 | (s/def :print-spec-keys/key-spec2 (s/keys 45 | :req-un [(and 46 | :print-spec-keys/field1 47 | (or 48 | :print-spec-keys/field2 49 | :print-spec-keys/field3))])) 50 | (s/def :print-spec-keys/key-spec3 (s/keys 51 | :req-un [:print-spec-keys/field1 52 | :print-spec-keys/field4 53 | :print-spec-keys/field5])) 54 | (s/def :print-spec-keys/set-spec (s/coll-of :print-spec-keys/field1 55 | :kind set?)) 56 | (s/def :print-spec-keys/vector-spec (s/coll-of :print-spec-keys/field1 57 | :kind vector?)) 58 | (s/def :print-spec-keys/key-spec4 (s/keys 59 | :req-un [:print-spec-keys/set-spec 60 | :print-spec-keys/vector-spec 61 | :print-spec-keys/key-spec3])) 62 | 63 | (defn copy-key [m k1 k2] 64 | (assoc m k2 (get m k1))) 65 | 66 | (deftest print-spec-keys* 67 | (is (= 68 | [{"key" :field2, "spec" "(coll-of :print-spec-keys/field1)"} 69 | {"key" :print-spec-keys/field1, "spec" "string?"}] 70 | (printer/print-spec-keys* 71 | (map #(copy-key % :via :expound/via) 72 | (::s/problems 73 | (s/explain-data 74 | :print-spec-keys/key-spec 75 | {})))))) 76 | (is (nil? 77 | (printer/print-spec-keys* 78 | (map #(copy-key % :via :expound/via) 79 | (::s/problems 80 | (s/explain-data 81 | (s/keys 82 | :req [:print-spec-keys/field1] 83 | :req-un [:print-spec-keys/field2]) 84 | {})))))) 85 | 86 | (is (= 87 | [{"key" :print-spec-keys/field1, "spec" "string?"}] 88 | (printer/print-spec-keys* 89 | (map #(copy-key % :via :expound/via) 90 | (::s/problems 91 | (s/explain-data 92 | (s/keys 93 | :req [:print-spec-keys/field1] 94 | :req-un [:print-spec-keys/field2]) 95 | {:field2 [""]})))))) 96 | 97 | (is (= 98 | [{"key" :print-spec-keys/field1, "spec" "string?"} 99 | {"key" :print-spec-keys/field2, 100 | "spec" "(coll-of :print-spec-keys/field1)"}] 101 | (printer/print-spec-keys* 102 | (map #(copy-key % :via :expound/via) 103 | (::s/problems 104 | (s/explain-data 105 | (s/keys 106 | :req [:print-spec-keys/field1 107 | :print-spec-keys/field2]) 108 | {})))))) 109 | (is (= 110 | [{"key" :field1, "spec" "string?"} 111 | {"key" :field2, "spec" "(coll-of :print-spec-keys/field1)"} 112 | {"key" :field3, "spec" "int?"}] 113 | (printer/print-spec-keys* 114 | (map #(copy-key % :via :expound/via) 115 | (::s/problems 116 | (s/explain-data 117 | :print-spec-keys/key-spec2 118 | {})))))) 119 | (is (= 120 | [{"key" :key-spec3, 121 | "spec" #?(:clj 122 | "(keys\n :req-un\n [:print-spec-keys/field1\n :print-spec-keys/field4\n :print-spec-keys/field5])" 123 | :cljs 124 | "(keys\n :req-un\n [:print-spec-keys/field1\n :print-spec-keys/field4 \n :print-spec-keys/field5])")} 125 | {"key" :set-spec, "spec" #?(:clj 126 | "(coll-of\n :print-spec-keys/field1\n :kind\n set?)" 127 | :cljs 128 | "(coll-of :print-spec-keys/field1 :kind set?)")} 129 | {"key" :vector-spec, "spec" #?(:clj "(coll-of\n :print-spec-keys/field1\n :kind\n vector?)" 130 | :cljs "(coll-of\n :print-spec-keys/field1 \n :kind \n vector?)")}] 131 | (printer/print-spec-keys* 132 | (map #(copy-key % :via :expound/via) 133 | (::s/problems 134 | (s/explain-data 135 | :print-spec-keys/key-spec4 136 | {}))))))) 137 | 138 | (deftest print-table 139 | (is (= 140 | " 141 | | :key | :spec | 142 | |======+=======| 143 | | abc | a | 144 | | | b | 145 | |------+-------| 146 | | def | d | 147 | | | e | 148 | " 149 | (printer/print-table [{:key "abc" :spec "a\nb"} 150 | {:key "def" :spec "d\ne"}]))) 151 | ;; can select ordering of keys 152 | (is (= 153 | " 154 | | :b | :c | 155 | |====+====| 156 | | 2 | 3 | 157 | |----+----| 158 | | {} | () | 159 | " 160 | (printer/print-table 161 | [:b :c] 162 | [{:a 1 :b 2 :c 3} 163 | {:a [] :b {} :c '()}]))) 164 | 165 | ;; ordering is deterministic, not based on hashmap 166 | ;; semantics 167 | (is (= 168 | " 169 | | :k | :a | :b | :c | :d | :e | :f | :g | :h | :i | :j | 170 | |====+====+====+====+====+====+====+====+====+====+====| 171 | | k | a | b | c | d | e | f | g | h | i | j | 172 | |----+----+----+----+----+----+----+----+----+----+----| 173 | | k | a | b | c | d | e | f | g | h | i | j | 174 | " 175 | (printer/print-table 176 | [:k :a :b :c :d :e :f :g :h :i :j] 177 | [{:a "a" :b "b" :c "c" :d "d" :e "e" :f "f" :g "g" :h "h" :i "i" :j "j" :k "k" :l "l"} 178 | {:l "l" :k "k" :j "j" :i "i" :h "h" :g "g" :f "f" :e "e" :d "d" :c "c" :b "b" :a "a"}])))) 179 | 180 | (deftest print-table-gen 181 | (checking 182 | "any table with have constant width" 183 | num-tests 184 | [col-count (s/gen pos-int?) 185 | keys (s/gen (s/coll-of keyword? :min-count 1)) 186 | row-count (s/gen pos-int?) 187 | vals (s/gen (s/coll-of 188 | (s/coll-of string? :count col-count) 189 | :count row-count)) 190 | :let [rows (mapv 191 | #(zipmap keys (get vals %)) 192 | (range 0 row-count)) 193 | table (printer/print-table rows) 194 | srows (rest (string/split table #"\n"))]] 195 | 196 | (is (apply = (map count srows)))) 197 | 198 | (checking 199 | "any table will contain a sub-table of all rows but the last" 200 | num-tests 201 | [col-count (s/gen pos-int?) 202 | keys (s/gen (s/coll-of keyword? :min-count 1)) 203 | row-count (s/gen (s/int-in 2 10)) 204 | vals (s/gen (s/coll-of 205 | (s/coll-of string? :count col-count) 206 | :count row-count)) 207 | :let [rows (mapv 208 | #(zipmap keys (get vals %)) 209 | (range 0 row-count)) 210 | sub-rows (butlast rows) 211 | table (printer/print-table rows) 212 | sub-table (printer/print-table sub-rows) 213 | sub-table-last-row (last (string/split sub-table #"\n")) 214 | table-last-row (last (string/split table #"\n"))]] 215 | ;; If the line we delete shrinks the width of the table 216 | ;; (because it was the widest value) 217 | ;; then the property will not apply 218 | (when (= (count sub-table-last-row) (count table-last-row)) 219 | (is (string/includes? table sub-table)))) 220 | 221 | #?(:clj 222 | (checking 223 | "for any known registered spec, table has max width" 224 | num-tests 225 | [spec sg/spec-gen 226 | :let [rows [{:key spec 227 | :spec (printer/expand-spec spec)}] 228 | table (printer/print-table rows) 229 | srows (rest (string/split table #"\n"))]] 230 | (is (< (count (last srows)) 200))) 231 | :cljs 232 | ;; Noop, just to make clj-kondo happy 233 | (sg/topo-sort []))) 234 | 235 | (deftest highlighted-value 236 | (testing "atomic value" 237 | (is (= "\"Fred\"\n^^^^^^" 238 | (printer/highlighted-value 239 | {} 240 | {:expound/form "Fred" 241 | :expound/in []})))) 242 | (testing "value in vector" 243 | (is (= "[... :b ...]\n ^^" 244 | (printer/highlighted-value 245 | {} 246 | {:expound/form [:a :b :c] 247 | :expound/in [1]})))) 248 | (testing "long, composite values are pretty-printed" 249 | (is (= (str "{:letters {:a \"aaaaaaaa\", 250 | :b \"bbbbbbbb\", 251 | :c \"cccccccd\", 252 | :d \"dddddddd\", 253 | :e \"eeeeeeee\"}}" 254 | #?(:clj "\n ^^^^^^^^^^^^^^^" 255 | :cljs "\n ^^^^^^^^^^^^^^^^")) 256 | ;; ^- the above works in clojure - maybe not CLJS? 257 | (printer/highlighted-value 258 | {} 259 | {:expound/form 260 | {:letters 261 | {:a "aaaaaaaa" 262 | :b "bbbbbbbb" 263 | :c "cccccccd" 264 | :d "dddddddd" 265 | :e "eeeeeeee"}} 266 | :expound/in [:letters]})))) 267 | (testing "args to function" 268 | (is (= "(1 ... ...)\n ^" 269 | (printer/highlighted-value 270 | {} 271 | {:expound/form (get-args 1 2 3) 272 | :expound/in [0]})))) 273 | (testing "show all values" 274 | (is (= "(1 2 3)\n ^" 275 | (printer/highlighted-value 276 | {:show-valid-values? true} 277 | {:expound/form (get-args 1 2 3) 278 | :expound/in [0]})))) 279 | 280 | (testing "special replacement chars are not used" 281 | (is (= "\"$ $$ $1 $& $` $'\"\n^^^^^^^^^^^^^^^^^^" 282 | (printer/highlighted-value 283 | {} 284 | (first 285 | (:expound/problems 286 | (problems/annotate 287 | (s/explain-data keyword? "$ $$ $1 $& $` $'")))))))) 288 | 289 | (testing "nested map-of specs" 290 | (is (= "{:a {:b 1}}\n ^" 291 | (printer/highlighted-value 292 | {} 293 | (first 294 | (:expound/problems 295 | (problems/annotate 296 | (s/explain-data :highlighted-value/nested-map-of {:a {:b 1}}))))))) 297 | (is (= "{:a {\"a\" ...}}\n ^^^" 298 | (printer/highlighted-value 299 | {} 300 | (first 301 | (:expound/problems 302 | (problems/annotate 303 | (s/explain-data :highlighted-value/nested-map-of {:a {"a" :b}}))))))) 304 | (is (= "{1 ...}\n ^" 305 | (printer/highlighted-value 306 | {} 307 | (first 308 | (:expound/problems 309 | (problems/annotate 310 | (s/explain-data :highlighted-value/nested-map-of {1 {:a :b}})))))))) 311 | 312 | (testing "nested keys specs" 313 | (is (= "{:address {:city 1}}\n ^" 314 | (printer/highlighted-value 315 | {} 316 | (first 317 | (:expound/problems 318 | (problems/annotate 319 | (s/explain-data :highlighted-value/house {:address {:city 1}}))))))) 320 | (is (= "{:address {\"city\" \"Denver\"}}\n ^^^^^^^^^^^^^^^^^" 321 | (printer/highlighted-value 322 | {} 323 | (first 324 | (:expound/problems 325 | (problems/annotate 326 | (s/explain-data :highlighted-value/house {:address {"city" "Denver"}}))))))) 327 | (is (= "{\"address\" {:city \"Denver\"}}\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^" 328 | (printer/highlighted-value 329 | {} 330 | (first 331 | (:expound/problems 332 | (problems/annotate 333 | (s/explain-data :highlighted-value/house {"address" {:city "Denver"}}))))))))) 334 | 335 | (deftest highlighted-value-on-alt 336 | (is (= "[... 0]\n ^" 337 | (printer/highlighted-value 338 | {} 339 | (first 340 | (:expound/problems 341 | (problems/annotate 342 | (s/explain-data 343 | (clojure.spec.alpha/alt :a int? 344 | :b (clojure.spec.alpha/spec (clojure.spec.alpha/cat :c int?))) 345 | [1 0])))))))) 346 | 347 | (deftest highlighted-value-on-coll-of 348 | ;; sets 349 | (is (= "#{1 3 2 :a}\n ^^" 350 | (printer/highlighted-value 351 | {} 352 | (first 353 | (:expound/problems 354 | (problems/annotate 355 | (s/explain-data 356 | (s/coll-of integer?) 357 | #{1 :a 2 3}))))))) 358 | (is (= "#{:a}\n ^^" 359 | (printer/highlighted-value 360 | {} 361 | (first 362 | (:expound/problems 363 | (problems/annotate 364 | (s/explain-data 365 | (s/coll-of integer?) 366 | #{:a}))))))) 367 | 368 | ;; lists 369 | (is (= "(... :a ... ...)\n ^^" 370 | (printer/highlighted-value 371 | {} 372 | (first 373 | (:expound/problems 374 | (problems/annotate 375 | (s/explain-data 376 | (s/coll-of integer?) 377 | '(1 :a 2 3)))))))) 378 | (is (= "(:a)\n ^^" 379 | (printer/highlighted-value 380 | {} 381 | (first 382 | (:expound/problems 383 | (problems/annotate 384 | (s/explain-data 385 | (s/coll-of integer?) 386 | '(:a)))))))) 387 | 388 | ;; vectors 389 | (is (= "[... :a ... ...]\n ^^" 390 | (printer/highlighted-value 391 | {} 392 | (first 393 | (:expound/problems 394 | (problems/annotate 395 | (s/explain-data 396 | (s/coll-of integer?) 397 | [1 :a 2 3]))))))) 398 | 399 | (is (= "[:a]\n ^^" 400 | (printer/highlighted-value 401 | {} 402 | (first 403 | (:expound/problems 404 | (problems/annotate 405 | (s/explain-data 406 | (s/coll-of integer?) 407 | [:a]))))))) 408 | 409 | ;; maps 410 | (is (= "[1 :a]\n^^^^^^" 411 | (printer/highlighted-value 412 | {} 413 | (first 414 | (:expound/problems 415 | (problems/annotate 416 | (s/explain-data 417 | (s/coll-of integer?) 418 | {1 :a 2 3}))))))) 419 | 420 | (is (= "[:a 1]\n^^^^^^" 421 | (printer/highlighted-value 422 | {} 423 | (first 424 | (:expound/problems 425 | (problems/annotate 426 | (s/explain-data 427 | (s/coll-of integer?) 428 | {:a 1})))))))) 429 | -------------------------------------------------------------------------------- /test/expound/problems_test.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.problems-test 2 | (:require [clojure.test :as ct :refer [is deftest use-fixtures]] 3 | [clojure.spec.alpha :as s] 4 | [expound.problems :as problems] 5 | [expound.test-utils :as test-utils])) 6 | 7 | (use-fixtures :once 8 | test-utils/check-spec-assertions 9 | test-utils/instrument-all) 10 | 11 | (s/def :highlighted-value/nested-map-of (s/map-of keyword? (s/map-of keyword? keyword?))) 12 | 13 | (s/def :highlighted-value/city string?) 14 | (s/def :highlighted-value/address (s/keys :req-un [:highlighted-value/city])) 15 | (s/def :highlighted-value/house (s/keys :req-un [:highlighted-value/address])) 16 | 17 | (s/def :annotate-test/div-fn (s/fspec 18 | :args (s/cat :x int? :y pos-int?))) 19 | (defn my-div [x y] 20 | (assert (pos? (/ x y)))) 21 | 22 | (deftest annotate-test 23 | (is (= {:expound/in [0] 24 | :val '(0 1) 25 | :reason "Assert failed: (pos? (/ x y))"} 26 | (-> (s/explain-data (s/coll-of :annotate-test/div-fn) [my-div]) 27 | problems/annotate 28 | :expound/problems 29 | first 30 | (select-keys [:expound/in :val :reason]))))) -------------------------------------------------------------------------------- /test/expound/spec_gen.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.spec-gen 2 | (:require [clojure.spec.alpha :as s] 3 | [com.stuartsierra.dependency :as deps] 4 | [clojure.test.check.generators :as gen] 5 | [expound.alpha :as expound])) 6 | 7 | ;; I want to do something like 8 | ;; (s/def :specs.coll-of/into #{[] '() #{}}) 9 | ;; but Clojure (not Clojurescript) won't allow 10 | ;; this. As a workaround, I'll just use vectors instead 11 | ;; of vectors and lists. 12 | ;; FIXME - force a specific type of into/kind one for each test 13 | ;; (one for vectors, one for lists, etc) 14 | 15 | (s/def :specs.coll-of/into #{[] #{}}) 16 | (s/def :specs.coll-of/kind #{vector? list? set?}) 17 | (s/def :specs.coll-of/count pos-int?) 18 | (s/def :specs.coll-of/max-count pos-int?) 19 | (s/def :specs.coll-of/min-count pos-int?) 20 | (s/def :specs.coll-of/distinct boolean?) 21 | 22 | (s/def :specs/every-args 23 | (s/keys :req-un 24 | [:specs.coll-of/into 25 | :specs.coll-of/kind 26 | :specs.coll-of/count 27 | :specs.coll-of/max-count 28 | :specs.coll-of/min-count 29 | :specs.coll-of/distinct])) 30 | 31 | (defn apply-coll-of [spec {:keys [into max-count min-count distinct]}] 32 | (s/coll-of spec :into into :min-count min-count :max-count max-count :distinct distinct)) 33 | 34 | (defn apply-map-of [spec1 spec2 {:keys [into max-count min-count distinct _gen-max]}] 35 | (s/map-of spec1 spec2 :into into :min-count min-count :max-count max-count :distinct distinct)) 36 | 37 | ;; Since CLJS prints out entire source of a function when 38 | ;; it pretty-prints a failure, the output becomes much nicer if 39 | ;; we wrap each function in a simple spec 40 | (expound/def :specs/string string? "should be a string") 41 | (expound/def :specs/vector vector? "should be a vector") 42 | (s/def :specs/int int?) 43 | (s/def :specs/boolean boolean?) 44 | (expound/def :specs/keyword keyword? "should be a keyword") 45 | (s/def :specs/map map?) 46 | (s/def :specs/symbol symbol?) 47 | (s/def :specs/pos-int pos-int?) 48 | (s/def :specs/neg-int neg-int?) 49 | (s/def :specs/zero #(and (number? %) (zero? %))) 50 | (s/def :specs/keys (s/keys 51 | :req-un [:specs/string] 52 | :req [:specs/map] 53 | :opt-un [:specs/vector] 54 | :opt [:specs/int])) 55 | 56 | (def simple-spec-gen (gen/one-of 57 | [(gen/elements [:specs/string 58 | :specs/vector 59 | :specs/int 60 | :specs/boolean 61 | :specs/keyword 62 | :specs/map 63 | :specs/symbol 64 | :specs/pos-int 65 | :specs/neg-int 66 | :specs/zero 67 | :specs/keys]) 68 | (gen/set gen/simple-type-printable)])) 69 | 70 | (defn spec-dependencies [spec] 71 | (->> spec 72 | s/form 73 | (tree-seq coll? seq) 74 | (filter #(and (s/get-spec %) (not= spec %))) 75 | distinct)) 76 | 77 | (defn topo-sort [specs] 78 | (deps/topo-sort 79 | (reduce 80 | (fn [gr spec] 81 | (reduce 82 | (fn [g d] 83 | ;; If this creates a circular reference, then 84 | ;; just skip it. 85 | (if (deps/depends? g d spec) 86 | g 87 | (deps/depend g spec d))) 88 | gr 89 | (spec-dependencies spec))) 90 | (deps/graph) 91 | specs))) 92 | 93 | #?(:clj 94 | (def spec-gen (gen/elements (->> (s/registry) 95 | (map key) 96 | topo-sort 97 | (filter keyword?))))) 98 | -------------------------------------------------------------------------------- /test/expound/specs_test.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.specs-test 2 | (:require [expound.specs] 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :as ct :refer [is deftest use-fixtures]] 5 | [expound.test-utils :as test-utils] 6 | [expound.alpha :as expound])) 7 | 8 | (use-fixtures :once 9 | test-utils/check-spec-assertions 10 | test-utils/instrument-all) 11 | 12 | (deftest provided-specs 13 | (binding [s/*explain-out* (expound/custom-printer {:print-specs? false})] 14 | (is (= "-- Spec failed -------------------- 15 | 16 | 1 17 | 18 | should be a keyword with no namespace 19 | 20 | ------------------------- 21 | Detected 1 error 22 | " 23 | (s/explain-str :expound.specs/simple-kw 1))) 24 | (doseq [kw expound.specs/public-specs] 25 | (is (some? (s/get-spec kw)) (str "Failed to find spec for keyword " kw)) 26 | (is (some? (expound/error-message kw)) (str "Failed to find error message for keyword " kw))))) 27 | -------------------------------------------------------------------------------- /test/expound/spell_spec_test.cljc: -------------------------------------------------------------------------------- 1 | ;; copied from 2 | ;; https://github.com/bhauman/spell-spec/blob/master/test/spell_spec/expound_test.cljc 3 | ;; so I don't break the extension API 4 | (ns expound.spell-spec-test 5 | (:require [#?(:clj clojure.test :cljs cljs.test) 6 | :refer [deftest is testing]] 7 | [#?(:clj clojure.spec.alpha 8 | :cljs cljs.spec.alpha) 9 | :as s] 10 | [clojure.string :as string] 11 | [spell-spec.alpha :as spell :refer [warn-keys strict-keys warn-strict-keys]] 12 | [expound.alpha :as exp] 13 | [spell-spec.expound :as sp.ex])) 14 | 15 | ;; copied from 16 | ;; https://github.com/bhauman/spell-spec/blob/48ea2ca544f02b04a73dc42a91aa4876dcc5fc95/src/spell_spec/expound.cljc#L23-L34 17 | ;; because test-refresh doesn't refesh libraries if I set explicit paths and 18 | ;; if I don't restrict the paths, it tries to reload deps in the CLJS build 19 | 20 | (defmethod exp/problem-group-str :spell-spec.alpha/misspelled-key [_type spec-name val path problems opts] 21 | (sp.ex/exp-formated "Misspelled map key" _type spec-name val path problems opts)) 22 | 23 | (defmethod exp/expected-str :spell-spec.alpha/misspelled-key [_type _spec-name _val _path problems _opts] 24 | (let [{:keys [:spell-spec.alpha/likely-misspelling-of]} (first problems)] 25 | (str "should probably be" (sp.ex/format-correction-list likely-misspelling-of)))) 26 | 27 | (defmethod exp/problem-group-str :spell-spec.alpha/unknown-key [_type spec-name val path problems opts] 28 | (sp.ex/exp-formated "Unknown map key" _type spec-name val path problems opts)) 29 | 30 | (defmethod exp/expected-str :spell-spec.alpha/unknown-key [_type _spec-name _val _path problems _opts] 31 | (str "should be" (sp.ex/format-correction-list (-> problems first :pred)))) 32 | 33 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;; 34 | 35 | (defn fetch-warning-output [thunk] 36 | #?(:clj (binding [*err* (java.io.StringWriter.)] 37 | (thunk) 38 | (str *err*)) 39 | :cljs (with-out-str (thunk)))) 40 | 41 | (deftest check-misspell-test 42 | (let [spec (spell/keys :opt-un [::hello ::there]) 43 | data {:there 1 :helloo 1 :barabara 1} 44 | result 45 | (exp/expound-str spec data)] 46 | (is (string/includes? result "Misspelled map key")) 47 | (is (string/includes? result "should probably be")) 48 | (is (string/includes? result " :hello\n")))) 49 | 50 | (deftest check-misspell-with-namespace-test 51 | (let [spec (spell/keys :opt [::hello ::there]) 52 | data {::there 1 ::helloo 1 :barabara 1} 53 | result (exp/expound-str spec data)] 54 | (is (string/includes? result "Misspelled map key")) 55 | (is (string/includes? result "should probably be")) 56 | (is (string/includes? result ":expound.spell-spec-test/hello\n")))) 57 | 58 | (s/def ::hello integer?) 59 | (s/def ::there integer?) 60 | 61 | (deftest other-errors-test 62 | (let [spec (spell/keys :opt-un [::hello ::there]) 63 | data {:there "1" :helloo 1 :barabara 1} 64 | result (exp/expound-str spec data)] 65 | (is (string/includes? result "Misspelled map key")) 66 | (is (string/includes? result "should probably be")) 67 | (is (string/includes? result " :hello\n")) 68 | 69 | (is (not (string/includes? result "Spec failed"))) 70 | (is (not (string/includes? result "should satisfy"))) 71 | (is (not (string/includes? result "integer?"))))) 72 | 73 | (deftest warning-is-valid-test 74 | (let [spec (warn-keys :opt-un [::hello ::there]) 75 | data {:there 1 :helloo 1 :barabara 1}] 76 | (testing "expound prints warning to *err*" 77 | (is (= (fetch-warning-output #(exp/expound-str spec data)) 78 | "SPEC WARNING: possible misspelled map key :helloo should probably be :hello in {:there 1, :helloo 1, :barabara 1}\n"))))) 79 | 80 | (deftest strict-keys-test 81 | (let [spec (strict-keys :opt-un [::hello ::there]) 82 | data {:there 1 :barabara 1} 83 | result (exp/expound-str spec data)] 84 | (is (string/includes? result "Unknown map key")) 85 | (is (string/includes? result "should be one of")) 86 | (is (string/includes? result " :hello, :there\n")))) 87 | 88 | (deftest warn-on-unknown-keys-test 89 | (let [spec (warn-strict-keys :opt-un [::hello ::there]) 90 | data {:there 1 :barabara 1}] 91 | (testing "expound prints warning to *err*" 92 | (is (= (fetch-warning-output #(exp/expound-str spec data)) 93 | "SPEC WARNING: unknown map key :barabara in {:there 1, :barabara 1}\n"))))) 94 | 95 | (deftest multiple-spelling-matches 96 | (let [spec (spell/keys :opt-un [::hello1 ::hello2 ::hello3 ::hello4 ::there]) 97 | data {:there 1 :helloo 1 :barabara 1} 98 | result (exp/expound-str spec data)] 99 | (is (string/includes? result "Misspelled map key")) 100 | (is (string/includes? result "should probably be one of")) 101 | (doseq [k [:hello1 :hello2 :hello3 :hello4]] 102 | (is (string/includes? result (pr-str k))))) 103 | (let [spec (spell/keys :opt-un [::hello1 ::hello2 ::hello3 ::there]) 104 | data {:there 1 :helloo 1 :barabara 1} 105 | result (exp/expound-str spec data)] 106 | (is (string/includes? result "Misspelled map key")) 107 | (is (string/includes? result "should probably be one of")) 108 | (is (not (string/includes? result (pr-str :hello4)))) 109 | (doseq [k [:hello1 :hello2 :hello3]] 110 | (is (string/includes? result (pr-str k))))) 111 | (let [spec (spell/keys :opt-un [::hello ::there]) 112 | data {:there 1 :helloo 1 :barabara 1} 113 | result (exp/expound-str spec data)] 114 | (is (string/includes? result "Misspelled map key")) 115 | (is (string/includes? result "should probably be: :hello\n")))) 116 | -------------------------------------------------------------------------------- /test/expound/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns expound.test-runner 2 | (:require [jx.reporter.karma :refer-macros [#_run-tests #_run-all-tests]] 3 | [expound.alpha-test] 4 | [expound.paths-test] 5 | [expound.printer-test] 6 | [expound.print-length-test] 7 | [expound.problems-test] 8 | [expound.test-utils] 9 | [expound.specs-test] 10 | [expound.spell-spec-test] 11 | [expound.util-test])) 12 | 13 | (enable-console-print!) 14 | 15 | ;; runs all tests in all namespaces 16 | ;; This is what runs by default 17 | (defn ^:export run-all [karma] 18 | (jx.reporter.karma/run-all-tests karma)) 19 | 20 | ;; runs all tests in all namespaces - only namespaces with names matching 21 | ;; the regular expression will be tested 22 | ;; You can use this by changing client.args in karma.conf.js 23 | #_(defn ^:export run-all-regex [karma] 24 | (run-all-tests karma #".*-test$")) 25 | 26 | ;; runs all tests in the given namespaces 27 | ;; You can use this by changing client.args in karma.conf.js 28 | #_(defn ^:export run [karma] 29 | (run-tests karma 'expound.alpha-test)) 30 | -------------------------------------------------------------------------------- /test/expound/test_utils.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.test-utils 2 | (:require [clojure.spec.alpha :as s] 3 | #?(:cljs 4 | [clojure.spec.test.alpha :as st] 5 | ;; FIXME 6 | ;; orchestra is supposed to work with cljs but 7 | ;; it isn't working for me right now 8 | #_[orchestra-cljs.spec.test :as st] 9 | :clj [orchestra.spec.test :as st]) 10 | [expound.alpha :as expound] 11 | [clojure.test :as ct] 12 | [com.gfredericks.test.chuck.clojure-test :as chuck] 13 | [expound.util :as util] 14 | [clojure.test.check.generators :as gen])) 15 | 16 | ;; test.chuck defines a reporter for the shrunk results, but only for the 17 | ;; default reporter (:cljs.test/default). Since karma uses its own reporter, 18 | ;; we need to provide an implementation of the report multimethod for 19 | ;; the karma reporter and shrunk results 20 | 21 | (defmethod ct/report [:jx.reporter.karma/karma ::chuck/shrunk] [m] 22 | (let [f (get (methods ct/report) [::ct/default ::chuck/shrunk])] 23 | (f m))) 24 | 25 | (defn check-spec-assertions [test-fn] 26 | (s/check-asserts true) 27 | (test-fn) 28 | (s/check-asserts false)) 29 | 30 | (defn instrument-all [test-fn] 31 | (binding [s/*explain-out* (expound/custom-printer {:theme :figwheel-theme})] 32 | (st/instrument) 33 | (test-fn) 34 | (st/unstrument))) 35 | 36 | (defn contains-nan? [x] 37 | (boolean (some util/nan? (tree-seq coll? identity x)))) 38 | 39 | (def any-printable-wo-nan (gen/such-that (complement contains-nan?) 40 | gen/any-printable)) 41 | -------------------------------------------------------------------------------- /test/expound/util_test.cljc: -------------------------------------------------------------------------------- 1 | (ns expound.util-test 2 | (:require 3 | [clojure.spec.alpha :as s] 4 | [clojure.test :as ct :refer [deftest is use-fixtures]] 5 | [expound.test-utils :as test-utils] 6 | [expound.util :as util])) 7 | 8 | (use-fixtures :once 9 | test-utils/check-spec-assertions 10 | test-utils/instrument-all) 11 | 12 | (deftest test-spec-vals 13 | (s/def ::foo-pred (fn [_] true)) 14 | (s/def ::foo-string string?) 15 | 16 | (s/def ::bar ::foo-pred) 17 | (s/def ::baz ::bar) 18 | 19 | (is (= (util/spec-vals ::bar) 20 | [::bar ::foo-pred (list `fn ['_] true)])) 21 | 22 | (s/def ::bar ::foo-string) 23 | (is (= (util/spec-vals ::bar) 24 | [::bar ::foo-string `string?])) 25 | 26 | (is (= (util/spec-vals ::foo-string) 27 | [::foo-string `string?])) 28 | 29 | (is (= (util/spec-vals ::lone) 30 | [::lone])) 31 | 32 | (s/def ::foo-pred nil) 33 | (s/def ::foo-string nil) 34 | (s/def ::bar nil) 35 | (s/def ::baz nil)) 36 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :clj 3 | :test-paths ["test"] 4 | } 5 | #_{:type :kaocha.type/spec.test.check 6 | :id :generative-fdef-checks} 7 | ]} 8 | --------------------------------------------------------------------------------