Welcome! Please see the About page for a little more info on how this works.

0 votes
in Spec by
h3. Problem

using {{clojure.spec}} in runtime border validation supporting multiple exchange formats is hard.

h3. Details

Currently in clojure.spec (alpha-14), conformers are attached to Spec instances at creation time and they are invoked on every conform. This is not very useful in system border validation, where conforming/coercion functions should be selected based on runtime data, e.g. the exchange format.


* a {{keyword?}} spec:
** with EDN, no coercion should be done (it can present Keywords)
** with JSON, String->Keyword coercion should be applied
** with String-based formats (CSV, query-params, ...), String->Keyword coercion should be applied

* a {{integer?}} spec:
** with EDN, no coercion should be done (it can present numbers)
** with JSON, no coercion should be done (it can present numbers)
** with String-based formats (CSV, query-params, ...), String->Long coercion should be applied

Here is a more complete example:

(s/def ::id integer?)
(s/def ::name string?)
(s/def ::title keyword?)
(s/def ::person (s/keys :opt [::id], :req-un [::name ::title]))

;; this is how we see the data over different exchange formats
(def edn-person {::id 1, :name "Tiina", :title :boss})
(def json-person {::id 1, :name "Tiina", :title "boss"})
(def string-person {::id "1", :name "Tiina", :title "boss"})

;; here's what we want
(def conformed-person edn-person)

To use this today, one needs to manually create new border specs with different conformers for all different exchange formats. Non-qualified keywords could be mapped in {{s/keys}} to work (e.g. {{::title}} => {{::title$JSON}}), but this wont work if fully qualified keys are exposed over the border (like {{::id}} in the example) - one can't register multiple, differently conforming version of the spec with same name.

h3. Suggestion

Support selective conforming in the Spec Protocol with a new 3-arity {{conform*}} and {{clojure.spec/conform}}, both taking a extra user-provided callback/visitor function. If the callback is provided, it's called from within the Specs {{conform*}} with the current spec as argument and it will return either {{nil}} or a 2-arity conformer function that should be used for the actual confrom.

Actual conforming-matcher implementations can be maintained in 3rd party libraries, like spec-tools[1].

Using it would look like this:

;; edn
(assert (= conformed-person (s/conform ::person edn-person)))
(assert (= conformed-person (s/conform ::person edn-person nil)))

;; json
(assert (= conformed-person (s/conform ::person json-person json-conforming-matcher)))

;; string
(assert (= conformed-person (s/conform ::person string-person string-conforming-matcher)))

h3. Alternative

Another option to support this would be to allow Specs to be extended with Protocols. 3rd party libs could have a new {{Conforming}} protocol with 3-arity {{conform}} and add implementations for it on all current specs. Currently this is not possible.

[1] https://github.com/metosin/spec-tools

16 Answers

0 votes
Reference: https://clojure.atlassian.net/browse/CLJ-2116 (reported by ikitommi)