Liking cljdoc? Tell your friends :D

Clojure.spec Coercion

The clojure.spec library specifies the structure of data, validates or destructures it, and can generate data based on the spec.

Warning

clojure.spec by itself doesn't support coercion. reitit uses spec-tools that adds coercion to spec. Like clojure.spec, it's alpha as it leans both on spec walking and clojure.spec.alpha/conform, which is considered a spec internal, that might be changed or removed later.

Usage

For simple specs (core predicates, spec-tools.core/spec, s/and, s/or, s/coll-of, s/keys, s/map-of, s/nillable and s/every), the transformation is inferred using spec-walker and is automatic. To support all specs (like regex-specs), specs need to be wrapped into Spec Records.

There are CLJ-2116 and CLJ-2251 that would help solve this elegantly. Go vote 'em up.

Example

(require '[reitit.coercion.spec])
(require '[reitit.coercion :as coercion])
(require '[spec-tools.spec :as spec])
(require '[clojure.spec.alpha :as s])
(require '[reitit.core :as r])

;; simple specs, inferred
(s/def ::company string?)
(s/def ::user-id int?)
(s/def ::path-params (s/keys :req-un [::company ::user-id]))

(def router
  (r/router
    ["/:company/users/:user-id" {:name ::user-view
                                 :coercion reitit.coercion.spec/coercion
                                 :parameters {:path ::path-params}}]
    {:compile coercion/compile-request-coercers}))

(defn match-by-path-and-coerce! [path]
  (if-let [match (r/match-by-path router path)]
    (assoc match :parameters (coercion/coerce! match))))

Successful coercion:

(match-by-path-and-coerce! "/metosin/users/123")
; #Match{:template "/:company/users/:user-id",
;        :data {:name :user/user-view,
;               :coercion <<:spec>>
;               :parameters {:path ::path-params}},
;        :result {:path #object[reitit.coercion$request_coercer$]},
;        :path-params {:company "metosin", :user-id "123"},
;        :parameters {:path {:company "metosin", :user-id 123}}
;        :path "/metosin/users/123"}

Failing coercion:

(match-by-path-and-coerce! "/metosin/users/ikitommi")
; => ExceptionInfo Request coercion failed...

Deeply nested

Spec-tools allow deeply nested specs to be coerced. One can test the coercion easily in the REPL.

Define some specs:

(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])

(s/def :sku/id keyword?)
(s/def ::sku (s/keys :req-un [:sku/id]))
(s/def ::skus (s/coll-of ::sku :into []))

(s/def :photo/id int?)
(s/def ::photo (s/keys :req-un [:photo/id]))
(s/def ::photos (s/coll-of ::photo :into []))

(s/def ::my-json-api (s/keys :req-un [::skus ::photos]))

Apply a string->edn coercion to the data:

(st/coerce
  ::my-json-api
  {:skus [{:id "123"}]
   :photos [{:id "123"}]}
  st/string-transformer)
; {:skus [{:id :123}]
;  :photos [{:id 123}]}

Apply a json->edn coercion to the data:

(st/coerce
  ::my-json-api
  {:skus [{:id "123"}]
   :photos [{:id "123"}]}
  st/json-transformer)
; {:skus [{:id :123}]
;  :photos [{:id "123"}]}

By default, reitit uses custom transformers that also strip out extra keys from s/keys specs:

(require '[reitit.coercion.spec :as rcs])

(st/coerce
  ::my-json-api
  {:TOO "MUCH"
   :skus [{:id "123"
           :INFOR "MATION"}]
   :photos [{:id "123"
             :HERE "TOO"}]}
  rcs/json-transformer)
; {:skus [{:id :123}]
;  :photos [{:id "123"}]}

Defining Optional Keys

Going back to the previous example.

Suppose you want the ::my-json-api to have optional remarks as string and each photo to have an optional height and width as integer. The s/keys accepts :opt-un to support optional keys.

(require '[clojure.spec.alpha :as s])
(require '[spec-tools.core :as st])

(s/def :sku/id keyword?)
(s/def ::sku (s/keys :req-un [:sku/id]))
(s/def ::skus (s/coll-of ::sku :into []))
(s/def ::remarks string?)  ;; define remarks as string

(s/def :photo/id int?)
(s/def :photo/height int?) ;; define height as int
(s/def :photo/width int?)  ;; define width as int
(s/def ::photo (s/keys :req-un [:photo/id]
                       :opt-un [:photo/height :photo/width])) ;; height and width are in :opt-un
(s/def ::photos (s/coll-of ::photo :into []))

(s/def ::my-json-api (s/keys :req-un [::skus ::photos]
                             :opt-un [::remarks])) ;; remarks is in the :opt-un

Apply a string->edn coercion to the data:

;; Omit optional keys
(st/coerce
  ::my-json-api
  {:skus [{:id "123"}]
   :photos [{:id "123"}]}
  st/string-transformer)
;;{:skus [{:id :123}],
;; :photos [{:id 123}]}


;; coerce the optional keys if present

(st/coerce
  ::my-json-api
  {:skus [{:id "123"}]
   :photos [{:id "123" :height "100" :width "100"}]
   :remarks "some remarks"}
  st/string-transformer)

;; {:skus [{:id :123}]
;;  :photos [{:id 123 :height 100 :width 100}]
;;  :remarks "some remarks"}

(st/coerce
  ::my-json-api
  {:skus [{:id "123"}]
   :photos [{:id "123" :height "100"}]}
  st/string-transformer)
;; {:skus [{:id :123}],
;;  :photos [{:id 123, :height 100}]}

Can you improve this documentation? These fine people already did:
Tommi Reiman, Jaime Sangcap, Marcus Spiegel & kalekale
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close