mirror of
https://github.com/metosin/reitit.git
synced 2026-02-15 15:55:15 +00:00
Coercion is a reified Protocol, not a Record
This commit is contained in:
parent
cc8bd542dd
commit
2dc3636064
7 changed files with 90 additions and 101 deletions
|
|
@ -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"},
|
||||||
|
|
|
||||||
|
|
@ -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$]},
|
||||||
|
|
|
||||||
|
|
@ -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$]},
|
||||||
|
|
|
||||||
|
|
@ -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$]},
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue