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 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
@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.