Skip to content

Commit

Permalink
Merge pull request #628 from metosin/openapi-parameters
Browse files Browse the repository at this point in the history
Openapi parameters
  • Loading branch information
ikitommi authored Aug 24, 2023
2 parents 73422e8 + 05cbed8 commit b0c810a
Show file tree
Hide file tree
Showing 13 changed files with 429 additions and 284 deletions.
30 changes: 15 additions & 15 deletions doc/ring/coercion.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,21 @@ You can also specify request and response body schemas per content-type. The syn
```clj
(def app
(ring/ring-handler
(ring/router
["/api"
["/example" {:post {:coercion reitit.coercion.schema/coercion
:parameters {:request {:content {"application/json" {:y s/Int}
"application/edn" {:z s/Int}}
;; default if no content-type matches:
:body {:yy s/Int}}}
:responses {200 {:content {"application/json" {:w s/Int}
"application/edn" {:x s/Int}}
;; default if no content-type matches:
:body {:ww s/Int}}
:handler ...}}]]
{:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})))
(ring/router
["/api"
["/example" {:post {:coercion reitit.coercion.schema/coercion
:request {:content {"application/json" {:schema {:y s/Int}}
"application/edn" {:schema {:z s/Int}}}
;; default if no content-type matches:
:body {:yy s/Int}}
:responses {200 {:content {"application/json" {:schema {:w s/Int}}
"application/edn" {:schema {:x s/Int}}}
;; default if no content-type matches:
:body {:ww s/Int}}}
:handler ...}}]]
{:data {:middleware [rrc/coerce-exceptions-middleware
rrc/coerce-request-middleware
rrc/coerce-response-middleware]}})))
```

## Pretty printing spec errors
Expand Down
108 changes: 65 additions & 43 deletions modules/reitit-core/src/reitit/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
(def ^:no-doc default-parameter-coercion
{:query (->ParameterCoercion :query-params :string true true)
:body (->ParameterCoercion :body-params :body false false)
:request (->ParameterCoercion :body-params :request false false)
:form (->ParameterCoercion :form-params :string true true)
:header (->ParameterCoercion :headers :string true true)
:path (->ParameterCoercion :path-params :string true true)
Expand Down Expand Up @@ -83,53 +82,72 @@
value)

;; TODO: support faster key walking, walk/keywordize-keys is quite slow...
(defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result]
(defn request-coercer [coercion type model {::keys [extract-request-format parameter-coercion serialize-failed-result skip]
:or {extract-request-format extract-request-format-default
parameter-coercion default-parameter-coercion}}]
parameter-coercion default-parameter-coercion
skip #{}}}]
(if coercion
(if-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
->open (if open? #(-open-model coercion %) identity)
format-schema-pairs (if (= :request style)
(conj (:content model) [:default (:body model)])
[[:default model]])
format->coercer (some->> (for [[format schema] format-schema-pairs
:when schema
:let [type (case style :request :body style)]]
[format (-request-coercer coercion type (->open schema))])
(filter second)
(seq)
(into {}))]
(when format->coercer
(fn [request]
(let [value (transform request)
format (extract-request-format request)
coercer (or (format->coercer format)
(format->coercer :default)
-identity-coercer)
result (coercer value format)]
(if (error? result)
(request-coercion-failed! result coercion value in request serialize-failed-result)
result))))))))
(when-let [{:keys [keywordize? open? in style]} (parameter-coercion type)]
(when-not (skip style)
(let [transform (comp (if keywordize? walk/keywordize-keys identity) in)
->open (if open? #(-open-model coercion %) identity)
coercer (-request-coercer coercion style (->open model))]
(when coercer
(fn [request]
(let [value (transform request)
format (extract-request-format request)
result (coercer value format)]
(if (error? result)
(request-coercion-failed! result coercion value in request serialize-failed-result)
result)))))))))

(defn get-default-schema [request-or-response]
(or (-> request-or-response :content :default :schema)
(:body request-or-response)))

