Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions doc/ring/coercion.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Coercion can be attached to route data under `:coercion` key. There can be multi

Parameters are defined in route data under `:parameters` key. It's value should be a map of parameter `:type` -> Coercion Schema.

Responses are defined in route data under `:responses` key. It's value should be a map of http status code to a map which can contain `:body` key with Coercion Schema as value.
Responses are defined in route data under `:responses` key. It's value should be a map of http status code to a map which can contain `:body` key with Coercion Schema as value. Additionally, the key `:default` specifies the coercion for other status codes.

Below is an example with [Plumatic Schema](https://github.com/plumatic/schema). It defines schemas for `:query`, `:body` and `:path` parameters and for http 200 response `:body`.

Expand All @@ -54,7 +54,8 @@ Handlers can access the coerced parameters via the `:parameters` key in the requ
:parameters {:query {:x s/Int}
:body {:y s/Int}
:path {:z s/Int}}
:responses {200 {:body {:total PositiveInt}}}
:responses {200 {:body {:total PositiveInt}}
:default {:body {:error s/Str}}}
:handler (fn [{:keys [parameters]}]
(let [total (+ (-> parameters :query :x)
(-> parameters :body :y)
Expand Down Expand Up @@ -206,6 +207,14 @@ is:
rrc/coerce-response-middleware]}})))
```

The resolution logic for response coercers is:
1. Get the response status, or `:default` from the `:responses` map
2. From this map, get use the first of these to coerce:
1. `:content <content-type> :schema`
2. `:content :default :schema`
3. `:body`
3. If nothing was found, do not coerce

## Pretty printing spec errors

Spec problems are exposed as is in request & response coercion errors. Pretty-printers like [expound](https://github.com/bhb/expound) can be enabled like this:
Expand Down
14 changes: 7 additions & 7 deletions doc/ring/compiling_middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ To demonstrate the two approaches, below is the response coercion middleware wri
coercion (-> match :data :coercion)
opts (-> match :data :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(coerce-response coercers request response))
(let [coercer (response-coercer coercion responses opts)]
(coercer request response))
response)))
([request respond raise]
(let [method (:request-method request)
Expand All @@ -37,8 +37,8 @@ To demonstrate the two approaches, below is the response coercion middleware wri
coercion (-> match :data :coercion)
opts (-> match :data :opts)]
(if (and coercion responses)
(let [coercers (response-coercers coercion responses opts)]
(handler request #(respond (coerce-response coercers request %))))
(let [coercer (response-coercer coercion responses opts)]
(handler request #(respond (coercer request %))))
(handler request respond raise))))))
```

