Last active
August 19, 2022 06:00
-
-
Save czan/8752cacf527ceb0e64982b9fa8747892 to your computer and use it in GitHub Desktop.
Reitit OpenAPI3 hack
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns ^{:doc "Convert Swagger 2 specs to OpenAPI 3 specs | |
We're using the standard reitit mechanisms to produce a Swagger spec, | |
but we'd much rather be using an OpenAPI 3 spec. This namespace does a | |
conversion for us, until the functionality lands in reitit."} | |
convert-to-openapi | |
(:require [camel-snake-kebab.core :refer [->PascalCase]] | |
[clojure.set :refer [rename-keys]] | |
[clojure.string :as str] | |
[clojure.walk :as walk] | |
[reitit.coercion.spec :as spec] | |
[reitit.coercion.schema :as schema])) | |
(defn map-vals [f m] | |
(into {} (map (fn [[k v]] [k (f v)])) m)) | |
(defn upgrade-to-openapi-v3 [route-data specification] | |
(let [schemas (volatile! {})] | |
(letfn [(clean-schema-title [title] | |
(when-not (false? (::component-schemas route-data)) | |
(condp = (:coercion route-data) | |
spec/coercion (->PascalCase title :separator #"[/-]") | |
schema/coercion (second (str/split title #"/")) | |
(throw (ex-info "Unknown coercion function, can't automatically map name to OpenAPI spec." | |
{:coercion (:coercion route-data)}))))) | |
(extract-schema [object] | |
(or (when-let [title (and (:type object) (:title object) (clean-schema-title (:title object)))] | |
(let [schema (assoc object :title title)] | |
(when-let [old-schema (get @schemas title)] | |
(when-not (= schema old-schema) | |
(throw (ex-info "Multiple schemas with the same name but different definitions" | |
{:name title | |
:schemas [schema old-schema]})))) | |
(vswap! schemas assoc title schema) | |
{"$ref" (str "#/components/schemas/" title)})) | |
(cond-> object | |
(:x-nullable object) (rename-keys {:x-nullable :nullable})))) | |
(transform-parameter [parameter] | |
(if (:schema parameter) | |
parameter | |
(assoc (dissoc parameter :type :format :enum) | |
:schema (-> parameter | |
(select-keys [:type :format :enum :x-nullable]) | |
(rename-keys {:x-nullable :nullable}))))) | |
(transform-endpoint [{:keys [produces responses] :as endpoint}] | |
(let [fixed-parameters (map transform-parameter (:parameters endpoint)) | |
body-parameter (first (filter #(= (:in %) "body") fixed-parameters)) | |
non-body-parameters (remove #(= (:in %) "body") fixed-parameters) | |
fixed-body-parameter (when body-parameter | |
{:required (:required body-parameter) | |
:content (into {} | |
(map (fn [content-type] | |
[content-type (select-keys body-parameter [:schema])])) | |
produces)}) | |
fixed-responses (map-vals (fn [response] | |
(let [content (into {} | |
(map (fn [content-type] | |
[content-type (select-keys response [:schema])])) | |
produces)] | |
(assoc (dissoc response :schema) | |
:content content))) | |
responses)] | |
(cond-> (dissoc endpoint :produces :consumes) | |
non-body-parameters (assoc :parameters non-body-parameters) | |
fixed-body-parameter (assoc :requestBody fixed-body-parameter) | |
fixed-responses (assoc :responses fixed-responses)))) | |
(transform-path [path] | |
(map-vals transform-endpoint path))] | |
(-> (walk/postwalk extract-schema specification) | |
(assoc-in [:components :schemas] @schemas) | |
(update :paths #(map-vals transform-path %)) | |
(dissoc :swagger) | |
(assoc :openapi "3.0.3"))))) | |
(def swagger->openapi | |
{:name ::extract-swagger-json-definitions | |
:compile (fn [route-data _opts] | |
(fn [handler] | |
(fn [request] | |
(update (handler request) :body #(upgrade-to-openapi-v3 route-data %)))))}) | |
;; Then your Swagger endpoint can look something like this: | |
;; | |
;; ["/swagger.json" | |
;; {:get {:no-doc true | |
;; :swagger {:basePath "/"} | |
;; :middleware [swagger->openapi] | |
;; :handler (swagger/create-swagger-handler)}}] |
@czan I had a spec that was generating {"requestBody": null }
which is invald, so I just made all of them conditional if null without analyzing the others too much.
@czan it looks like you missed the (dissoc endpoint :produces **:consumes**)
part of it which was the other bug I encountered. Consumes was valid in openapi2 (just like :produces) but is obsolete in openapi 3.
Good catch. I've fixed that up now. Thanks!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thanks @sundbry, I've updated the gist with the patch from metosin/reitit#84. From what I can see, though, only
fixed-body-parameter
can ever be falsey?non-body-parameters
is the result ofremove
, andfixed-responses
is the result ofmap-vals
, and neither of these functions can return a falsey value. Can you elaborate on why you needed them in thecond->
?