(defn get-default [request-or-response]
(or (-> request-or-response :content :default)
(some->> request-or-response :body (assoc {} :schema))))

(defn content-request-coercer [coercion {:keys [content body]} {::keys [extract-request-format serialize-failed-result]
:or {extract-request-format extract-request-format-default}}]
(when coercion
(let [in :body-params
format->coercer (some->> (concat (when body
[[:default (-request-coercer coercion :body body)]])
(for [[format {:keys [schema]}] content, :when schema]
[format (-request-coercer coercion :body schema)]))
(filter second) (seq) (into (array-map)))]
(when format->coercer
(fn [request]
(let [value (in request)
format (extract-request-format request)
coercer (or (format->coercer format)
(format->coercer :default)
-identity-coercer)
result (coercer value format)]
(if (error? result)
(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 [per-format-coercers (some->> (for [[format schema] content
:when schema]
[format (-response-coercer coercion schema)])
(filter second)
(seq)
(into {}))
default (when body (-response-coercer coercion body))]
(when (or per-format-coercers default)
(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 (get per-format-coercers format (or default -identity-coercer))
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)
Expand All @@ -153,10 +171,15 @@
(impl/fast-assoc response :body (coercer request response))
response)))

(defn request-coercers [coercion parameters opts]
(some->> (for [[k v] parameters, :when v]
[k (request-coercer coercion k v opts)])
(filter second) (seq) (into {})))
(defn request-coercers
([coercion parameters opts]
(some->> (for [[k v] parameters, :when v]
[k (request-coercer coercion k v opts)])
(filter second) (seq) (into {})))
([coercion parameters route-request opts]
(let [crc (when route-request (some->> (content-request-coercer coercion route-request opts) (array-map :request)))
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]
Expand All @@ -170,8 +193,8 @@
;; api-docs
;;

(defn -warn-unsupported-coercions [{:keys [parameters responses] :as _data}]
(when (:request parameters)
(defn -warn-unsupported-coercions [{:keys [request responses] :as _data}]
(when request
(println "WARNING [reitit.coercion]: swagger apidocs don't support :request coercion"))
(when (some :content (vals responses))
(println "WARNING [reitit.coercion]: swagger apidocs don't support :responses :content coercion")))
Expand All @@ -197,7 +220,6 @@
(into {}))))
(-get-apidocs coercion specification))))))


;;
;; integration
;;
Expand Down
5 changes: 4 additions & 1 deletion modules/reitit-core/src/reitit/spec.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@

(s/def :reitit.core.coercion/model any?)

(s/def :reitit.core.coercion/schema any?)
(s/def :reitit.core.coercion/map-model (s/keys :opt-un [:reitit.core.coercion/schema]))

(s/def :reitit.core.coercion/content
(s/map-of string? :reitit.core.coercion/model))
(s/map-of (s/or :string string?, :default #{:default}) :reitit.core.coercion/map-model))

(s/def :reitit.core.coercion/query :reitit.core.coercion/model)
(s/def :reitit.core.coercion/body :reitit.core.coercion/model)
Expand Down
6 changes: 3 additions & 3 deletions modules/reitit-http/src/reitit/http/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@
[]
{:name ::coerce-request
:spec ::rs/parameters
:compile (fn [{:keys [coercion parameters]} opts]
:compile (fn [{:keys [coercion parameters request]} opts]
(cond
;; no coercion, skip
(not coercion) nil
;; just coercion, don't mount
(not parameters) {}
(not (or parameters request)) {}
;; mount
:else
(if-let [coercers (coercion/request-coercers coercion parameters opts)]
(if-let [coercers (coercion/request-coercers coercion parameters request opts)]
{:enter (fn [ctx]
(let [request (:request ctx)
coerced (coercion/coerce-request coercers request)
Expand Down
79 changes: 45 additions & 34 deletions modules/reitit-malli/src/reitit/coercion/malli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,22 @@
;; malli options
:options nil})

;; TODO: this is now seems like a generic transforming function that could be used in all of malli, spec, schema
;; ... just tranform the schemas in place
;; also, this has internally massive amount of duplicate code, could be simplified
;; ... tests too
(defn -get-apidocs-openapi
[coercion {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options]
(let [{:keys [body request multipart]} parameters
[_ {:keys [request parameters responses content-types] :or {content-types ["application/json"]}} options]
(let [{:keys [body multipart]} parameters
parameters (dissoc parameters :request :body :multipart)
->schema-object (fn [schema opts]
(let [current-opts (merge options opts)]
(json-schema/transform schema current-opts)))]
(json-schema/transform schema current-opts)))
->content (fn [data schema]
(merge
{:schema schema}
(select-keys data [:description :examples])
(:openapi data)))]
(merge
(when (seq parameters)
{:parameters
Expand Down Expand Up @@ -168,20 +177,21 @@
;; request allow to different :requestBody per content-type
{:requestBody
{:content (merge
(when (:body request)
(select-keys request [:description])
(when-let [{:keys [schema] :as data} (coercion/get-default request)]
(into {}
(map (fn [content-type]
(let [schema (->schema-object (:body request) {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
(let [schema (->schema-object schema {:in :requestBody
:type :schema
:content-type content-type})]
[content-type (->content data schema)])))
content-types))
(into {}
(map (fn [[content-type requestBody]]
(let [schema (->schema-object requestBody {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
(map (fn [[content-type {:keys [schema] :as data}]]
(let [schema (->schema-object schema {:in :requestBody
:type :schema
:content-type content-type})]
[content-type (->content data schema)])))
(:content request)))}})
(when multipart
{:requestBody
Expand All @@ -194,29 +204,30 @@
(when responses
{:responses
(into {}
(map (fn [[status {:keys [body content]
:as response}]]
(let [content (merge
(when body
(into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :responses
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types))
(when content
(into {}
(map (fn [[content-type schema]]
(let [schema (->schema-object schema {:in :responses
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content)))]
(map (fn [[status {:keys [content], :as response}]]
(let [default (coercion/get-default-schema response)
content (-> (merge
(when default
(into {}
(map (fn [content-type]
(let [schema (->schema-object default {:in :responses
:type :schema
:content-type content-type})]
[content-type (->content nil schema)])))
content-types))
(when content
(into {}
(map (fn [[content-type {:keys [schema] :as data}]]
(let [schema (->schema-object schema {:in :responses
:type :schema
:content-type content-type})]
[content-type (->content data schema)])))
content)))
(dissoc :default))]
[status (merge (select-keys response [:description])
(when content
{:content content}))])))
responses)}))))
{:content content}))]))
responses))}))))

(defn create
([]
Expand Down
22 changes: 14 additions & 8 deletions modules/reitit-ring/src/reitit/ring.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,25 @@
(defn -update-paths [f]
(let [not-request? #(not= :request %)
http-method? #(contains? http-methods %)]
[;; default parameters and responses
[;; default parameters
[[:parameters not-request?] f]
[[http-method? :parameters not-request?] f]

;; default responses
[[:responses any? :body] f]
[[http-method? :responses any? :body] f]

;; openapi3 parameters and responses
[[:parameters :request :content any?] f]
[[http-method? :parameters :request :content any?] f]
[[:parameters :request :body] f]
[[http-method? :parameters :request :body] f]
[[:responses any? :content any?] f]
[[http-method? :responses any? :content any?] f]]))
;; openapi3 request
[[:request :content any? :schema] f]
[[http-method? :request :content any? :schema] f]

;; openapi3 LEGACY body
[[:request :body] f]
[[http-method? :request :body] f]

;; openapi3 responses
[[:responses any? :content any? :schema] f]
[[http-method? :responses any? :content any? :schema] f]]))

(defn -compile-coercion [{:keys [coercion] :as data}]
(cond-> data coercion (impl/path-update (-update-paths #(coercion/-compile-model coercion % nil)))))
Expand Down
Loading

0 comments on commit b0c810a

Please sign in to comment.