Expand All @@ -60,13 +60,13 @@ To demonstrate the two approaches, below is the response coercion middleware wri
:spec ::rs/responses
:compile (fn [{:keys [coercion responses]} opts]
(if (and coercion responses)
(let [coercers (coercion/response-coercers coercion responses opts)]
(let [coercer (coercion/response-coercer coercion responses opts)]
(fn [handler]
(fn
([request]
(coercion/coerce-response coercers request (handler request)))
(coercer request (handler request)))
([request respond raise]
(handler request #(respond (coercion/coerce-response coercers request %)) raise)))))))})
(handler request #(respond (coercer request %)) raise)))))))})
```

It has 50% less code, it's much easier to reason about and is much faster.
Expand Down
14 changes: 10 additions & 4 deletions examples/openapi/src/example/server.clj
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
{:status 200
:body {:color :red
:pineapple true}})}
:post {:summary "Create a pizza | Multiple content-types, multiple examples"
:post {:summary "Create a pizza | Multiple content-types, multiple examples | Default response schema"
:request {:description "Create a pizza using json or EDN"
:content {"application/json" {:schema [:map
[:color :keyword]
Expand All @@ -83,10 +83,16 @@
:pineapple false})}}}}}
:responses {200 {:description "Success"
:content {:default {:schema [:map [:success :boolean]]
:example {:success true}}}}}
:example {:success true}}}}
:default {:description "Not success"
:content {:default {:schema [:map [:error :string]]
:example {:error "error"}}}}}
:handler (fn [_request]
{:status 200
:body {:success true}})}}]
(if (< (Math/random) 0.5)
{:status 200
:body {:success true}}
{:status 500
:body {:error "an error happened"}}))}}]


["/contact"
Expand Down
72 changes: 36 additions & 36 deletions modules/reitit-core/src/reitit/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -130,29 +130,6 @@
(request-coercion-failed! result coercion value in request serialize-failed-result)
result)))))))

(defn extract-response-format-default [request _]
(-> request :muuntaja/response :format))

(defn response-coercer [coercion {:keys [content body]} {:keys [extract-response-format serialize-failed-result]
:or {extract-response-format extract-response-format-default}}]
(if coercion
(let [format->coercer (some->> (concat (when body
[[:default (-response-coercer coercion body)]])
(for [[format {:keys [schema]}] content, :when schema]
[format (-response-coercer coercion schema)]))
(filter second) (seq) (into (array-map)))]
(when format->coercer
(fn [request response]
(let [format (extract-response-format request response)
value (:body response)
coercer (or (format->coercer format)
(format->coercer :default)
-identity-coercer)
result (coercer value format)]
(if (error? result)
(response-coercion-failed! result coercion value request response serialize-failed-result)
result)))))))

(defn encode-error [data]
(-> data
(dissoc :request :response)
Expand All @@ -165,12 +142,6 @@
(impl/fast-assoc acc k (coercer request)))
{} coercers))

(defn coerce-response [coercers request response]
(if response
(if-let [coercer (or (coercers (:status response)) (coercers :default))]
(impl/fast-assoc response :body (coercer request response))
response)))

(defn request-coercers
([coercion parameters opts]
(some->> (for [[k v] parameters, :when v]
Expand All @@ -181,13 +152,42 @@
rcs (request-coercers coercion parameters (cond-> opts route-request (assoc ::skip #{:body})))]
(if (and crc rcs) (into crc (vec rcs)) (or crc rcs)))))

(defn response-coercers [coercion responses opts]
(some->> (for [[status model] responses]
(do
(when-not (int? status)
(throw (ex-info "Response status must be int" {:status status})))
[status (response-coercer coercion model opts)]))
(filter second) (seq) (into {})))
(defn extract-response-format-default [request _]
(-> request :muuntaja/response :format))

(defn -format->coercer [coercion {:keys [content body]} _opts]
(->> (concat (when body
[[:default (-response-coercer coercion body)]])
(for [[format {:keys [schema]}] content, :when schema]
[format (-response-coercer coercion schema)]))
(filter second) (into (array-map))))

(defn response-coercer [coercion responses {:keys [extract-response-format serialize-failed-result]
:or {extract-response-format extract-response-format-default}
:as opts}]
(when coercion
(let [status->format->coercer
(into {}
(for [[status model] responses]
(do
(when-not (or (= :default status) (int? status))
(throw (ex-info "Response status must be int or :default" {:status status})))
[status (-format->coercer coercion model opts)])))]
(when-not (every? empty? (vals status->format->coercer)) ;; fast path: return nil if there are no models to coerce
(fn [request response]
(let [format->coercer (or (status->format->coercer (:status response))
(status->format->coercer :default))
format (extract-response-format request response)
coercer (or (format->coercer format)
(format->coercer :default))]
(if-not coercer
response
(let [value (:body response)
coerced (coercer (:body response) format)
result (if (error? coerced)
(response-coercion-failed! coerced coercion value request response serialize-failed-result)
coerced)]
(impl/fast-assoc response :body result)))))))))

(defn -compile-parameters [data coercion]
(impl/path-update data [[[:parameters any?] #(-compile-model coercion % nil)]]))
Expand Down
4 changes: 2 additions & 2 deletions modules/reitit-http/src/reitit/http/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@
(not responses) {}
;; mount
:else
(if-let [coercers (coercion/response-coercers coercion responses opts)]
(if-let [coercer (coercion/response-coercer coercion responses opts)]
{:leave (fn [ctx]
(let [request (:request ctx)
response (:response ctx)
response (coercion/coerce-response coercers request response)]
response (coercer request response)]
(assoc ctx :response response)))}
{})))})

Expand Down
6 changes: 3 additions & 3 deletions modules/reitit-ring/src/reitit/ring/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,13 @@
(not responses) {}
;; mount
:else
(if-let [coercers (coercion/response-coercers coercion responses opts)]
(if-let [coercer (coercion/response-coercer coercion responses opts)]
(fn [handler]
(fn
([request]
(coercion/coerce-response coercers request (handler request)))
(coercer request (handler request)))
([request respond raise]
(handler request #(respond (coercion/coerce-response coercers request %)) raise))))
(handler request #(respond (coercer request %)) raise))))
{})))})

(def coerce-exceptions-middleware
Expand Down
46 changes: 23 additions & 23 deletions test/cljc/reitit/openapi_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@
:responses {200 {:description "success"
:body {:total int?}}
500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:default {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:handler (fn [{{{:keys [z]} :path
xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
Expand All @@ -96,9 +96,9 @@
:responses {200 {:description "success"
:body [:map [:total int?]]}
500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:default {:description "default"
:content {:default {:schema {:error string?}}}
:body {:masked string?}}}
:handler (fn [{{{:keys [z]} :path
xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]
Expand All @@ -125,9 +125,9 @@
:responses {200 {:description "success"
:body {:total s/Int}}
500 {:description "fail"}
504 {:description "default"
:content {:default {:schema {:error s/Str}}}
:body {:masked s/Str}}}
:default {:description "default"
:content {:default {:schema {:error s/Str}}}
:body {:masked s/Str}}}
:handler (fn [{{{:keys [z]} :path
xs :body} :parameters}]
{:status 200, :body {:total (+ (reduce + xs) z)}})}}]]]
Expand Down Expand Up @@ -200,10 +200,10 @@
400 {:content {"application/json" {:schema {:type "string"}}}
:description "kosh"}
500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:default {:description "default"
:content {"application/json" {:schema {:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:summary "plus with body"}}
"/api/malli/plus/{z}" {:get {:parameters [{:in "query"
:name :x
Expand Down Expand Up @@ -242,11 +242,11 @@
400 {:description "kosh"
:content {"application/json" {:schema {:type "string"}}}}
500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {:error {:type "string"}}
:required [:error]
:type "object"}}}}}
:default {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {:error {:type "string"}}
:required [:error]
:type "object"}}}}}
:summary "plus with body"}}
"/api/schema/plus/{z}" {:get {:parameters [{:in "query"
:name "x"
Expand Down Expand Up @@ -292,11 +292,11 @@
400 {:description "kosh"
:content {"application/json" {:schema {:type "string"}}}}
500 {:description "fail"}
504 {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:default {:description "default"
:content {"application/json" {:schema {:additionalProperties false
:properties {"error" {:type "string"}}
:required ["error"]
:type "object"}}}}}
:summary "plus with body"}}}}]
(is (= expected spec))
(is (= nil (validate spec))))))
Expand Down
Loading
Loading