-
Notifications
You must be signed in to change notification settings - Fork 257
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support for OpenAPI3 #563
Support for OpenAPI3 #563
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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] | ||||||||||||||||||||||||||||||||
|
@@ -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)}}) | ||||||||||||||||||||||||||||||||
Comment on lines
+161
to
+167
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a very minor suggestion for this snippet and below. Feel free to discard.
Suggested change
|
||||||||||||||||||||||||||||||||
(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)) | ||||||||||||||||||||||||||||||||
|
@@ -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 | ||||||||||||||||||||||||||||||||
|
@@ -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) | ||||||||||||||||||||||||||||||||
|
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]]) |
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I really like this thorough, nice docstring. I noticed top-level directories |
||
|
||
[\"/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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? Completely worth keeping if it's a helpful breadcrumb for later developer, I can't judge. |
||
(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 {}))] | ||
souenzzo marked this conversation as resolved.
Show resolved
Hide resolved
|
||
[(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)))))) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do accept that this is most desirable (in some personal projects I exclude malli from reitit and import the latest version), but is it required by this diff? Would it be more appropriate in a dependency-related PR? |
||
[lambdaisland/deep-diff "0.0-47"] | ||
[meta-merge "1.0.0"] | ||
[com.bhauman/spell-spec "0.1.2"] | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this vertical space compare with neighbouring ones?