Skip to content

Commit 8dd2c18

Browse files
authored
Add response specs (#6)
1 parent 7ad64ac commit 8dd2c18

File tree

3 files changed

+112
-29
lines changed

3 files changed

+112
-29
lines changed

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,11 @@ Experimental
1212

1313
- Read OpenAPI 3 definitions as JSON or YAML
1414
- Remote and relative [refs](https://swagger.io/docs/specification/using-ref/)
15-
- Request coercions powered by [Malli](https://github.com/metosin/malli)
15+
- Request and response coercions powered by [Malli](https://github.com/metosin/malli)
1616
- requestBody coercion
1717
- A fair set of OpenAPI types are currently supported, please raise an issue if something is unsupported
1818

1919
Currently unsupported:
20-
- Response coercions
2120
- Other coercion libs
2221

2322
Any contributions are much much welcome and appreciated!

src/navi/core.clj

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
QueryParameter
1313
RequestBody
1414
Parameter]
15+
[io.swagger.v3.oas.models.responses ApiResponse]
1516
[io.swagger.v3.oas.models Operation
16-
PathItem]
17+
PathItem
18+
PathItem$HttpMethod]
1719
[io.swagger.v3.parser OpenAPIV3Parser]
1820
[io.swagger.v3.parser.core.models ParseOptions]))
1921

@@ -27,6 +29,17 @@
2729
(contains? m k)
2830
(update-in [k] #(into [:map] %))))
2931

32+
(defn update-kvs
33+
"Update a map using `key-fn` and `val-fn`.
34+
Sort of like composing `update-keys` and `update-vals`.
35+
Unlike `update-keys` or `update-vals`, preserve `nil`s."
36+
[m key-fn val-fn]
37+
(when m
38+
(reduce-kv (fn kv-mapper [m k v]
39+
(assoc m (key-fn k) (val-fn v)))
40+
{}
41+
m)))
42+
3043
(defn schema->spec [^Schema schema]
3144
(let [types (.getTypes schema)]
3245
(if (= 1 (count types))
@@ -72,7 +85,7 @@
7285

7386
(defmulti spec
7487
(fn [^Schema schema]
75-
(first (.getTypes schema))))
88+
(or (first (.getTypes schema)) "null")))
7689

7790
(defmethod spec
7891
"string"
@@ -102,6 +115,11 @@
102115
[_]
103116
nil?)
104117

118+
(defmethod spec
119+
nil
120+
[_]
121+
nil?)
122+
105123
(defmethod spec
106124
"object"
107125
[^Schema schema]
@@ -158,8 +176,49 @@
158176
body-spec
159177
[:or nil? body-spec])}))
160178

179+
;;; Handle reponses
180+
181+
(defn handle-response-key
182+
"Reitit seems to want status codes of a response to be integer keys,
183+
rather than keyword keys or string keys (except for `:default`).
184+
So, convert a string to a Long if relevant.
185+
Else if the string is \"default\", then return `:default`, otherwise pass through.
186+
Arguably, all non-integer status codes should be converted to keywords."
187+
[s]
188+
(cond (re-matches #"\d{3}" s) (Long/parseLong s)
189+
(= "default" s) :default
190+
:else s))
191+
192+
(defn media-type->data
193+
"Convert a Java Schema's MediaType to a spec that Reitit will accept."
194+
[^MediaType mt]
195+
{:schema (spec (.getSchema mt))})
196+
197+
(defn handle-media-type-key
198+
"If the media type is \"default\", then return it as a keyword, otherwise pass through."
199+
[s]
200+
(if (= "default" s)
201+
:default
202+
s))
203+
204+
(defn response->data
205+
"Convert an ApiResponse to a response conforming to reitit."
206+
[^ApiResponse response]
207+
(let [orig-content (.getContent response)
208+
;; If no content then use the nil? schema with a default media type.
209+
;; This is a work-around for a current Reitit bug.
210+
;; See https://github.com/metosin/reitit/issues/691
211+
content (if orig-content
212+
(update-kvs orig-content handle-media-type-key media-type->data)
213+
{:default {:schema nil?}})
214+
description (.getDescription response)]
215+
;; TODO: Perhaps handle other ApiResponse fields as well?
216+
(cond-> {:content content}
217+
description (assoc :description description))))
218+
161219
(defn operation->data
162-
"Converts an Operation to map of parameters, schemas and handler conforming to reitit"
220+
"Converts a Java Operation to a map of parameters, responses, schemas and handler
221+
that conforms to reitit."
163222
[^Operation op handlers]
164223
(let [params (into [] (.getParameters op))
165224
request-body (.getRequestBody op)
@@ -171,39 +230,29 @@
171230
(apply merge-with into)
172231
(wrap-map :path)
173232
(wrap-map :query)
174-
(wrap-map :header))]
233+
(wrap-map :header))
234+
responses (-> (.getResponses op)
235+
(update-kvs handle-response-key response->data)) ]
175236
(cond-> {:handler (get handlers (.getOperationId op))}
176-
(seq schemas)
177-
(assoc :parameters schemas))))
237+
(seq schemas) (assoc :parameters schemas)
238+
(seq responses) (assoc :responses responses))))
178239

