spec.alpha
spec.alpha copied to clipboard
CLJ2116: Support for selective conforming with clojure.spec
THIS IS A DUMMY PR for discussion purposes, will create a patch via Jira is gets further.
What
Support separation of conform from Specs, allowing different conforming to be run for same specs at runtime.
This is already implemented in spec-tools, but it needs to use Dynamic Binding and wrap specs into Spec Records to make this work.
Why
Allows Specs to be used as a runtime transformation engine, main use cases being the Web: sending and receiving Spec'd data over different formats (String, JSON, Transit) without needing to write manually differently conforming specs for all combinations.
How
- Add a extra argument
cc(conforming callback) to:conform*,explain*,conform,explain,explain-dataandexplain-str: all support the old arities, causingccto be set tonil- BREAKING:
conform*andexplain*always have the extra parameter, passed on to internal spec functions - if
ccis set inconform, it is called with aspecargument. It should return eithernil(default case, run conform as before) or a anonymousconform*function, which is used to pre-conform the value before passing it to normal conform - BREAKING: all calls to to subspecs
conform*fromconform* need to be called via top-levelconform` - which has a perf optimized arity for this.
Todo
- [ ] Support also runtime conforming
explain - [ ] Fix docs
- [ ] More tests
Notes
The actual supporting converters (string->long, string->keyword) and Conforming Callback could be hosted in non-core project like in spec-tools to enable moving fast - and supporting both clj & cljs.
Example
(deftest conforming-callback-test
(let [string->int-conforming
(fn [spec]
(condp = spec
int? (fn [_ x _]
(cond
(int? x) x
(string? x) (try
(Long/parseLong x)
(catch Exception _
::s/invalid))
:else ::s/invalid))
:else nil))]
(testing "no conforming callback"
(is (= 1 (s/conform int? 1)))
(is (= ::s/invalid (s/conform int? "1"))))
(testing "with conforming callback"
(is (= 1 (s/conform int? 1 string->int-conforming)))
(is (= 1 (s/conform int? "1" string->int-conforming))))))