Expound

repository·master·Indexed 21 days ago

https://github.com/bhb/expound

A formatting library for Clojure and ClojureScript that transforms technical clojure.spec error messages into human-friendly, structured reports. It provides visual summaries of failed values and unmet requirements, replacing standard clojure.spec.alpha explain functions. Expound supports custom error messages via defmsg, configurable printer options for themes and value formatting, and integration with clojure.spec.test.alpha/check via explain-results.

Tokens
7.4K
Snippets
31
Records
42
Agent score
75%

What's inside expound

  1. What is Expound

    master
    Expound is a tool designed to format clojure.spec error messages into a human-readable format. Instead of raw, repetitive spec failure logs, Expound provides structured, visual summaries that clearly show the value that failed and the specific requirements (predicates/keys) that were not met.
  2. Identify missing keys in maps

    master

    For specs using s/keys or s/req-un, Expound generates a table when keys are missing. This table explicitly maps each missing key to its required spec, providing a clear checklist for validation.

    (s/def :address.west-coast/city clojure.core/string?)
    (s/def :address.west-coast/state #{"CA" "WA" "OR"})
    (s/def :app/address (s/keys :req-un [:address.west-coast/city :address.west-coast/state]))
    
    ;; Input: {}
    ;; Expound output:
    ;; | key    | spec              |
    ;; |========+===================|
    ;; | :city  | string?           |
    ;; | :state | #{"CA" "WA" "OR"} |
  3. Handle set-based spec failures

    master

    If a value fails a spec defined by a set (e.g., a collection of allowed values), Expound lists the valid options that the value should have been one of.

    (s/def :address.west-coast/state #{"CA" "WA" "OR"})
    
    ;; Input: {:state "ID"}
    ;; Expound output:
    ;; should be one of: "CA", "OR", "WA"
  4. Debug sequence length errors in `s/cat`

    master

    Expound provides specific syntax error messages for sequence-based specs (s/cat):

    1. Too few elements: If a sequence is too short, Expound identifies the missing element and the spec it was expected to satisfy.
    2. Too many elements: If a sequence has extra elements, Expound points to the specific extra input that should be removed.
    (s/def :app/ingredient (s/cat :quantity clojure.core/number? :unit clojure.core/keyword?))
    
    ;; Too few elements: [100]
    ;; Expound: should have additional elements. The next element ":unit" should satisfy keyword?
    
    ;; Too many elements: [100 :teaspoon :sugar]
    ;; Expound: [... ... :sugar] has extra input
  5. Understand the structure of Spec problems

    master

    When using expound to analyze Clojure Spec failures, each problem in the :clojure.spec.alpha/problems vector contains specific keys that describe why a value failed validation. Understanding these keys allows you to pinpoint exactly where in a data structure a spec failed and which predicate was responsible.

    Key fields in a problem map:

    • :in: A vector of keys used to navigate to the invalid value in the original data structure. Unlike get-in, these keys can locate keys within maps and work with lists.
    • :via: A vector of spec names (keywords) traversed to reach the failing spec. Unnamed specs appear as :missing.
    • :path: A vector containing both the keys used for navigation and the names of alternate branches chosen when evaluating s/or specs.
    • :pred: The symbol of the predicate function that failed.
    • :val: The actual value that caused the spec failure.
    ;; Example problem structure returned by (s/explain-data ...)
    {:path [:id :num],
     :pred clojure.core/pos-int?,
     :val -1,
     :via [:example/entity :example/id],
     :in [:id]}
  6. Configure value printers using a printer builder function

    master
    Expound allows users to customize how data values are printed (e.g., whether to omit large or irrelevant values to reduce noise) by using a "printer builder" function. Instead of manually constructing complex printer internals, you can use a builder function that accepts configuration arguments and returns a new printer function. This printer function can then be assigned to the *explain-out* dynamic variable to change the output format for Expound.
  7. Handling non-conforming values when using conformers

    master

    Expound highlights non-conforming values (NCV) within a larger context. However, if a Clojure spec uses a conformer to transform a value (e.g., treating a string as a sequence or a collection of characters), the NCV might not exist as an atomic value in the original context.

    For example, if a spec uses (s/conformer seq) to validate a string against a regex, the NCV might be a single character (like \C) that is not an atomic element of the original string context.

    If you encounter issues where Expound cannot correctly locate or print the non-conforming value due to transformations, you may need to provide a custom printer.

  8. Understand the limitations of Expound error message manipulation

    master

    Currently, Expound primarily returns error messages as plain strings via expound-str. Because the error messages are strings, users attempting to programmatically manipulate them (e.g., to shrink the context, hide irrelevant data, or inject custom information) must rely on brittle and error-prone string parsing.

    Expound error messages work by displaying a "problem" value within its surrounding "context" value. For example, if a value in a map is invalid, Expound prints the entire map (the context) and uses carets (^^^^^) to point to the specific invalid value (the problem).

    Common pain points include:

    • Large Contexts: Very large or deeply nested maps/sequences make error messages difficult to read.
    • Long Sequences: Long sequences often use ... to truncate, which can be improved with more descriptive summaries (e.g., < 7 more >).
    • Irrelevant Data: In complex structures, much of the printed context might not be relevant to why the predicate failed.
    • Lack of Customization: There is currently no standard way to inject additional metadata or prevent specific large records from being printed in their entirety.
  9. How Expound represents problem and context values

    master

    Expound's mental model for errors relies on two components:

    1. Problem Value: The specific piece of data that failed a predicate (e.g., "456").
    2. Context Value: The larger data structure containing the problem value (e.g., {:ids [123 "456" 789]}).

    When an error is generated, Expound prints the context and uses visual markers to highlight the problem.

    Example Output:

      {:ids [... "456" ...]}
                 ^^^^^
    
    should satisfy
    
      int?
  10. Locate errors in nested data structures

    master

    When a value fails a spec inside a nested structure, Expound provides a visual pointer (using ^^^^^) to the exact location of the invalid data within the input map or vector, making it easier to debug than standard clojure.spec messages.

    (s/def :db/id clojure.core/pos-int?)
    (s/def :db/ids (s/coll-of :db/id))
    (s/def :app/request (s/keys :req-un [:db/ids]))
    
    ;; Input: {:ids [123 "456" 789]}
    ;; Expound output highlights the invalid element:
    ;; {:ids [... "456" ...]}
    ;;             ^^^^^
  11. Understand grouped alternatives in `s/or`

    master

    When using s/or to define multiple valid paths, Expound groups the alternatives in the error message, showing the logical 'or' relationship between the possible satisfying specs.

    (s/def :address.west-coast/zip (s/or :str clojure.core/string? :num clojure.core/pos-int?))
    
    ;; Input: :98109
    ;; Expound output:
    ;; should satisfy
    ;;   string?
    ;; or
    ;;   pos-int?
  12. Configure Expound for macro expansion in ClojureScript

    master

    Due to how macros are expanded in ClojureScript, you must configure Expound in Clojure to receive Expound errors during macro-expansion. This requirement does not apply to self-hosted ClojureScript.

    To enable this, you must set clojure.spec.alpha/*explain-out* to expound.alpha/printer using the -e argument when running cljs.main.

    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