|
12 | 12 | QueryParameter
|
13 | 13 | RequestBody
|
14 | 14 | Parameter]
|
| 15 | + [io.swagger.v3.oas.models.responses ApiResponse] |
15 | 16 | [io.swagger.v3.oas.models Operation
|
16 |
| - PathItem] |
| 17 | + PathItem |
| 18 | + PathItem$HttpMethod] |
17 | 19 | [io.swagger.v3.parser OpenAPIV3Parser]
|
18 | 20 | [io.swagger.v3.parser.core.models ParseOptions]))
|
19 | 21 |
|
|
27 | 29 | (contains? m k)
|
28 | 30 | (update-in [k] #(into [:map] %))))
|
29 | 31 |
|
| 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 | + |
30 | 43 | (defn schema->spec [^Schema schema]
|
31 | 44 | (let [types (.getTypes schema)]
|
32 | 45 | (if (= 1 (count types))
|
|
72 | 85 |
|
73 | 86 | (defmulti spec
|
74 | 87 | (fn [^Schema schema]
|
75 |
| - (first (.getTypes schema)))) |
| 88 | + (or (first (.getTypes schema)) "null"))) |
76 | 89 |
|
77 | 90 | (defmethod spec
|
78 | 91 | "string"
|
|
102 | 115 | [_]
|
103 | 116 | nil?)
|
104 | 117 |
|
| 118 | +(defmethod spec |
| 119 | + nil |
| 120 | + [_] |
| 121 | + nil?) |
| 122 | + |
105 | 123 | (defmethod spec
|
106 | 124 | "object"
|
107 | 125 | [^Schema schema]
|
|
158 | 176 | body-spec
|
159 | 177 | [:or nil? body-spec])}))
|
160 | 178 |
|
| 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 | + |
161 | 219 | (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." |
163 | 222 | [^Operation op handlers]
|
164 | 223 | (let [params (into [] (.getParameters op))
|
165 | 224 | request-body (.getRequestBody op)
|
|
171 | 230 | (apply merge-with into)
|
172 | 231 | (wrap-map :path)
|
173 | 232 | (wrap-map :query)
|
174 |
| - (wrap-map :header))] |
| 233 | + (wrap-map :header)) |
| 234 | + responses (-> (.getResponses op) |
| 235 | + (update-kvs handle-response-key response->data)) ] |
175 | 236 | (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)))) |
178 | 239 |
|
179 | 240 | (defn path-item->data
|
180 | 241 | "Converts a path to its corresponding vector of method and the operation map"
|
181 | 242 | [^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))) |
193 | 246 |
|
194 | 247 | (defn routes-from
|
195 | 248 | "Takes in the OpenAPI JSON/YAML as string and a map of OperationId to handler fns.
|
196 | 249 | Returns the reitit route map with malli schemas"
|
197 | 250 | [^String api-spec handlers]
|
198 | 251 | (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))))) |
207 | 256 |
|
208 | 257 | (comment
|
209 | 258 | (require '[clojure.pprint :as pp])
|
|
0 commit comments