Coercion is a reified Protocol, not a Record

This commit is contained in:
Tommi Reiman 2017-12-13 18:00:50 +02:00
parent cc8bd542dd
commit 2dc3636064
7 changed files with 90 additions and 101 deletions

View file

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

View file

@ -65,7 +65,7 @@ A Match:
(r/match-by-path r "/metosin/users/123") (r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id", ; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view, ; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...} ; :coercion <<:schema>>
; :parameters {:path {:company java.lang.String, ; :parameters {:path {:company java.lang.String,
; :user-id Int}}}, ; :user-id Int}}},
; :result nil, ; :result nil,
@ -105,7 +105,7 @@ Routing again:
(r/match-by-path r "/metosin/users/123") (r/match-by-path r "/metosin/users/123")
; #Match{:template "/:company/users/:user-id", ; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view, ; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...} ; :coercion <<:schema>>
; :parameters {:path {:company java.lang.String, ; :parameters {:path {:company java.lang.String,
; :user-id Int}}}, ; :user-id Int}}},
; :result {:path #object[reitit.coercion$request_coercer$]}, ; :result {:path #object[reitit.coercion$request_coercer$]},
@ -143,6 +143,7 @@ Here's an full example for doing both routing and coercion with Reitit:
```clj ```clj
(require '[reitit.coercion.schema]) (require '[reitit.coercion.schema])
(require '[reitit.coercion :as coercion]) (require '[reitit.coercion :as coercion])
(require '[reitit.core :as r])
(require '[schema.core :as s]) (require '[schema.core :as s])
(def router (def router
@ -160,7 +161,7 @@ Here's an full example for doing both routing and coercion with Reitit:
(match-by-path-and-coerce! "/metosin/users/123") (match-by-path-and-coerce! "/metosin/users/123")
; #Match{:template "/:company/users/:user-id", ; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view, ; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...} ; :coercion <<:schema>>
; :parameters {:path {:company java.lang.String, ; :parameters {:path {:company java.lang.String,
; :user-id Int}}}, ; :user-id Int}}},
; :result {:path #object[reitit.coercion$request_coercer$]}, ; :result {:path #object[reitit.coercion$request_coercer$]},

View file

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

View file

@ -27,7 +27,7 @@ Successful coercion:
(match-by-path-and-coerce! "/metosin/users/123") (match-by-path-and-coerce! "/metosin/users/123")
; #Match{:template "/:company/users/:user-id", ; #Match{:template "/:company/users/:user-id",
; :data {:name :user/user-view, ; :data {:name :user/user-view,
; :coercion #SchemaCoercion{...} ; :coercion <<:schema>>
; :parameters {:path {:company java.lang.String, ; :parameters {:path {:company java.lang.String,
; :user-id Int}}}, ; :user-id Int}}},
; :result {:path #object[reitit.coercion$request_coercer$]}, ; :result {:path #object[reitit.coercion$request_coercer$]},

View file

@ -1,8 +1,8 @@
(ns reitit.coercion (ns reitit.coercion
(:require [clojure.walk :as walk] (:require [clojure.walk :as walk]
[spec-tools.core :as st] [reitit.impl :as impl])
[reitit.ring :as ring] #?(:clj
[reitit.impl :as impl])) (:import (java.io Writer))))
;; ;;
;; Protocol ;; Protocol
@ -11,6 +11,7 @@
(defprotocol Coercion (defprotocol Coercion
"Pluggable coercion protocol" "Pluggable coercion protocol"
(-get-name [this] "Keyword name for the coercion") (-get-name [this] "Keyword name for the coercion")
(-get-options [this] "Coercion options")
(-get-apidocs [this model data] "???") (-get-apidocs [this model data] "???")
(-compile-model [this model name] "Compiles a model") (-compile-model [this model name] "Compiles a model")
(-open-model [this model] "Returns a new model which allows extra keys in maps") (-open-model [this model] "Returns a new model which allows extra keys in maps")
@ -18,6 +19,10 @@
(-request-coercer [this type model] "Returns a `value format => value` request coercion function") (-request-coercer [this type model] "Returns a `value format => value` request coercion function")
(-response-coercer [this model] "Returns a `value format => value` response coercion function")) (-response-coercer [this model] "Returns a `value format => value` response coercion function"))
#?(:clj
(defmethod print-method ::coercion [coercion ^Writer w]
(.write w (str "<<" (-get-name coercion) ">>"))))
(defrecord CoercionError []) (defrecord CoercionError [])
(defn error? [x] (defn error? [x]

View file

@ -5,7 +5,6 @@
[schema.coerce :as sc] [schema.coerce :as sc]
[schema.utils :as su] [schema.utils :as su]
[schema-tools.coerce :as stc] [schema-tools.coerce :as stc]
[spec-tools.swagger.core :as swagger]
[reitit.coercion :as coercion])) [reitit.coercion :as coercion]))
(def string-coercion-matcher (def string-coercion-matcher
@ -33,45 +32,6 @@
:else x)) :else x))
schema)) schema))
(defrecord SchemaCoercion [name matchers coerce-response?]
coercion/Coercion
(-get-name [_] name)
(-get-apidocs [_ _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses)
parameters (assoc ::swagger/parameters parameters)
responses (assoc ::swagger/responses responses)))
(-compile-model [_ model _] model)
(-open-model [_ schema] (st/open-schema schema))
(-encode-error [_ error]
(-> error
(update :schema stringify)
(update :errors stringify)))
(-request-coercer [_ type schema]
(let [{:keys [formats default]} (matchers type)
coercers (->> (for [m (conj (vals formats) default)]
[m (sc/coercer schema m)])
(into {}))]
(fn [value format]
(if-let [matcher (or (get formats format) default)]
(let [coercer (coercers matcher)
coerced (coercer value)]
(if-let [error (su/error-val coerced)]
(coercion/map->CoercionError
{:schema schema
:errors error})
coerced))
value))))
(-response-coercer [this schema]
(if (coerce-response? schema)
(coercion/-request-coercer this :response schema))))
(def default-options (def default-options
{:coerce-response? coerce-response? {:coerce-response? coerce-response?
:matchers {:body {:default default-coercion-matcher :matchers {:body {:default default-coercion-matcher
@ -79,7 +39,38 @@
:string {:default string-coercion-matcher} :string {:default string-coercion-matcher}
:response {:default default-coercion-matcher}}}) :response {:default default-coercion-matcher}}})
(defn create [{:keys [matchers coerce-response?]}] (defn create [{:keys [matchers coerce-response?] :as opts}]
(->SchemaCoercion :schema matchers coerce-response?)) ^{:type ::coercion/coercion}
(reify coercion/Coercion
(-get-name [_] :schema)
(-get-options [_] opts)
(-get-apidocs [_ _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses)
parameters (assoc ::parameters parameters)
responses (assoc ::responses responses)))
(-compile-model [_ model _] model)
(-open-model [_ schema] (st/open-schema schema))
(-encode-error [_ error]
(-> error
(update :schema stringify)
(update :errors stringify)))
(-request-coercer [_ type schema]
(let [{:keys [formats default]} (matchers type)
coercers (->> (for [m (conj (vals formats) default)]
[m (sc/coercer schema m)])
(into {}))]
(fn [value format]
(if-let [matcher (or (get formats format) default)]
(let [coercer (coercers matcher)
coerced (coercer value)]
(if-let [error (su/error-val coerced)]
(coercion/map->CoercionError
{:schema schema
:errors error})
coerced))
value))))
(-response-coercer [this schema]
(if (coerce-response? schema)
(coercion/-request-coercer this :response schema)))))
(def coercion (create default-options)) (def coercion (create default-options))

View file

@ -52,54 +52,6 @@
(defmulti coerce-response? identity :default ::default) (defmulti coerce-response? identity :default ::default)
(defmethod coerce-response? ::default [_] true) (defmethod coerce-response? ::default [_] true)
(defrecord SpecCoercion [name conforming coerce-response?]
coercion/Coercion
(-get-name [_] name)
(-get-apidocs [this _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses)
parameters (assoc
::swagger/parameters
(into
(empty parameters)
(for [[k v] parameters]
[k (coercion/-compile-model this v nil)])))
responses (assoc
::swagger/responses
(into
(empty responses)
(for [[k response] responses]
[k (update response :schema #(coercion/-compile-model this % nil))])))))
(-compile-model [_ model _]
(into-spec model (or name (gensym "spec"))))
(-open-model [_ spec] spec)
(-encode-error [_ error]
(-> error
(update :spec (comp str s/form))
(update :problems (partial mapv #(update % :pred stringify-pred)))))
(-request-coercer [this type spec]
(let [spec (coercion/-compile-model this spec nil)
{:keys [formats default]} (conforming type)]
(fn [value format]
(if-let [conforming (or (get formats format) default)]
(let [conformed (st/conform spec value conforming)]
(if (s/invalid? conformed)
(let [problems (st/explain-data spec value conforming)]
(coercion/map->CoercionError
{:spec spec
:problems (::s/problems problems)}))
(s/unform spec conformed)))
value))))
(-response-coercer [this spec]
(if (coerce-response? spec)
(coercion/-request-coercer this :response spec))))
(def default-options (def default-options
{:coerce-response? coerce-response? {:coerce-response? coerce-response?
:conforming {:body {:default default-conforming :conforming {:body {:default default-conforming
@ -107,7 +59,47 @@
:string {:default string-conforming} :string {:default string-conforming}
:response {:default default-conforming}}}) :response {:default default-conforming}}})
(defn create [{:keys [conforming coerce-response?]}] (defn create [{:keys [conforming coerce-response?] :as opts}]
(->SpecCoercion :spec conforming coerce-response?)) ^{:type ::coercion/coercion}
(reify coercion/Coercion
(-get-name [_] :spec)
(-get-options [_] opts)
(-get-apidocs [this _ {:keys [parameters responses] :as info}]
(cond-> (dissoc info :parameters :responses)
parameters (assoc
::swagger/parameters
(into
(empty parameters)
(for [[k v] parameters]
[k (coercion/-compile-model this v nil)])))
responses (assoc
::swagger/responses
(into
(empty responses)
(for [[k response] responses]
[k (update response :schema #(coercion/-compile-model this % nil))])))))
(-compile-model [_ model name]
(into-spec model (or name (gensym "spec"))))
(-open-model [_ spec] spec)
(-encode-error [_ error]
(-> error
(update :spec (comp str s/form))
(update :problems (partial mapv #(update % :pred stringify-pred)))))
(-request-coercer [this type spec]
(let [spec (coercion/-compile-model this spec nil)
{:keys [formats default]} (conforming type)]
(fn [value format]
(if-let [conforming (or (get formats format) default)]
(let [conformed (st/conform spec value conforming)]
(if (s/invalid? conformed)
(let [problems (st/explain-data spec value conforming)]
(coercion/map->CoercionError
{:spec spec
:problems (::s/problems problems)}))
(s/unform spec conformed)))
value))))
(-response-coercer [this spec]
(if (coerce-response? spec)
(coercion/-request-coercer this :response spec)))))
(def coercion (create default-options)) (def coercion (create default-options))