179240
(defn path-item->data
180241
"Converts a path to its corresponding vector of method and the operation map"
181242
[^PathItem path-item handlers]
182-
(->> path-item
183-
.readOperationsMap
184-
(map #(vector (-> ^Map$Entry %
185-
.getKey
186-
.toString
187-
.toLowerCase
188-
keyword)
189-
(-> ^Map$Entry %
190-
.getValue
191-
(operation->data handlers))))
192-
(into {})))
243+
(update-kvs (.readOperationsMap path-item)
244+
#(keyword (.toLowerCase (.toString ^PathItem$HttpMethod %)))
245+
#(operation->data % handlers)))
193246

194247
(defn routes-from
195248
"Takes in the OpenAPI JSON/YAML as string and a map of OperationId to handler fns.
196249
Returns the reitit route map with malli schemas"
197250
[^String api-spec handlers]
198251
(let [parse-options (doto (ParseOptions.)
199-
(.setResolveFully true))]
200-
(->> (.readContents (OpenAPIV3Parser.) api-spec nil parse-options)
201-
.getOpenAPI
202-
.getPaths
203-
(mapv #(vector (.getKey ^Map$Entry %)
204-
(-> ^Map$Entry %
205-
.getValue
206-
(path-item->data handlers)))))))
252+
(.setResolveFully true))
253+
contents (.readContents (OpenAPIV3Parser.) api-spec nil parse-options)
254+
paths (.getPaths (.getOpenAPI contents))]
255+
(mapv identity (update-kvs paths identity #(path-item->data % handlers)))))
207256

208257
(comment
209258
(require '[clojure.pprint :as pp])

test/navi/core_test.clj

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
[io.swagger.v3.oas.models Operation PathItem]
1212
[io.swagger.v3.oas.models.media Content StringSchema IntegerSchema JsonSchema
1313
NumberSchema ObjectSchema ArraySchema MediaType UUIDSchema Schema]
14-
[io.swagger.v3.oas.models.parameters Parameter PathParameter HeaderParameter QueryParameter RequestBody]))
14+
[io.swagger.v3.oas.models.parameters Parameter PathParameter HeaderParameter QueryParameter RequestBody]
15+
[io.swagger.v3.oas.models.responses ApiResponses ApiResponse]))
1516

1617
(deftest map-to-malli-spec
1718
(testing "surrounding values of a clojure map to a malli map spec"
@@ -126,6 +127,31 @@
126127
(is (#{[:or string? int?] [:or int? string?]}
127128
(core/schema->spec strint))))))
128129

130+
(deftest responses-to-malli-spec
131+
(testing "empty response"
132+
(let [response (ApiResponse.)]
133+
(is (= {:content {:default {:schema nil?}}}
134+
(core/response->data response)))))
135+
(testing "default media type"
136+
(let [media (doto (MediaType.)
137+
(.setSchema (StringSchema.)))
138+
content (doto (Content.)
139+
(.put "default" media))
140+
response (doto (ApiResponse.)
141+
(.setContent content)) ]
142+
(is (= {:content {:default {:schema string?}}}
143+
(core/response->data response)))))
144+
(testing "json object response"
145+
(let [media (doto (MediaType.)
146+
(.setSchema (ObjectSchema.)))
147+
content (doto (Content.)
148+
(.put "application/json" media))
149+
response (doto (ApiResponse.)
150+
(.setContent content)) ]
151+
(is (= {:content {"application/json" {:schema [:map {:closed false}]}}}
152+
(core/response->data response)))))
153+
)
154+
129155
(deftest parameters-to-malli-spec
130156
(testing "path"
131157
(let [param (doto (PathParameter.)
@@ -185,13 +211,22 @@
185211
hparam (doto (HeaderParameter.)
186212
(.setName "y")
187213
(.setSchema (StringSchema.)))
214+
response (doto (ApiResponse.)
215+
(.setContent (doto (Content.)
216+
(.put "application/json"
217+
(doto (MediaType.)
218+
(.setSchema (ObjectSchema.)))))))
219+
responses (doto (ApiResponses.)
220+
(.put "200" response))
188221
operation (doto (Operation.)
189222
(.setParameters [param hparam])
223+
(.setResponses responses)
190224
(.setOperationId "TestOp"))
191225
handlers {"TestOp" "a handler"}]
192226
(is (= {:handler "a handler"
193227
:parameters {:path [:map [:x int?]]
194-
:header [:map [:y {:optional true} string?]]}}
228+
:header [:map [:y {:optional true} string?]]}
229+
:responses {200 {:content {"application/json" {:schema [:map {:closed false}]}}}}}
195230
(core/operation->data operation handlers))))))
196231

197232
(deftest openapi-path-to-malli-spec

0 commit comments

Comments
 (0)