spec.alpha icon indicating copy to clipboard operation
spec.alpha copied to clipboard

CLJ2116: Support for selective conforming with clojure.spec

Open ikitommi opened this issue 8 years ago • 0 comments

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-data and explain-str: all support the old arities, causing cc to be set to nil
    • BREAKING: conform* and explain* always have the extra parameter, passed on to internal spec functions
    • if cc is set in conform, it is called with a spec argument. It should return either nil (default case, run conform as before) or a anonymous conform* function, which is used to pre-conform the value before passing it to normal conform
    • BREAKING: all calls to to subspecs conform* from conform* need to be called via top-level conform` - 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))))))

ikitommi avatar Jul 21 '17 09:07 ikitommi