From 8a320164a423ad983f2042906aa52c4cbe565765 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Mon, 14 May 2018 08:21:47 +0300 Subject: [PATCH] welcome swagger-ui! --- examples/ring-swagger/src/example/server.clj | 11 +++- modules/reitit-swagger-ui/project.clj | 11 ++++ .../src/reitit/swagger_ui.cljc | 52 +++++++++++++++ modules/reitit-swagger/project.clj | 4 +- .../reitit-swagger/src/reitit/swagger.cljc | 64 ++++--------------- modules/reitit/project.clj | 3 +- project.clj | 4 +- scripts/lein-modules | 2 +- test/cljc/reitit/ring_coercion_test.cljc | 4 +- test/cljc/reitit/swagger_test.clj | 30 +++++++++ 10 files changed, 125 insertions(+), 60 deletions(-) create mode 100644 modules/reitit-swagger-ui/project.clj create mode 100644 modules/reitit-swagger-ui/src/reitit/swagger_ui.cljc diff --git a/examples/ring-swagger/src/example/server.clj b/examples/ring-swagger/src/example/server.clj index 5cd274b4..03033c6c 100644 --- a/examples/ring-swagger/src/example/server.clj +++ b/examples/ring-swagger/src/example/server.clj @@ -1,6 +1,7 @@ (ns example.server (:require [reitit.ring :as ring] [reitit.swagger :as swagger] + [reitit.swagger-ui :as swagger-ui] [reitit.ring.coercion :as rrc] [reitit.coercion.spec :as spec] [reitit.coercion.schema :as schema] @@ -50,9 +51,15 @@ swagger/swagger-feature rrc/coerce-exceptions-middleware rrc/coerce-request-middleware - rrc/coerce-response-middleware]}}) + rrc/coerce-response-middleware] + :swagger {:produces #{"application/json" + "application/edn" + "application/transit+json"} + :consumes #{"application/json" + "application/edn" + "application/transit+json"}}}}) (ring/routes - (swagger/create-swagger-ui-handler + (swagger-ui/create-swagger-ui-handler {:path "", :url "/api/swagger.json"}) (ring/create-default-handler)))) diff --git a/modules/reitit-swagger-ui/project.clj b/modules/reitit-swagger-ui/project.clj new file mode 100644 index 00000000..fa43ea9e --- /dev/null +++ b/modules/reitit-swagger-ui/project.clj @@ -0,0 +1,11 @@ +(defproject metosin/reitit-swagger-ui "0.1.1-SNAPSHOT" + :description "Reitit: Swagger-ui support" + :url "https://github.com/metosin/reitit" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} + :plugins [[lein-parent "0.3.2"]] + :parent-project {:path "../../project.clj" + :inherit [:deploy-repositories :managed-dependencies]} + :dependencies [[metosin/reitit-ring] + [metosin/jsonista] + [metosin/ring-swagger-ui]]) diff --git a/modules/reitit-swagger-ui/src/reitit/swagger_ui.cljc b/modules/reitit-swagger-ui/src/reitit/swagger_ui.cljc new file mode 100644 index 00000000..202556a8 --- /dev/null +++ b/modules/reitit-swagger-ui/src/reitit/swagger_ui.cljc @@ -0,0 +1,52 @@ +(ns reitit.swagger-ui + (:require [clojure.string :as str] + [reitit.ring :as ring] + #?@(:clj [ + [jsonista.core :as j]]))) + +#?(:clj + (defn create-swagger-ui-handler + "Creates a ring handler which can be used to serve swagger-ui. + + | key | description | + | -----------------|-------------| + | :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:` + | :root | optional resource root, defaults to `\"swagger-ui\"` + | :url | path to swagger endpoint, defaults to `/swagger.json` + | :path | optional path to mount the handler to. Works only if mounted outside of a router. + | :config | parameters passed to swaggger-ui, keys transformed into camelCase. + + See https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md + for all available :config options + + Examples: + + ;; with defaults + (create-swagger-ui-handler) + + ;; with path and url set, swagger validator disabled + (swagger-ui/create-swagger-ui-handler + {:path \"\" + :url \"/api/swagger.json\" + :config {:validator-url nil})" + ([] + (create-swagger-ui-handler nil)) + ([options] + (let [mixed-case (fn [k] + (let [[f & rest] (str/split (name k) #"-")] + (apply str (str/lower-case f) (map str/capitalize rest)))) + mixed-case-key (fn [[k v]] [(mixed-case k) v]) + config-json (fn [{:keys [url config]}] (j/write-value-as-string (merge config {:url url}))) + conf-js (fn [opts] (str "window.API_CONF = " (config-json opts) ";")) + options (as-> options $ + (update $ :root (fnil identity "swagger-ui")) + (update $ :url (fnil identity "/swagger.json")) + (update $ :config #(->> % (map mixed-case-key) (into {}))) + (assoc $ :paths {"conf.js" {:headers {"Content-Type" "application/javascript"} + :status 200 + :body (conf-js $)} + "config.json" {:headers {"Content-Type" "application/json"} + :status 200 + :body (config-json $)}}))] + (ring/routes + (ring/create-resource-handler options)))))) diff --git a/modules/reitit-swagger/project.clj b/modules/reitit-swagger/project.clj index 34c5613a..384e6c5f 100644 --- a/modules/reitit-swagger/project.clj +++ b/modules/reitit-swagger/project.clj @@ -6,6 +6,4 @@ :plugins [[lein-parent "0.3.2"]] :parent-project {:path "../../project.clj" :inherit [:deploy-repositories :managed-dependencies]} - :dependencies [[metosin/reitit-ring] - [metosin/jsonista] - [metosin/ring-swagger-ui]]) + :dependencies [[metosin/reitit-core]]) diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index 41d2a33f..1679a061 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -3,13 +3,9 @@ [meta-merge.core :refer [meta-merge]] [clojure.spec.alpha :as s] [clojure.set :as set] - [clojure.string :as str] - [reitit.ring :as ring] - [reitit.coercion :as coercion] - #?@(:clj [ - [jsonista.core :as j]]))) + [reitit.coercion :as coercion])) -(s/def ::id keyword?) +(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?) @@ -25,16 +21,11 @@ documentation keys for the route data. Should be accompanied by a [[swagger-spec-handler]] to expose the swagger spec. - Swagger-specific keys: - - | key | description | - | --------------|-------------| - | :swagger | map of any swagger-data. Must have `:id` to identify the api - - The following common keys also contribute to swagger spec: + New route data keys contributing to swagger docs: | key | description | | --------------|-------------| + | :swagger | map of any swagger-data. Must have `:id` (keyword or sequence of keywords) to identify the api | :no-doc | optional boolean to exclude endpoint from api docs | :tags | optional set of strings of keywords tags for an endpoint api docs | :summary | optional short string summary of an endpoint @@ -42,6 +33,8 @@ Also the coercion keys contribute to swagger spec: + | key | description | + | --------------|-------------| | :parameters | optional input parameters for a route, in a format defined by the coercion | :responses | optional descriptions of responess, in a format defined by coercion @@ -70,12 +63,16 @@ :spec ::spec}) (defn create-swagger-handler [] - "Create a ring handler to emit swagger spec." + "Create a ring handler to emit swagger spec. Collects all routes from router which have + an intersecting `[:swagger :id]` and which are not marked with `:no-doc` route data." (fn [{:keys [::r/router ::r/match :request-method]}] (let [{:keys [id] :as swagger} (-> match :result request-method :data :swagger) - swagger (->> (set/rename-keys swagger {:id :x-id}) - (merge {:swagger "2.0"})) - accept-route #(-> % second :swagger :id (= id)) + ->set (fn [x] (if (or (set? x) (sequential? x)) (set x) (conj #{} x))) + ids (->set id) + swagger (->> (dissoc swagger :id) + (merge {:swagger "2.0" + :x-id ids})) + accept-route #(-> % second :swagger :id ->set (set/intersection ids) seq) transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data}]] (if (and data (not no-doc)) [method @@ -91,36 +88,3 @@ (let [paths (->> router (r/routes) (filter accept-route) (map transform-path) (into {}))] {:status 200 :body (meta-merge swagger {:paths paths})}))))) - -#?(:clj - (defn create-swagger-ui-handler - "Creates a ring handler which can be used to serve swagger-ui. - - | key | description | - | -----------------|-------------| - | :parameter | optional name of the wildcard parameter, defaults to unnamed keyword `:` - | :root | optional resource root, defaults to `\"swagger-ui\"` - | :url | path to swagger endpoint, defaults to `/swagger.json` - | :path | optional path to mount the handler to. Works only if mounted outside of a router. - | :config | parameters passed to swaggger-ui, keys transformed into camelCase." - ([] - (create-swagger-ui-handler nil)) - ([options] - (let [mixed-case (fn [k] - (let [[f & rest] (str/split (name k) #"-")] - (apply str (str/lower-case f) (map str/capitalize rest)))) - mixed-case-key (fn [[k v]] [(mixed-case k) v]) - config-json (fn [{:keys [url config]}] (j/write-value-as-string (merge config {:url url}))) - conf-js (fn [opts] (str "window.API_CONF = " (config-json opts) ";")) - options (as-> options $ - (update $ :root (fnil identity "swagger-ui")) - (update $ :url (fnil identity "/swagger.json")) - (update $ :config #(->> % (map mixed-case-key) (into {}))) - (assoc $ :paths {"conf.js" {:headers {"Content-Type" "application/javascript"} - :status 200 - :body (conf-js $)} - "config.json" {:headers {"Content-Type" "application/json"} - :status 200 - :body (config-json $)}}))] - (ring/routes - (ring/create-resource-handler options)))))) diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj index 9263d98b..21b5b664 100644 --- a/modules/reitit/project.clj +++ b/modules/reitit/project.clj @@ -10,4 +10,5 @@ [metosin/reitit-ring] [metosin/reitit-spec] [metosin/reitit-schema] - [metosin/reitit-swagger]]) + [metosin/reitit-swagger] + [metosin/reitit-swagger-ui]]) diff --git a/project.clj b/project.clj index 01948ad4..8d1b12bd 100644 --- a/project.clj +++ b/project.clj @@ -15,6 +15,7 @@ [metosin/reitit-spec "0.1.1-SNAPSHOT"] [metosin/reitit-schema "0.1.1-SNAPSHOT"] [metosin/reitit-swagger "0.1.1-SNAPSHOT"] + [metosin/reitit-swagger-ui "0.1.1-SNAPSHOT"] [meta-merge "1.0.0"] [ring/ring-core "1.6.3"] @@ -38,7 +39,8 @@ "modules/reitit-ring/src" "modules/reitit-spec/src" "modules/reitit-schema/src" - "modules/reitit-swagger/src"] + "modules/reitit-swagger/src" + "modules/reitit-swagger-ui/src"] :dependencies [[org.clojure/clojure "1.9.0"] [org.clojure/clojurescript "1.10.238"] diff --git a/scripts/lein-modules b/scripts/lein-modules index e93284b9..daf815fe 100755 --- a/scripts/lein-modules +++ b/scripts/lein-modules @@ -3,6 +3,6 @@ set -e # Modules -for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit; do +for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit-swagger-ui reitit; do cd modules/$ext; lein "$@"; cd ../..; done diff --git a/test/cljc/reitit/ring_coercion_test.cljc b/test/cljc/reitit/ring_coercion_test.cljc index a3e708ea..11eb5141 100644 --- a/test/cljc/reitit/ring_coercion_test.cljc +++ b/test/cljc/reitit/ring_coercion_test.cljc @@ -84,11 +84,11 @@ (app valid-request)))) (testing "invalid request" - (let [{:keys [status body]} (app invalid-request)] + (let [{:keys [status]} (app invalid-request)] (is (= 400 status)))) (testing "invalid response" - (let [{:keys [status body]} (app invalid-request2)] + (let [{:keys [status]} (app invalid-request2)] (is (= 500 status)))))))) (deftest schema-coercion-test diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj index d9eb412f..6745be01 100644 --- a/test/cljc/reitit/swagger_test.clj +++ b/test/cljc/reitit/swagger_test.clj @@ -98,3 +98,33 @@ :type "object"}}} :summary "plus"}}}} spec))))) + +(deftest multiple-swagger-apis-test + (let [ping-route ["/ping" {:get (constantly "ping")}] + spec-route ["/swagger.json" + {:get {:no-doc true + :handler (swagger/create-swagger-handler)}}] + app (ring/ring-handler + (ring/router + [["/common" {:swagger {:id #{::one ::two}}} + ping-route] + + ["/one" {:swagger {:id ::one}} + ping-route + spec-route] + + ["/two" {:swagger {:id ::two}} + ping-route + spec-route + ["/deep" {:swagger {:id ::one}} + ping-route]] + ["/one-two" {:swagger {:id #{::one ::two}}} + spec-route]])) + spec-paths (fn [uri] + (-> {:request-method :get, :uri uri} app :body :paths keys))] + (is (= ["/common/ping" "/one/ping" "/two/deep/ping"] + (spec-paths "/one/swagger.json"))) + (is (= ["/common/ping" "/two/ping"] + (spec-paths "/two/swagger.json"))) + (is (= ["/common/ping" "/one/ping" "/two/ping" "/two/deep/ping"] + (spec-paths "/one-two/swagger.json")))))