Skip to content

Commit

Permalink
OpenAPI V3 Support
Browse files Browse the repository at this point in the history
  • Loading branch information
souenzzo committed Sep 5, 2022
1 parent 3dff4c8 commit c576b47
Show file tree
Hide file tree
Showing 8 changed files with 805 additions and 2 deletions.
2 changes: 2 additions & 0 deletions modules/reitit-core/src/reitit/coercion.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
:path :path
:multipart :formData}]
(case specification
:openapi (-get-apidocs coercion specification data)
:swagger (->> (update
data
:parameters
Expand All @@ -156,6 +157,7 @@
(into {}))))
(-get-apidocs coercion specification)))))


;;
;; integration
;;
Expand Down
74 changes: 73 additions & 1 deletion modules/reitit-malli/src/reitit/coercion/malli.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
[malli.edn :as edn]
[malli.error :as me]
[malli.experimental.lite :as l]
[malli.json-schema :as json-schema]
[malli.swagger :as swagger]
[malli.transform :as mt]
[malli.util :as mu]
Expand Down Expand Up @@ -132,6 +133,76 @@
;; malli options
:options nil})

(defn -get-apidocs-openapi
[coercion {:keys [parameters responses content-types] :or {content-types ["application/json"]}} options]
(let [{:keys [body request]} parameters
parameters (dissoc parameters :request :body)
->schema-object (fn [schema opts]
(let [current-opts (merge options opts)]
(json-schema/transform (coercion/-compile-model coercion schema current-opts)
current-opts)))]

(merge
(when (seq parameters)
{:parameters
(->> (for [[in schema] parameters
:let [{:keys [properties required] :as root} (->schema-object schema {:in in :type :parameter})
required? (partial contains? (set required))]
[k schema] properties]
(merge {:in (name in)
:name k
:required (required? k)
:schema schema}
(select-keys root [:description])))
(into []))})
(when body
;; body uses a single schema to describe every :requestBody
;; the schema-object transformer should be able to transform into distinct content-types
{:requestBody {:content (into {}
(map (fn [content-type]
(let [schema (->schema-object body {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
content-types)}})
(when request
;; request allow to different :requestBody per content-type
{:requestBody
{:content
(into {}
(map (fn [[content-type requestBody]]
(let [schema (->schema-object requestBody {:in :requestBody
:type :schema
:content-type content-type})]
[content-type {:schema schema}])))
(:content request))}})
(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)))]
[status (merge (select-keys response [:description])
(when content
{:content content}))])))
responses)}))))

