From 6b37215e622b223ca27c17890f621dbd6ab713c8 Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 20:06:41 +0200 Subject: [PATCH 1/9] Ignore plans-folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 07ed09e..7651b45 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,4 @@ /tmp/* !/tmp/.keep +/plans From 004c1949d24185c4d782ef73037ff7e3024a3b01 Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 21:23:16 +0200 Subject: [PATCH 2/9] Selection error via ex-info not assert --- CHANGELOG.md | 20 ++++++++++++++++++++ README.md | 12 +++++++----- src/malli_select/core.clj | 20 +++++++++++--------- test/malli_select/core_test.clj | 10 ++++++++-- 4 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..cabb773 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## Unreleased + +### Breaking + +- An invalid selection now throws an `ExceptionInfo` instead of an `AssertionError`. + This makes it catchable via `(catch ExceptionInfo e ...)` and works in ClojureScript + (where asserts may be elided in release builds). The unknown and available paths are + available as data: + ```clojure + (ex-data e) + ;; => {:type :malli-select.core/unknown-paths + ;; :data {:paths (...), :available (...)}} + ``` + If you were catching `AssertionError` or matching the assert message, update your code. + The preferred spelling of the `:verify-selection` option is now `:throw` (the default); + `:assert` still works, as do the `:skip`/`nil`/`false` opt-outs. + +[Unreleased]: https://github.com/eval/malli-select/compare/v0.7.0...HEAD diff --git a/README.md b/README.md index 8a6eec5..2879ffe 100644 --- a/README.md +++ b/README.md @@ -75,13 +75,15 @@ user=> (mg/generate (ms/select Person ^:only [:name])) ;; selecting something not contained in the schema: user=> (ms/select Person [:a]) -Execution error (AssertionError) at dk.thinkcreate.malli-select/select (malli_select.clj:175). -Assert failed: Selection contains unknown paths: ([:a]) +Execution error (ExceptionInfo) at malli-select.core/-fail! (core.clj:7). +:malli-select.core/unknown-paths {:paths ([:a]), :available ([:addresses :street] [:addresses :zip] [:addresses] [:age] [:name])} -Available: -([:addresses] [:age] [:name] [:addresses :street] [:addresses :zip]) +;; the unknown and available paths are also in the ex-data: +user=> (ex-data *e) +{:type :malli-select.core/unknown-paths, + :data {:paths ([:a]), + :available ([:addresses :street] [:addresses :zip] [:addresses] [:age] [:name])}} -(empty? invalid-selection-paths) ;; bypass this check: user=> (ms/select Person [:a] {:verify-selection false}) ``` diff --git a/src/malli_select/core.clj b/src/malli_select/core.clj index 59f69c2..8aaf8b0 100644 --- a/src/malli_select/core.clj +++ b/src/malli_select/core.clj @@ -1,9 +1,11 @@ (ns malli-select.core "Select a subset of a malli schema." - (:require [clojure.pprint :refer [pprint]] - [malli.core :as m] + (:require [malli.core :as m] [malli.util :as mu])) +(defn- -fail! [type data] + (throw (ex-info (str type " " (pr-str data)) {:type type :data data}))) + (defn- clean-path [path] (loop [p path r (transient [])] @@ -86,7 +88,7 @@ ::keys [optionalized] :keys [verify-selection prune-optionals] :or - {verify-selection :assert}}] + {verify-selection :throw}}] (letfn [(in? [coll elm] (some #(= % elm) coll))] (let [all-optional? (empty? selection) @@ -147,9 +149,9 @@ {::m/walk-schema-refs true ::m/walk-refs true})] (when verify-selection? (let [invalid-selection-paths (remove @!seen selection-paths)] - (assert (empty? invalid-selection-paths) - (str "Selection contains unknown paths: " (prn-str invalid-selection-paths) - "\nAvailable: \n" (with-out-str (pprint (sort (selectable-paths schema)))))))) + (when (seq invalid-selection-paths) + (-fail! ::unknown-paths {:paths invalid-selection-paths + :available (sort-by pr-str (selectable-paths schema))})))) walked))) @@ -167,7 +169,7 @@ - `[{:friends [:name]} {:friends [:age]}]` - only require `:age` of friends if `:friends` provided (last selection wins). `options`: - - `verify-selection` (`:assert` (default), `:skip`, `false`, `nil`) - what to do when `selection` contains paths not in `schema`. + - `verify-selection` (`:throw` (default, `:assert` works as well), `:skip`, `false`, `nil`) - what to do when `selection` contains paths not in `schema`. Throws an `ExceptionInfo` with `{:type ::unknown-paths :data {:paths ... :available ...}}` as `ex-data`. - `prune-optionals` (`false` (default), `true`) - whether all fully optional subtrees should be removed from the resulting schema. Alternatively via metadata of selection: `^:only [:name]` (flag takes precedence over metadata). Typically used when the selected schema is used for data generation. @@ -179,7 +181,7 @@ (select Person [:name :handle]) ;; Require specific root attributes. (select Person [{:address ['*]}]) ;; Require the full address if provided. - (select Person [:foo]) ;; Assert exception about non existing path, showing all possible paths. + (select Person [:foo]) ;; Throws ExceptionInfo about non existing path, ex-data contains all possible paths. ``` " ([schema] @@ -188,7 +190,7 @@ (select schema selection nil)) ([schema selection {:as options :keys [verify-selection prune-optionals] - :or {verify-selection :assert}}] + :or {verify-selection :throw}}] (-select schema selection (assoc options :verify-selection verify-selection :prune-optionals prune-optionals)))) diff --git a/test/malli_select/core_test.clj b/test/malli_select/core_test.clj index b9dc36b..99d4e25 100644 --- a/test/malli_select/core_test.clj +++ b/test/malli_select/core_test.clj @@ -192,8 +192,14 @@ {:that {:other "?"}} ":other can be a string as it should no longer be part of the schema")))) (testing "verify-selection" - (is (thrown-with-msg? AssertionError #"unknown paths: \(\[:a\]\)" - (select int? [:a]))) + (is (thrown? clojure.lang.ExceptionInfo + (select int? [:a]))) + (let [ex-data (try + (select [:map [:name string?]] [:a]) + (catch clojure.lang.ExceptionInfo e (ex-data e)))] + (is (= ::sut/unknown-paths (:type ex-data))) + (is (= '([:a]) (:paths (:data ex-data)))) + (is (= '([:name]) (:available (:data ex-data))))) (testing "disabling it" (is (some? (select int? [:a] {:verify-selection :skip}))) (is (some? (select int? [:a] {:verify-selection nil}))))))) From 083c88fb5094a94106537fd14f99a1e4c22389eb Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 21:30:23 +0200 Subject: [PATCH 3/9] Support cljs --- README.md | 4 ++-- src/malli_select/{core.clj => core.cljc} | 0 test/malli_select/{core_test.clj => core_test.cljc} | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/malli_select/{core.clj => core.cljc} (100%) rename test/malli_select/{core_test.clj => core_test.cljc} (97%) diff --git a/README.md b/README.md index 2879ffe..cbbe908 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ user=> (mg/generate (ms/select Person ^:only [:name])) ;; selecting something not contained in the schema: user=> (ms/select Person [:a]) -Execution error (ExceptionInfo) at malli-select.core/-fail! (core.clj:7). +Execution error (ExceptionInfo) at malli-select.core/-fail! (core.cljc:7). :malli-select.core/unknown-paths {:paths ([:a]), :available ([:addresses :street] [:addresses :zip] [:addresses] [:age] [:name])} ;; the unknown and available paths are also in the ex-data: @@ -88,7 +88,7 @@ user=> (ex-data *e) user=> (ms/select Person [:a] {:verify-selection false}) ``` -See [the tests](./test/malli_select/core_test.clj) for more. +See [the tests](./test/malli_select/core_test.cljc) for more. ## LICENSE diff --git a/src/malli_select/core.clj b/src/malli_select/core.cljc similarity index 100% rename from src/malli_select/core.clj rename to src/malli_select/core.cljc diff --git a/test/malli_select/core_test.clj b/test/malli_select/core_test.cljc similarity index 97% rename from test/malli_select/core_test.clj rename to test/malli_select/core_test.cljc index 99d4e25..0b9338a 100644 --- a/test/malli_select/core_test.clj +++ b/test/malli_select/core_test.cljc @@ -1,10 +1,10 @@ (ns malli-select.core-test + #?(:cljs (:require-macros [malli-select.core-test])) (:require [clojure.pprint :refer [pprint]] [clojure.test :as t :refer [deftest is testing]] [malli-select.core :as sut :refer [select selector]] - [malli.core :as m] - [malli.util :as mu])) + [malli.core :as m])) (defonce ^:private ^:dynamic *schema* nil) @@ -192,11 +192,11 @@ {:that {:other "?"}} ":other can be a string as it should no longer be part of the schema")))) (testing "verify-selection" - (is (thrown? clojure.lang.ExceptionInfo + (is (thrown? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core/ExceptionInfo) (select int? [:a]))) (let [ex-data (try (select [:map [:name string?]] [:a]) - (catch clojure.lang.ExceptionInfo e (ex-data e)))] + (catch #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core/ExceptionInfo) e (ex-data e)))] (is (= ::sut/unknown-paths (:type ex-data))) (is (= '([:a]) (:paths (:data ex-data)))) (is (= '([:name]) (:available (:data ex-data))))) From 1cfe3e7b965508c12f71a21658c409b437c0a646 Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 21:42:32 +0200 Subject: [PATCH 4/9] Have test-cljs --- .gitignore | 1 + build.clj | 40 +++++++++++++++++++++-------- deps.edn | 6 +++++ test/malli_select/core_test.cljc | 44 +++++++++++++++++--------------- 4 files changed, 61 insertions(+), 30 deletions(-) diff --git a/.gitignore b/.gitignore index 7651b45..b36e9bb 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ /tmp/* !/tmp/.keep /plans +/cljs-test-runner-out diff --git a/build.clj b/build.clj index 9587f67..c7bef4d 100644 --- a/build.clj +++ b/build.clj @@ -31,6 +31,17 @@ (update-keys (filter (comp #(= (name ns) %) namespace key) m) (comp keyword name))) +(defn- runner-flags + "E.g. `(runner-flags \"test\" {:test/H true}) ;;=> {\"-H\" \"true\"}`" + [ns opts] + (-> (extract-keys-with-ns ns opts) + (update-keys (fn [k] + ;; :H => "-H", :help => "--help" + (let [k (name k)] + (cond->> (str "-" k) + (> (count k) 1) (str "-"))))) + (update-vals str))) + (defn ^#:fika{:examples [":test/d '\"some-dir\"'" ":test/n '\"some.namespace-test\"'" "# see all runner options\n:test/H true"]} @@ -39,14 +50,7 @@ Passing options to test-runner possible, see examples." [opts] #_(prn :opts opts) - (let [test-options (extract-keys-with-ns "test" opts) - test-options (-> test-options - (update-keys (fn [k] - ;; :H => "-H", :help => "--help" - (let [k (name k)] - (cond->> (str "-" k) - (> (count k) 1) (str "-"))))) - (update-vals str)) + (let [test-options (runner-flags "test" opts) basis (b/create-basis {:aliases [:test]}) cmds (doto (b/java-command {:basis basis @@ -58,6 +62,22 @@ (when-not (zero? exit) (throw (ex-info "Tests failed" {})))) opts) +(defn ^#:fika{:examples [":test-cljs/n '\"some.namespace-test\"'" + "# see all runner options\n:test-cljs/H true"]} + test-cljs + "Run all the tests on ClojureScript (Node). + + Passing options to cljs-test-runner possible, see examples." [opts] + (let [test-options (runner-flags "test-cljs" opts) + basis (b/create-basis {:aliases [:cljs-test]}) + cmds (b/java-command + {:basis basis + :main 'clojure.main + :main-args (reduce into ["-m" "cljs-test-runner.main"] test-options)}) + {:keys [exit]} (b/process cmds)] + (when-not (zero? exit) (throw (ex-info "CLJS tests failed" {})))) + opts) + (b/java-command {:basis (b/create-basis {:aliases [:test]}) :main 'clojure.main}) (defn- pom-template [version version-type] [[:description "spec2-inspired selection of Malli schemas"] @@ -171,14 +191,14 @@ ":build/git-version $(printf '\"%s\"' $(git describe --tags))" ":build/git-version '\"v1.2.3\"' :deploy/only-jar-version-type :full-and-snapshot"] - :options.from-commands '[test build deploy]} + :options.from-commands '[test test-cljs build deploy]} release "Test, build and deploy. Deploys *only* when name of the jar built has the right format, see option `deploy/only-jar-version-type`." [opts] #_(prn :release-opts opts) - (deploy (build (test opts)))) + (deploy (build (test-cljs (test opts))))) (comment diff --git a/deps.edn b/deps.edn index 35b85eb..3653294 100644 --- a/deps.edn +++ b/deps.edn @@ -31,6 +31,12 @@ {:extra-paths ["test"] :extra-deps {io.github.cognitect-labs/test-runner {:git/tag "v0.5.1" :git/sha "dfb30dd"}} :exec-fn cognitect.test-runner.api/test} + + :cljs-test ;; run tests on Node: clojure -M:cljs-test + {:extra-paths ["test"] + :extra-deps {org.clojure/clojurescript {:mvn/version "1.12.42"} + olical/cljs-test-runner {:mvn/version "3.8.1"}} + :main-opts ["-m" "cljs-test-runner.main"]} :perf {#_#_:extra-paths ["perf"] :extra-deps {criterium/criterium {:mvn/version "0.4.6"} org.clojure/clojure {:mvn/version "1.12.0"} diff --git a/test/malli_select/core_test.cljc b/test/malli_select/core_test.cljc index 0b9338a..5ec2054 100644 --- a/test/malli_select/core_test.cljc +++ b/test/malli_select/core_test.cljc @@ -1,5 +1,7 @@ (ns malli-select.core-test - #?(:cljs (:require-macros [malli-select.core-test])) + #?(:cljs (:require-macros [malli-select.core-test + :refer [expect-selection-to-validate + expect-selection-to-invalidate]])) (:require [clojure.pprint :refer [pprint]] [clojure.test :as t :refer [deftest is testing]] @@ -15,25 +17,27 @@ (defn pps [o] (with-out-str (pprint o))) -(defmacro expect-selection-to-validate [sel & data+maybe-reason] - `(if ~sel - (let [data# ~(first data+maybe-reason) - sel-schema# (if *selector* (*selector* ~sel) (select *schema* ~sel (meta ~sel))) - result# (or (m/validate sel-schema# data#) (m/explain sel-schema# data#))] - (is (true? result#) - (cond-> (str "Expected data:\n" (pps data#) "to be valid given schema:\n" (pps (m/form sel-schema#))) - ~(second data+maybe-reason) (str "because:\n" ~(second data+maybe-reason)) - :always (str "\nvalidate errors:\n" (pps (:errors result#)))))) - *schema*)) - -(defmacro expect-selection-to-invalidate [sel & data+maybe-reason] - `(if ~sel - (let [data# ~(first data+maybe-reason) - sel-schema# (if *selector* (*selector* ~sel) (select *schema* ~sel (meta ~sel)))] - (is (false? (m/validate sel-schema# data#)) - (cond-> (str "Expected data:\n" (pps data#) "to be *invalid* given schema:\n" (pps (m/form sel-schema#))) - ~(second data+maybe-reason) (str "because:\n" ~(second data+maybe-reason))))) - *schema*)) +#?(:clj + (defmacro expect-selection-to-validate [sel & data+maybe-reason] + `(if ~sel + (let [data# ~(first data+maybe-reason) + sel-schema# (if *selector* (*selector* ~sel) (select *schema* ~sel (meta ~sel))) + result# (or (m/validate sel-schema# data#) (m/explain sel-schema# data#))] + (is (true? result#) + (cond-> (str "Expected data:\n" (pps data#) "to be valid given schema:\n" (pps (m/form sel-schema#))) + ~(second data+maybe-reason) (str "because:\n" ~(second data+maybe-reason)) + :always (str "\nvalidate errors:\n" (pps (:errors result#)))))) + *schema*))) + +#?(:clj + (defmacro expect-selection-to-invalidate [sel & data+maybe-reason] + `(if ~sel + (let [data# ~(first data+maybe-reason) + sel-schema# (if *selector* (*selector* ~sel) (select *schema* ~sel (meta ~sel)))] + (is (false? (m/validate sel-schema# data#)) + (cond-> (str "Expected data:\n" (pps data#) "to be *invalid* given schema:\n" (pps (m/form sel-schema#))) + ~(second data+maybe-reason) (str "because:\n" ~(second data+maybe-reason))))) + *schema*))) (deftest select-test From 29571a7c98b9f5adeb8c7250b90b4372efbebcff Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 21:47:55 +0200 Subject: [PATCH 5/9] Have CI run cljs This requires newer java as Closure compiler is compiled for java v21 --- .github/workflows/ci.yml | 17 +++++++++++++++++ .github/workflows/release.yml | 7 +++++++ CHANGELOG.md | 5 +++++ README.md | 8 +++++++- RELEASING.md | 7 ++++--- build.clj | 2 +- 6 files changed, 41 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9fe8ad2..c1e0cb1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,3 +39,20 @@ jobs: clojure -X:test:malli-${{ matrix.malli }} ;; esac + Test-cljs: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Install java + # ClojureScript's Closure compiler requires Java 21+ + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: 21 + - name: Install clojure tools + uses: DeLaGuardo/setup-clojure@4c7a6f613e5089821bb3bb2a33a3ee115578580d # 13.6.1 + with: + cli: latest + - name: Run tests on ClojureScript (Node) + run: clojure -M:cljs-test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49be626..7bc2e29 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,13 @@ jobs: with: fetch-depth: 0 + - name: Install java + # ClojureScript's Closure compiler (used by test-cljs) requires Java 21+ + uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: 21 + - name: Install clojure tools uses: DeLaGuardo/setup-clojure@4c7a6f613e5089821bb3bb2a33a3ee115578580d # 13.6.1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index cabb773..28e6467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Added + +- ClojureScript support: the library is now `.cljc` and tested on Node + (`clojure -M:cljs-test`) as well as the JVM. + ### Breaking - An invalid selection now throws an `ExceptionInfo` instead of an `AssertionError`. diff --git a/README.md b/README.md index cbbe908..a403703 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Clojars Project](https://img.shields.io/clojars/v/dk.thinkcreate/malli-select.svg?include_prereleases)](https://clojars.org/dk.thinkcreate/malli-select) [![cljdoc badge](https://cljdoc.org/badge/dk.thinkcreate/malli-select)](https://cljdoc.org/d/dk.thinkcreate/malli-select) [![Tests](https://github.com/eval/malli-select/actions/workflows/ci.yml/badge.svg)](https://github.com/eval/malli-select/actions/workflows/ci.yml) -Create subschemas of [malli](https://github.com/metosin/malli)-schemas using a spec2-inspired select notation. +Create subschemas of [malli](https://github.com/metosin/malli)-schemas using a spec2-inspired select notation. Works on Clojure and ClojureScript. It's based on Rich Hickey's ideas from his talk ["Maybe Not"](https://youtu.be/YR5WdGrpoug?feature=shared&t=1965) about how [spec-alpha2](https://github.com/clojure/spec-alpha2) might allow for schema reuse. @@ -86,8 +86,14 @@ user=> (ex-data *e) ;; bypass this check: user=> (ms/select Person [:a] {:verify-selection false}) +;; :verify-selection defaults to :throw (`:assert`, the pre-v0.8 spelling, still works); +;; :skip, nil and false disable the check. ``` +> [!NOTE] +> Before v0.8 an invalid selection threw an `AssertionError` instead of an +> `ExceptionInfo` — see the [CHANGELOG](CHANGELOG.md) if you were catching it. + See [the tests](./test/malli_select/core_test.cljc) for more. diff --git a/RELEASING.md b/RELEASING.md index 93295db..4b97ada 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -33,7 +33,7 @@ The workflow runs on every push to `main` and on every tag: clojure -T:build release :build/git-version $(printf '"%s"' $(git describe --tags)) :deploy/only-jar-version-type :full-and-snapshot ``` -`release` (see [build.clj](build.clj)) chains `test` → `build` → `deploy`. +`release` (see [build.clj](build.clj)) chains `test` → `test-cljs` → `build` → `deploy`. The output of `git describe --tags` determines the version and whether the built jar is actually deployed: @@ -83,5 +83,6 @@ CLOJARS_USERNAME=... CLOJARS_PASSWORD= \ - `git describe --tags` needs the full history: the checkout step uses `fetch-depth: 0` for this — keep it when touching the workflow. -- Tests run against the `:test` alias; a test failure aborts the release - before anything is built or deployed. +- Tests run on both the JVM (`:test` alias) and ClojureScript/Node + (`:cljs-test` alias); a failure in either suite aborts the release before + anything is built or deployed. diff --git a/build.clj b/build.clj index c7bef4d..ac7ed9a 100644 --- a/build.clj +++ b/build.clj @@ -80,7 +80,7 @@ (b/java-command {:basis (b/create-basis {:aliases [:test]}) :main 'clojure.main}) (defn- pom-template [version version-type] - [[:description "spec2-inspired selection of Malli schemas"] + [[:description "Create subschemas of malli-schemas using a spec2-inspired select notation. Works on Clojure and ClojureScript."] [:url "https://github.com/eval/malli-select"] [:licenses [:license From c5a1fe8ba309f47dde8277afcd7b22499cd6689b Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 21:59:52 +0200 Subject: [PATCH 6/9] Add log option for :verify-selection --- CHANGELOG.md | 4 ++++ README.md | 6 ++++++ src/malli_select/core.cljc | 19 ++++++++++++++++--- test/malli_select/core_test.cljc | 11 +++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28e6467..7d5c8ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - ClojureScript support: the library is now `.cljc` and tested on Node (`clojure -M:cljs-test`) as well as the JVM. +- `:verify-selection` accepts two new values besides `:throw`/`:assert` and + the `:skip`/`nil`/`false` opt-outs: `:log` (print a warning — stderr on + Clojure, `console.warn` on ClojureScript — and continue) and a function + (called with `{:paths ... :available ...}`, selection continues). ### Breaking diff --git a/README.md b/README.md index a403703..f7fb88b 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,12 @@ user=> (ex-data *e) user=> (ms/select Person [:a] {:verify-selection false}) ;; :verify-selection defaults to :throw (`:assert`, the pre-v0.8 spelling, still works); ;; :skip, nil and false disable the check. + +;; other options: :log warns (stderr/console.warn) and continues... +user=> (ms/select Person [:a] {:verify-selection :log}) +WARNING: :malli-select.core/unknown-paths {:paths ([:a]), :available (...)} +;; ...and a function gets the report, e.g.: +user=> (ms/select Person [:a] {:verify-selection #(log/warn "unknown paths" (:paths %))}) ``` > [!NOTE] diff --git a/src/malli_select/core.cljc b/src/malli_select/core.cljc index 8aaf8b0..93f330c 100644 --- a/src/malli_select/core.cljc +++ b/src/malli_select/core.cljc @@ -6,6 +6,11 @@ (defn- -fail! [type data] (throw (ex-info (str type " " (pr-str data)) {:type type :data data}))) +(defn- -warn! [type data] + (let [msg (str type " " (pr-str data))] + #?(:clj (binding [*out* *err*] (println "WARNING:" msg)) + :cljs (js/console.warn msg)))) + (defn- clean-path [path] (loop [p path r (transient [])] @@ -150,8 +155,12 @@ (when verify-selection? (let [invalid-selection-paths (remove @!seen selection-paths)] (when (seq invalid-selection-paths) - (-fail! ::unknown-paths {:paths invalid-selection-paths - :available (sort-by pr-str (selectable-paths schema))})))) + (let [report {:paths invalid-selection-paths + :available (sort-by pr-str (selectable-paths schema))}] + (cond + (fn? verify-selection) (verify-selection report) + (= :log verify-selection) (-warn! ::unknown-paths report) + :else (-fail! ::unknown-paths report)))))) walked))) @@ -169,7 +178,11 @@ - `[{:friends [:name]} {:friends [:age]}]` - only require `:age` of friends if `:friends` provided (last selection wins). `options`: - - `verify-selection` (`:throw` (default, `:assert` works as well), `:skip`, `false`, `nil`) - what to do when `selection` contains paths not in `schema`. Throws an `ExceptionInfo` with `{:type ::unknown-paths :data {:paths ... :available ...}}` as `ex-data`. + - `verify-selection` - what to do when `selection` contains paths not in `schema`: + - `:throw` (default, `:assert` works as well) - throw an `ExceptionInfo` with `{:type ::unknown-paths :data {:paths ... :available ...}}` as `ex-data`. + - `:log` - print a warning (stderr on Clojure, `console.warn` on ClojureScript) and continue. + - a function - called with `{:paths ... :available ...}`, result ignored, selection continues. + - `:skip`, `false`, `nil` - don't verify. - `prune-optionals` (`false` (default), `true`) - whether all fully optional subtrees should be removed from the resulting schema. Alternatively via metadata of selection: `^:only [:name]` (flag takes precedence over metadata). Typically used when the selected schema is used for data generation. diff --git a/test/malli_select/core_test.cljc b/test/malli_select/core_test.cljc index 5ec2054..cca4927 100644 --- a/test/malli_select/core_test.cljc +++ b/test/malli_select/core_test.cljc @@ -204,6 +204,17 @@ (is (= ::sut/unknown-paths (:type ex-data))) (is (= '([:a]) (:paths (:data ex-data)))) (is (= '([:name]) (:available (:data ex-data))))) + (testing ":log logs instead of throwing" + (is (some? #?(:clj (binding [*err* (java.io.StringWriter.)] + (select int? [:a] {:verify-selection :log})) + :cljs (select int? [:a] {:verify-selection :log}))))) + (testing "a function receives the report" + (let [!report (atom nil) + result (select [:map [:name string?]] [:a] + {:verify-selection (partial reset! !report)})] + (is (some? result)) + (is (= '([:a]) (:paths @!report))) + (is (= '([:name]) (:available @!report))))) (testing "disabling it" (is (some? (select int? [:a] {:verify-selection :skip}))) (is (some? (select int? [:a] {:verify-selection nil}))))))) From 1f51e0143fd9f9a52374d1252bcc1eba877f27b0 Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 22:07:17 +0200 Subject: [PATCH 7/9] Allow for default verify-selection --- CHANGELOG.md | 3 +++ README.md | 3 +++ src/malli_select/core.cljc | 16 ++++++++++++---- test/malli_select/core_test.cljc | 12 +++++++++++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d5c8ac..e76c11a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ the `:skip`/`nil`/`false` opt-outs: `:log` (print a warning — stderr on Clojure, `console.warn` on ClojureScript — and continue) and a function (called with `{:paths ... :available ...}`, selection continues). +- The dynamic var `*verify-selection*` (initially `:throw`) provides the + default for the `:verify-selection` option; an explicitly passed option + still wins. ### Breaking diff --git a/README.md b/README.md index f7fb88b..2af6b57 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,9 @@ user=> (ms/select Person [:a] {:verify-selection :log}) WARNING: :malli-select.core/unknown-paths {:paths ([:a]), :available (...)} ;; ...and a function gets the report, e.g.: user=> (ms/select Person [:a] {:verify-selection #(log/warn "unknown paths" (:paths %))}) + +;; change the default via the dynamic var ms/*verify-selection*: +user=> (alter-var-root #'ms/*verify-selection* (constantly :log)) ;; CLJS: (set! ms/*verify-selection* :log) ``` > [!NOTE] diff --git a/src/malli_select/core.cljc b/src/malli_select/core.cljc index 93f330c..2049b03 100644 --- a/src/malli_select/core.cljc +++ b/src/malli_select/core.cljc @@ -3,6 +3,14 @@ (:require [malli.core :as m] [malli.util :as mu])) +(def ^:dynamic *verify-selection* + "Default for the `:verify-selection` option of `select`/`selector`. + See `select` for accepted values. + + Rebind with `binding`, or set app-wide via `alter-var-root` + (Clojure) / `set!` (ClojureScript)." + :throw) + (defn- -fail! [type data] (throw (ex-info (str type " " (pr-str data)) {:type type :data data}))) @@ -93,7 +101,7 @@ ::keys [optionalized] :keys [verify-selection prune-optionals] :or - {verify-selection :throw}}] + {verify-selection *verify-selection*}}] (letfn [(in? [coll elm] (some #(= % elm) coll))] (let [all-optional? (empty? selection) @@ -178,8 +186,8 @@ - `[{:friends [:name]} {:friends [:age]}]` - only require `:age` of friends if `:friends` provided (last selection wins). `options`: - - `verify-selection` - what to do when `selection` contains paths not in `schema`: - - `:throw` (default, `:assert` works as well) - throw an `ExceptionInfo` with `{:type ::unknown-paths :data {:paths ... :available ...}}` as `ex-data`. + - `verify-selection` - what to do when `selection` contains paths not in `schema`. Defaults to `*verify-selection*` (initially `:throw`): + - `:throw` (`:assert` works as well) - throw an `ExceptionInfo` with `{:type ::unknown-paths :data {:paths ... :available ...}}` as `ex-data`. - `:log` - print a warning (stderr on Clojure, `console.warn` on ClojureScript) and continue. - a function - called with `{:paths ... :available ...}`, result ignored, selection continues. - `:skip`, `false`, `nil` - don't verify. @@ -203,7 +211,7 @@ (select schema selection nil)) ([schema selection {:as options :keys [verify-selection prune-optionals] - :or {verify-selection :throw}}] + :or {verify-selection *verify-selection*}}] (-select schema selection (assoc options :verify-selection verify-selection :prune-optionals prune-optionals)))) diff --git a/test/malli_select/core_test.cljc b/test/malli_select/core_test.cljc index cca4927..cd8d52e 100644 --- a/test/malli_select/core_test.cljc +++ b/test/malli_select/core_test.cljc @@ -217,7 +217,17 @@ (is (= '([:name]) (:available @!report))))) (testing "disabling it" (is (some? (select int? [:a] {:verify-selection :skip}))) - (is (some? (select int? [:a] {:verify-selection nil}))))))) + (is (some? (select int? [:a] {:verify-selection nil})))) + (testing "*verify-selection* provides the default" + (let [!report (atom nil)] + (binding [sut/*verify-selection* (partial reset! !report)] + (is (some? (select [:map [:name string?]] [:a]))) + (is (= '([:a]) (:paths @!report))) + ;; explicitly passed option wins + (reset! !report nil) + (is (thrown? #?(:clj clojure.lang.ExceptionInfo :cljs cljs.core/ExceptionInfo) + (select int? [:a] {:verify-selection :throw}))) + (is (nil? @!report)))))))) (deftest selector-test (binding [*selector* (selector [:map From 33d89c67dd95c15524d392f71bd9893b995a692a Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 22:42:23 +0200 Subject: [PATCH 8/9] Prereleases are no longer -SNAPSHOT --- .github/workflows/release.yml | 2 +- RELEASING.md | 23 ++++++++++++----------- build.clj | 27 +++++++++++---------------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bc2e29..a526b78 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,4 +33,4 @@ jobs: env: CLOJARS_USERNAME: ${{ secrets.CLOJARS_USERNAME }} CLOJARS_PASSWORD: ${{ secrets.CLOJARS_PASSWORD }} - run: clojure -T:build release :build/git-version "$(printf '"%s"' "$(git describe --tags)")" :deploy/only-jar-version-type :full-and-snapshot + run: clojure -T:build release :build/git-version "$(printf '"%s"' "$(git describe --tags)")" :deploy/only-jar-version-type :full-and-pre diff --git a/RELEASING.md b/RELEASING.md index 4b97ada..6edaccf 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -9,7 +9,7 @@ deploys `dk.thinkcreate/malli-select` to [Clojars](https://clojars.org/dk.thinkc Local tags must be *signed* (`-s`): ```sh -# snapshot release (publishes X.Y.Z-SNAPSHOT) +# prerelease (publishes X.Y.Z-pre.1) git tag -s vX.Y.Z-pre.1 -m "vX.Y.Z-pre.1" git push origin vX.Y.Z-pre.1 @@ -30,7 +30,7 @@ signed but are attributed to your GitHub account. The workflow runs on every push to `main` and on every tag: ```sh -clojure -T:build release :build/git-version $(printf '"%s"' $(git describe --tags)) :deploy/only-jar-version-type :full-and-snapshot +clojure -T:build release :build/git-version $(printf '"%s"' $(git describe --tags)) :deploy/only-jar-version-type :full-and-pre ``` `release` (see [build.clj](build.clj)) chains `test` → `test-cljs` → `build` → `deploy`. @@ -40,19 +40,20 @@ built jar is actually deployed: | `git describe --tags` | jar version | deployed? | |-----------------------------------|-------------------|-------------------------------| | `v1.2.3` (exact release tag) | `1.2.3` | yes — full release | -| `v1.2.3-pre.1` (pre-tag, or any commit after one) | `1.2.3-SNAPSHOT` | yes — snapshot | -| `v1.2.3-5-gabc123` (commits after a release tag) | `1.2.3-5-gabc123` | no — build only | +| `v1.2.3-pre.1` (exact pre-tag) | `1.2.3-pre.1` | yes — prerelease | +| any commits after a tag (e.g. `v1.2.3-5-gabc123`) | verbatim minus the `v` | no — build only | Consequences: -- A full release requires an *exact* `vX.Y.Z` tag on the commit. -- After pushing a `vX.Y.Z-pre.N` tag, every subsequent push to `main` - re-publishes `X.Y.Z-SNAPSHOT` — until the next exact release tag. -- Ordinary pushes to `main` after a release tag act as a dry run: +- A (pre)release requires an *exact* tag on the commit. +- Prereleases are ordinary immutable releases — to publish another, tag + `vX.Y.Z-pre.N+1`. (They're exact versions so consumers — e.g. a + ClojureScript project dogfooding an upcoming release — can pin them.) +- Ordinary pushes to `main` act as a dry run: tests run and the jar is built, but nothing is deployed. - Pick pre-tag versions to match the *next* intended release, e.g. after releasing `v0.7.0` the next pre-tag should be `v0.8.0-pre.1`. -- For a full release the POM's `` is set to `vX.Y.Z`. +- For both release types the POM's `` is set to the tag. ## Credentials @@ -67,7 +68,7 @@ Consequences: To rotate: create a new token on Clojars, then `gh secret set CLOJARS_PASSWORD --repo eval/malli-select`, and delete the old token. The cheapest end-to-end check of the credentials is publishing a -snapshot via a `-pre` tag (see TL;DR); the token's "last used" date on the +prerelease via a `-pre` tag (see TL;DR); the token's "last used" date on the Clojars tokens page should update. ## Local release @@ -76,7 +77,7 @@ The same can be done locally (e.g. when CI is down): ```sh CLOJARS_USERNAME=... CLOJARS_PASSWORD= \ - clojure -T:build release :build/git-version $(printf '"%s"' $(git describe --tags)) :deploy/only-jar-version-type :full-and-snapshot + clojure -T:build release :build/git-version $(printf '"%s"' $(git describe --tags)) :deploy/only-jar-version-type :full-and-pre ``` ## Gotchas diff --git a/build.clj b/build.clj index ac7ed9a..d633c66 100644 --- a/build.clj +++ b/build.clj @@ -93,7 +93,7 @@ [:url "https://github.com/eval/malli-select"] [:connection "scm:git:https://github.com/eval/malli-select.git"] [:developerConnection "scm:git:ssh:git@github.com:eval/malli-select.git"]] - (= :exact version-type) (conj [:tag (str "v" version)]))]) + (#{:exact :pre} version-type) (conj [:tag (str "v" version)]))]) (defn- jar-opts [{:keys [version version-type] :as opts}] @@ -111,16 +111,11 @@ e.g. `v1.2.3`, `v1.2.3-pre.1` or `v1.2.3-1-g`. Yields map with `version` and `type`." [git-version] - (let [type (condp re-find git-version - #"^v\d+\.\d+\.\d+$" :exact - #"^v\d+\.\d+\.\d+-pre\.\d+" :pre ;; pre-tag and any commit after - :build) - exact-version (second (re-find #"v(\d+\.\d+\.\d+)" git-version)) - version (case type - :exact exact-version - :pre (str exact-version "-SNAPSHOT") - :build (subs git-version 1))] - {:version version :version-type type})) + (let [type (condp re-find git-version + #"^v\d+\.\d+\.\d+$" :exact + #"^v\d+\.\d+\.\d+-pre\.\d+$" :pre + :build)] + {:version (subs git-version 1) :version-type type})) (comment (git-version->version&type "v1.2.3-123") @@ -161,20 +156,20 @@ (defn ^#:fika{:option.only-jar-version-type {:name "deploy/only-jar-version-type" - :desc "Deploy the built jar based on the type of version it has. One of :full (default, e.g. \"1.2.3\"), :full-and-snapshot (also jar-versions like \"1.2.3-SNAPSHOT\"), :all (any jar that was built)."}} + :desc "Deploy the built jar based on the type of version it has. One of :full (default, e.g. \"1.2.3\"), :full-and-pre (also prerelease versions like \"1.2.3-pre.1\"), :all (any jar that was built)."}} deploy "Deploy the built jar." [{:deploy/keys [only-jar-version-type] :or {only-jar-version-type :full} :as opts}] - {:pre [(#{:full-and-snapshot :full :all} only-jar-version-type)]} + {:pre [(#{:full-and-pre :full :all} only-jar-version-type)]} (let [{:keys [jar-file] :as opts} (jar-opts opts) pom-file (b/pom-path (select-keys opts [:lib :class-dir])) version (pom-path->version pom-file) - [v s] (re-find #"^\d+\.\d+\.\d+(-SNAPSHOT)?$" version) + [v pre] (re-find #"^\d+\.\d+\.\d+(-pre\.\d+)?$" version) deploy? (or (= :all only-jar-version-type) - (and (= :full-and-snapshot only-jar-version-type) v) + (and (= :full-and-pre only-jar-version-type) v) (and (= :full only-jar-version-type) v - (not s)))] + (not pre)))] (if deploy? (do ;; guards against the write-pom patch (top of this file) silently losing effect From 41b3f5ee14c1e2bbf1554d66a0a2cb7ad5a15ccb Mon Sep 17 00:00:00 2001 From: Gert Goet Date: Sun, 2 Aug 2026 22:45:35 +0200 Subject: [PATCH 9/9] Fix quickstart example --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2af6b57..0fc34b7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ user=> (p (ms/select Person [:name])) [:vector [:map [:street {:optional true} string?] - [:country {:optional true} string?]]]]] + [:zip {:optional true} string?]]]]] ;; *if* any address is provided, it should at least have :street user=> (p (ms/select Person [{:addresses [:street]}])) @@ -44,13 +44,13 @@ user=> (p (ms/select Person [{:addresses [:street]}])) [:addresses {:optional true} [:vector - [:map [:street string?] [:country {:optional true} string?]]]]] + [:map [:street string?] [:zip {:optional true} string?]]]]] ;; example valid data: ;; {}, {:addresses []}, {:addresses [{:street "Main"}]} ;; ;; example invalid data: -;; {:addresses nil}, {:addresses [{}]}, {:addresses [{:street "Foo" :country :se}]} +;; {:addresses nil}, {:addresses [{}]}, {:addresses [{:street "Foo" :zip 1234}]} ;; any address provided should be a full address user=> (p (ms/select Person [{:addresses ['*]}]))