Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@

### Added

- `:multi` schema support: branches are selected with a *map*-selection —
keys are dispatch values, values are sub-selections. `{:human [:name]}`
requires `:name` in branch `:human`; unmentioned branches become
all-optional. `'*` as branch key addresses every branch (its sub-selection
must be satisfiable in each; `'*` merges with explicit branch keys). A
keyword `:dispatch` key is auto-required in every explicit map-branch.
Pruning (`^:only`) drops unmentioned branches (mentioning none keeps all).
`selectable-paths` and verification errors spell branch segments as
`{:branch dispatch-value}`, and shape mismatches (vector selection on a
`:multi`, map selection on a `:map`) come with a `:hint`.
See the README's "Multi-schemas" section, incl. limitations.

- 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
Expand All @@ -28,5 +40,18 @@
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.
- Selections with duplicate keys now merge instead of "last wins"
([`404d677`](https://github.com/eval/malli-select/commit/404d677)).
`[{:friends [:name]} {:friends [:age]}]` now requires both `:name` and
`:age` of friends; it used to require only `:age`. The merge is a union of
required paths, so results only get stricter, and it applies at every
nesting level.
Consequences:
- To override instead of merge, build a single map yourself.
- A later `{:friends []}` is now a no-op; it used to reset the
`:friends` sub-selection to all-optional.
- With `:prune-optionals` / `^:only`, merged selections keep more
attributes, so generated samples can gain fields.

[Unreleased]: https://github.com/eval/malli-select/compare/v0.7.0...HEAD
Older releases are documented on the
[GitHub releases page](https://github.com/eval/malli-select/releases).
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,62 @@ user=> (alter-var-root #'ms/*verify-selection* (constantly :log)) ;; CLJS: (set!
See [the tests](./test/malli_select/core_test.cljc) for more.


## Multi-schemas

Branches of a `:multi` schema are selected with a *map*: keys are dispatch
values, values are sub-selections. The selection mirrors the shape of the
schema — a vector addresses attributes of a `:map`, a map addresses branches
of a `:multi` — at any nesting depth.

``` clojure
user=> (def Animal
[:multi {:dispatch :type}
[:human [:map [:type :keyword] [:name string?] [:age pos-int?]]]
[:sized [:map [:type :keyword] [:size pos-int?]]]])

;; require :name in the :human branch; other branches become all-optional
user=> (p (ms/select Animal {:human [:name]}))
[:multi {:dispatch :type}
[:human [:map [:type :keyword] [:name string?] [:age {:optional true} pos-int?]]]
[:sized [:map [:type :keyword] [:size {:optional true} pos-int?]]]]

;; note that :type stays required: a keyword :dispatch key is auto-required in
;; every explicit map-branch, as data without it won't dispatch anyway.
;; (`::m/default` and nil branches are left alone - they also match data
;; *without* a dispatch value.)

;; '* addresses all branches...
user=> (p (ms/select Animal {'* [:type]}))

;; ...so the sub-selection must be satisfiable in *every* branch:
user=> (ms/select Animal {'* [:name]})
Execution error (ExceptionInfo) ...
;; :available contains what all branches have in common, e.g. [{:branch *} :type]

;; be more precise ('* merges with explicit branches):
user=> (ms/select Animal {'* [:type] :human [:name]})
;; ...or skip verification (branches lacking :name are then left as-is):
user=> (ms/select Animal {'* [:name]} {:verify-selection false})

;; a nested multi (a vector selection addresses map-attributes,
;; a map selection addresses branches):
user=> (def Person [:map [:id :int] [:pet [:multi {:dispatch :kind}
[:dog [:map [:kind :keyword] [:breed string?]]]
[:cat [:map [:kind :keyword] [:lives :int]]]]]])
user=> (p (ms/select Person [:id {:pet {:dog [:breed]}}]))

;; pruning drops unmentioned branches (mentioning none keeps all),
;; e.g. to generate only humans:
user=> (mg/generate (ms/select Animal ^:only {:human [:name]}))
{:name "x2Ep", :type :human}
```

Limitations:
- Branches that are not `:map` schemas (e.g. `[:multi ... [:str :string]]`) pass through untouched and cannot be selected.
- Numeric dispatch values `0` and `1` are unsupported (they collide with how paths are cleaned internally).
- A function-valued `:dispatch` disables the dispatch-key auto-require.


## LICENSE

Copyright (c) 2026 Gert Goet, ThinkCreate.
Expand Down
Loading