(defn create
([]
(create nil))
Expand All @@ -145,7 +216,7 @@
(reify coercion/Coercion
(-get-name [_] :malli)
(-get-options [_] opts)
(-get-apidocs [_ specification {:keys [parameters responses]}]
(-get-apidocs [this specification {:keys [parameters responses] :as data}]
(case specification
:swagger (merge
(if parameters
Expand All @@ -167,6 +238,7 @@
(update :schema compile options)
(update :schema swagger/transform {:type :schema}))
$))]))}))
:openapi (-get-apidocs-openapi this data options)
(throw
(ex-info
(str "Can't produce Schema apidocs for " specification)
Expand Down
12 changes: 12 additions & 0 deletions modules/reitit-openapi/project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
(defproject metosin/reitit-openapi "0.5.18"
:description "Reitit: OpenAPI-support"
:url "https://github.com/metosin/reitit"
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:scm {:name "git"
:url "https://github.com/metosin/reitit"
:dir "../.."}
:plugins [[lein-parent "0.3.8"]]
:parent-project {:path "../../project.clj"
:inherit [:deploy-repositories :managed-dependencies]}
:dependencies [[metosin/reitit-core]])
109 changes: 109 additions & 0 deletions modules/reitit-openapi/src/reitit/openapi.cljc
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
(ns reitit.openapi
(:require [clojure.set :as set]
[clojure.spec.alpha :as s]
[clojure.string :as str]
[meta-merge.core :refer [meta-merge]]
[reitit.coercion :as coercion]
[reitit.core :as r]
[reitit.trie :as trie]))

(s/def ::id (s/or :keyword keyword? :set (s/coll-of keyword? :into #{})))
(s/def ::no-doc boolean?)
(s/def ::tags (s/coll-of (s/or :keyword keyword? :string string?) :kind #{}))
(s/def ::summary string?)
(s/def ::description string?)

(s/def ::openapi (s/keys :opt-un [::id]))
(s/def ::spec (s/keys :opt-un [::openapi ::no-doc ::tags ::summary ::description]))

(def openapi-feature
"Feature for handling openapi-documentation for routes.
Works both with Middleware & Interceptors. Does not participate
in actual request processing, just provides specs for the new
documentation keys for the route data. Should be accompanied by a
[[openapi-spec-handler]] to expose the openapi spec.
New route data keys contributing to openapi docs:
| key | description |
| --------------|-------------|
| :openapi | map of any openapi-data. Must have `:id` (keyword or sequence of keywords) to identify the api
| :no-doc | optional boolean to exclude endpoint from api docs
| :summary | optional short string summary of an endpoint
| :description | optional long description of an endpoint. Supports http://spec.commonmark.org/
Also the coercion keys contribute to openapi spec:
| key | description |
| --------------|-------------|
| :parameters | optional input parameters for a route, in a format defined by the coercion
| :responses | optional descriptions of responses, in a format defined by coercion
Example:
[\"/api\"
{:openapi {:id :my-api}
:middleware [reitit.openapi/openapi-feature]}
[\"/openapi.json\"
{:get {:no-doc true
:openapi {:info {:title \"my-api\"}}
:handler reitit.openapi/openapi-spec-handler}}]
[\"/plus\"
{:get {:openapi {:tags \"math\"}
:summary \"adds numbers together\"
:description \"takes `x` and `y` query-params and adds them together\"
:parameters {:query {:x int?, :y int?}}
:responses {200 {:body {:total pos-int?}}}
:handler (fn [{:keys [parameters]}]
{:status 200
:body (+ (-> parameters :query :x)
(-> parameters :query :y)})}}]]"
{:name ::openapi
:spec ::spec})

(defn- openapi-path [path opts]
(-> path (trie/normalize opts) (str/replace #"\{\*" "{")))

(defn create-openapi-handler
"Create a ring handler to emit openapi spec. Collects all routes from router which have
an intersecting `[:openapi :id]` and which are not marked with `:no-doc` route data."
[]
(fn create-openapi
([{::r/keys [router match] :keys [request-method]}]
(let [{:keys [id] :or {id ::default} :as openapi} (-> match :result request-method :data :openapi)
ids (trie/into-set id)
strip-top-level-keys #(dissoc % :id :info :host :basePath :definitions :securityDefinitions)
strip-endpoint-keys #(dissoc % :id :parameters :responses :summary :description)
openapi (->> (strip-endpoint-keys openapi)
(merge {:openapi "3.1.0"
:x-id ids}))
accept-route (fn [route]
(-> route second :openapi :id (or ::default) (trie/into-set) (set/intersection ids) seq))
;base-openapi-spec {:responses ^:displace {:default {:description ""}}}
transform-endpoint (fn [[method {{:keys [coercion no-doc openapi] :as data} :data
middleware :middleware
interceptors :interceptors}]]
(if (and data (not no-doc))
[method
(meta-merge
#_base-openapi-spec
(apply meta-merge (keep (comp :openapi :data) middleware))
(apply meta-merge (keep (comp :openapi :data) interceptors))
(if coercion
(coercion/get-apidocs coercion :openapi data))
(select-keys data [:tags :summary :description])
(strip-top-level-keys openapi))]))
transform-path (fn [[p _ c]]
(if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))]
[(openapi-path p (r/options router)) endpoint]))
map-in-order #(->> % (apply concat) (apply array-map))
paths (->> router (r/compiled-routes) (filter accept-route) (map transform-path) map-in-order)]
{:status 200
:body (meta-merge openapi {:paths paths})}))
([req res raise]
(try
(res (create-openapi req))
(catch #?(:clj Exception :cljs :default) e
(raise e))))))
28 changes: 28 additions & 0 deletions modules/reitit-schema/src/reitit/coercion/schema.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[reitit.coercion :as coercion]
[schema-tools.coerce :as stc]
[schema-tools.core :as st]
[schema-tools.openapi.core :as openapi]
[schema-tools.swagger.core :as swagger]
[schema.coerce :as sc]
[schema.core :as s]
Expand Down Expand Up @@ -67,6 +68,33 @@
(if (:schema $)
(update $ :schema #(coercion/-compile-model this % nil))
$))]))})))
:openapi (merge
(when (seq (dissoc parameters :body :request))
(openapi/openapi-spec {::openapi/parameters
(into
(empty parameters)
(for [[k v] (dissoc parameters :body :request)]
[k (coercion/-compile-model this v nil)]))}))
(when (:body parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content {"application/json" (:body parameters)}})})
(when (:request parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (:content (:request parameters))})})
(when responses
{:responses
(into
(empty responses)
(for [[k response] responses]
[k (merge
(select-keys response [:description])
(when (:body response)
(openapi/openapi-spec
{::openapi/content {"application/json" (coercion/-compile-model this (:body response) nil)}}))
(when (:content response)
(openapi/openapi-spec
{::openapi/content (:content response)})))]))}))

(throw
(ex-info
(str "Can't produce Schema apidocs for " specification)
Expand Down
27 changes: 27 additions & 0 deletions modules/reitit-spec/src/reitit/coercion/spec.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[reitit.coercion :as coercion]
[spec-tools.core :as st #?@(:cljs [:refer [Spec]])]
[spec-tools.data-spec :as ds #?@(:cljs [:refer [Maybe]])]
[spec-tools.openapi.core :as openapi]
[spec-tools.swagger.core :as swagger])
#?(:clj
(:import (spec_tools.core Spec)
Expand Down Expand Up @@ -105,6 +106,32 @@
(if (:schema $)
(update $ :schema #(coercion/-compile-model this % nil))
$))]))})))
:openapi (openapi/openapi-spec
(merge
(when (seq (dissoc parameters :body :request))
{::openapi/parameters
(into (empty parameters)
(for [[k v] (dissoc parameters :body :request)]
[k (coercion/-compile-model this v nil)]))})
(when (:body parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content {"application/json" (coercion/-compile-model this (:body parameters) nil)}})})
(when (:request parameters)
{:requestBody (openapi/openapi-spec
{::openapi/content (coercion/-compile-model this (:content (:request parameters)) nil)})})
(when responses
{:responses
(into
(empty responses)
(for [[k response] responses]
[k (merge
(select-keys response [:description])
(when (:body response)
(openapi/openapi-spec
{::openapi/content {"application/json" (coercion/-compile-model this (:body response) nil)}}))
(when (:content response)
(openapi/openapi-spec
{::openapi/content (coercion/-compile-model this (:content response) nil)})))]))})))
(throw
(ex-info
(str "Can't produce Spec apidocs for " specification)
Expand Down
3 changes: 2 additions & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"modules/reitit-ring/src"
"modules/reitit-http/src"
"modules/reitit-middleware/src"
"modules/reitit-openapi/src"
"modules/reitit-interceptors/src"
"modules/reitit-malli/src"
"modules/reitit-spec/src"
Expand All @@ -86,7 +87,7 @@
[metosin/muuntaja "0.6.8"]
[metosin/sieppari "0.0.0-alpha13"]
[metosin/jsonista "0.3.5"]
[metosin/malli "0.8.2"]
[metosin/malli "0.8.9"]
[lambdaisland/deep-diff "0.0-47"]
[meta-merge "1.0.0"]
[com.bhauman/spell-spec "0.1.2"]
Expand Down
Loading

0 comments on commit c576b47

Please sign in to comment.