-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implementing the helpers of Swagger Data generation. * Implementing the core functionality of swagger data generetion. * Writing base tests for swagger data generation. * Clearing the test file from dev expressions. * Fix on json sym -> keyw * Add Swagger config keys. * Update and re-factor Swagger implementation. * Fix styling with a new-line. * Improve functionality with arbitrary compilation map of routes. * Kondo fixes. * Fix on Styling. * Fix over render? flag. * Removing the logic from it since it's not the right place now. * Multiple fixes and changes. Track on PR. * Fix on tests. The var itself moved up. * Providing new swagger/ui values on config. * A new dependency, hiccup added. * Re-factore the Swagger/UI implementation on Xiana; * Providing swagger-ui as a config generated content, implicitly. * Providing swagger/ui end-points implicitly. * New API over Swagger functionality. * Removing left-over require. * Formatted internally. * Update swagger.clj Removing unwanted, development s-expressions from the PR. * Changes from re-factoring session with Core team. * Fixing the re-factored function usage. * Fix bugs WIP * Refactor nonsense code * Clean ->swagger-data function * Clean routes->swagger-data function * Refactor routes->swagger-data * Add new docstrings * Fix clj-kondo * Move swagger tests to proper namespace * Fix code to pass tests * Remove unused namespace * Change functions name and refactor code * Cleanup swagger.json response function * cleanup names * Fix swagger * Apply cljstyle to tests * Apply cljstyle to route_test * Add missing reitit.swagger namespace * Update Readme Co-authored-by: Gustavo Valente <gustavo.valente@flexiana.com> --------- Co-authored-by: Gustavo Valente <gustavomsvalente@gmail.com> Co-authored-by: iain <iain@soulflyer.co.uk> Co-authored-by: Gustavo Valente <gustavo.valente@flexiana.com>
- Loading branch information
1 parent
a8edb93
commit 080d0e2
Showing
7 changed files
with
350 additions
and
2 deletions.
There are no files selected for viewing
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
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
(ns xiana.swagger | ||
(:require | ||
[clojure.string :as str] | ||
[hiccup.core :as h] | ||
[jsonista.core :as json] | ||
[malli.util] | ||
[meta-merge.core :refer [meta-merge]] | ||
[reitit.coercion :as coercion] | ||
[reitit.coercion.malli] | ||
[reitit.core :as r] | ||
[reitit.ring :as ring] | ||
[reitit.swagger] | ||
[reitit.trie :as trie] | ||
[ring.util.response])) | ||
|
||
(def all-methods | ||
[:get :patch :trace :connect :delete :head :post :options :put]) | ||
|
||
(defn xiana-route->reitit-route | ||
"xiana-route->reitit-route is taking route entry of our custom shape of routes | ||
and transforms it into proper reitit route entry that is valid on the Swagger | ||
implemention of reitit. | ||
(xiana-route->reitit-route [\"/swagger-ui\" {:action :swagger-ui | ||
:some-values true}]) | ||
;; => [\"/swagger-ui\" | ||
{:get | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:patch | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:trace | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:connect | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:delete | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:head | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:post | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:action :swagger-ui, | ||
:options | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:put | ||
{:handler #function[clojure.core/identity], :action :swagger-ui}, | ||
:some-values true}] | ||
" | ||
[[url opt-map & nested-routes :as route] all-methods] | ||
(let [new-opt-map (if (:action opt-map) | ||
(let [action' (:action opt-map) | ||
swagger-base-of-endpoint (:swagger-* opt-map)] | ||
(reduce (fn [acc method] | ||
(-> acc | ||
(assoc-in [method :handler] identity) | ||
(assoc-in [method :action] action') | ||
(merge swagger-base-of-endpoint))) | ||
opt-map | ||
all-methods)) | ||
(let [swagger-base-of-endpoint (get opt-map :swagger-* {})] | ||
(reduce (fn [acc method] | ||
(if (get acc method) | ||
(if (get-in acc [method :handler]) | ||
acc | ||
(-> acc | ||
(assoc-in [method :handler] identity) | ||
(merge swagger-base-of-endpoint))) | ||
acc)) | ||
opt-map | ||
all-methods)))] | ||
(if (-> route meta :no-doc) | ||
nil | ||
(apply conj [url new-opt-map] | ||
(map #(xiana-route->reitit-route % all-methods) nested-routes))))) | ||
|
||
(defn xiana-routes->reitit-routes | ||
"Transforms routes to the proper reitit form." | ||
[routes all-methods] | ||
(vec | ||
(keep #(xiana-route->reitit-route % all-methods) routes))) | ||
|
||
(defn strip-top-level-keys | ||
[m] | ||
(dissoc m :id :info :host :basePath :definitions :securityDefinitions)) | ||
|
||
(def base-swagger-spec {:responses ^:displace {:default {:description ""}}}) | ||
|
||
(defn transform-endpoint | ||
[[method {{:keys [coercion no-doc swagger] :as data} :data | ||
middleware :middleware | ||
interceptors :interceptors}]] | ||
(when (and data (not no-doc)) | ||
[method | ||
(meta-merge | ||
base-swagger-spec | ||
(apply meta-merge (keep (comp :swagger :data) middleware)) | ||
(apply meta-merge (keep (comp :swagger :data) interceptors)) | ||
(when coercion | ||
(coercion/get-apidocs coercion :swagger data)) | ||
(select-keys data [:tags :summary :description]) | ||
(strip-top-level-keys swagger))])) | ||
|
||
(defn swagger-path | ||
[path opts] | ||
(-> path (trie/normalize opts) (str/replace #"\{\*" "{"))) | ||
|
||
(defn transform-path | ||
"Transform a path of a compiled route to swagger format." | ||
[[path _ api-verb-map] router] | ||
(when-let [endpoint (some->> api-verb-map (keep transform-endpoint) (seq) (into {}))] | ||
[(swagger-path path (r/options router)) endpoint])) | ||
|
||
(defn routes->swagger-map | ||
"Creates the json representation of the routes " | ||
[routes & {route-opt-map :route-opt-map}] | ||
(let [router (ring/router routes (or route-opt-map {})) | ||
swagger {:swagger "2.0" | ||
:x-id ::default} | ||
map-in-order #(->> % (apply concat) (apply array-map)) | ||
paths (->> router | ||
(r/compiled-routes) | ||
(map #(transform-path % router)) | ||
map-in-order)] | ||
(meta-merge swagger {:paths paths}))) | ||
|
||
#_(-> (config/config {:framework-edn-config "config/dev/config.edn"}) | ||
->default-internal-swagger-ui-html) | ||
|
||
(defn ->default-internal-swagger-ui-html | ||
"Generate the html for swagger UI" | ||
[config] | ||
(let [schema-protocol (get-in config [:deps :xiana/web-server :protocol] :http) | ||
swagger-json-uri-path (get-in config [:deps :xiana/swagger :uri-path])] | ||
(h/html [:html {:lang "en"} | ||
[:head | ||
[:meta {:charset "UTF-8"}] | ||
[:title "Swagger UI"] | ||
[:link | ||
{:referrerpolicy "no-referrer", | ||
:crossorigin "anonymous", | ||
:integrity | ||
"sha512-lfbw/3iTOqI2s3gVb0fIwex5Y9WpcFM8Oq6XMpD8R5jMjOgzIgXjDNg7mNqbWS1I6qqC7sFaaMHXNsnVstkQYQ==", | ||
:href | ||
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.4/swagger-ui.min.css", | ||
:rel "stylesheet"}] | ||
[:style | ||
"html {box-sizing: border-box; overflow: -moz-scrollbars-vertical; overflow-y: scroll;} | ||
*, *:before, *:after { box-sizing: inherit;} | ||
body {margin: 0; background: #fafafa;}"] | ||
[:link | ||
{:sizes "32x32", | ||
:href "./favicon-32x32.png", | ||
:type "image/png", | ||
:rel "icon"}] | ||
[:link | ||
{:sizes "16x16", | ||
:href "./favicon-16x16.png", | ||
:type "image/png", | ||
:rel "icon"}]] | ||
[:body | ||
[:div#swagger-ui] | ||
[:script | ||
{:referrerpolicy "no-referrer", | ||
:crossorigin "anonymous", | ||
:integrity | ||
"sha512-w+D7rGMfhW/r7/lGU7mu92gjvuo4ZQddFOm5iJ0EAQNS7mmhCb10I8GcgrGTr1zJvCYcxj4roHMo66sLNQOgqA==", | ||
:src | ||
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.4/swagger-ui-bundle.min.js"}] | ||
[:script | ||
{:referrerpolicy "no-referrer", | ||
:crossorigin "anonymous", | ||
:integrity | ||
"sha512-OdiS0y42zD5WmBnJ6H8K3SCYjAjIJQrUOAraBx5PH1QSLtq+KNLy80uQKruXCJTGZKdQ7hhu/AD+WC+wuYUS+w==", | ||
:src | ||
"https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/3.52.4/swagger-ui-standalone-preset.min.js"}] | ||
[:script | ||
(str "window.onload = function () | ||
{ | ||
// TODO: [@LeaveNhA] can be replace-able with in-app configuration and pass it with json-encoding | ||
window.ui = SwaggerUIBundle( | ||
{ | ||
url: '" swagger-json-uri-path "', | ||
schemes: ['" (name schema-protocol) "'], | ||
dom_id: '#swagger-ui', | ||
deepLinking: true, | ||
presets: [SwaggerUIBundle.presets.apis, | ||
SwaggerUIStandalonePreset], | ||
plugins: [SwaggerUIBundle.plugins.DownloadUrl], | ||
layout: 'StandaloneLayout'} | ||
); | ||
};")]]]))) | ||
|
||
(defn- swagger-ui-endpoint | ||
[config] | ||
(let [{:keys [uri-path]} (get-in config [:xiana/swagger-ui])] | ||
^{:no-doc true} | ||
[uri-path | ||
{:get {:action | ||
(fn [state] | ||
(assoc state | ||
:response | ||
(-> state | ||
->default-internal-swagger-ui-html | ||
ring.util.response/response | ||
(ring.util.response/header "Content-Type" "text/html; charset=utf-8"))))}}])) | ||
|
||
(defn swagger-json-endpoint-action | ||
[state] | ||
(assoc state | ||
:response | ||
(-> (str (-> state :deps :swagger.json)) | ||
ring.util.response/response | ||
(ring.util.response/header "Content-Type" "application/json; charset=utf-8")))) | ||
|
||
(defn- swagger-json-endpoint | ||
[config] | ||
(let [{:keys [uri-path]} (get-in config [:xiana/swagger])] | ||
^{:no-doc true} | ||
[uri-path | ||
{:action swagger-json-endpoint-action}])) | ||
|
||
(defn swagger-dot-json | ||
"Create swagger.json for all methods for each endpoint" | ||
[routes & {type :type | ||
route-opt-map :route-opt-map}] | ||
(let [reitit-routes (xiana-routes->reitit-routes routes all-methods) | ||
swagger-map (routes->swagger-map reitit-routes :route-opt-map route-opt-map)] | ||
(cond | ||
(= type :json) (json/write-value-as-string swagger-map) | ||
(= type :edn) swagger-map))) | ||
|
||
(defn swagger-config? | ||
"Checks if the config has the required keys for swagger functionality. | ||
Required keys: | ||
* :xiana/swagger | ||
* :xiana/swagger-ui" | ||
[config] | ||
(every? some? ((juxt :xiana/swagger-ui :xiana/swagger) config))) | ||
|
||
(defn add-swagger-endpoints | ||
"Takes the config and returns it with the swagger endpoints added" | ||
[config] | ||
(let [type :json | ||
config (update-in config [:xiana/swagger :data] eval) | ||
route-opt-map {:data (get-in config [:xiana/swagger :data])} | ||
config (assoc-in config [:xiana/swagger :data] route-opt-map)] | ||
(if (swagger-config? config) | ||
(let [routes (get config :routes) | ||
swagger-routes (apply conj routes [(swagger-ui-endpoint config) (swagger-json-endpoint config)]) | ||
json-routes (swagger-dot-json swagger-routes | ||
:type type | ||
:route-opt-map route-opt-map)] | ||
(-> config | ||
(assoc :swagger.json json-routes) | ||
(assoc :routes swagger-routes))) | ||
config))) |
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
(ns xiana.swagger-test | ||
(:require | ||
[clojure.test :as t :refer [deftest is testing]] | ||
[xiana.swagger :as sut])) | ||
|
||
(def sample-routes | ||
"Sample routes structure." | ||
{:routes [["/" {:action :action}]]}) | ||
|
||
(def sample-routes-with-handler | ||
"Sample routes structure." | ||
{:routes [["/" {:handler :handler}]]}) | ||
|
||
(def sample-routes-without-action | ||
"Sample routes structure (without action or handler)." | ||
{:routes [["/" {}]]}) | ||
|
||
(deftest swagger-route-data-generation | ||
(testing "Swagger Data generation from Routes\n" | ||
(testing "Swagger Data from Empty Route" | ||
(let [generated (-> [] | ||
:routes | ||
(sut/swagger-dot-json :type :edn) | ||
:paths) | ||
count-of-generated-routes-data (count generated)] | ||
(is generated) | ||
(is (zero? count-of-generated-routes-data)))) | ||
|
||
(testing "Swagger Data from Sample Route /w handle\n" | ||
(let [generated-swagger-data (-> sample-routes-with-handler | ||
:routes | ||
(sut/swagger-dot-json :type :edn))] | ||
(testing "One swagger route for one route entry?" | ||
(let [generated-route-count (-> generated-swagger-data | ||
:paths | ||
count)] | ||
(is (= generated-route-count 1)))) | ||
(testing "Actions should generate every methods" | ||
(let [index-generated-methods-by-sample (-> | ||
generated-swagger-data | ||
:paths | ||
(get "/") | ||
keys | ||
set)] | ||
(is (= index-generated-methods-by-sample | ||
(set sut/all-methods))))))) | ||
|
||
(testing "Swagger Data from Sample Route /w action" | ||
(let [generated-swagger-data (-> sample-routes | ||
:routes | ||
(sut/swagger-dot-json :type :edn))] | ||
(testing "One swagger route for one route entry?" | ||
(let [generated-route-count (-> | ||
generated-swagger-data | ||
:paths | ||
keys | ||
count)] | ||
(is (= generated-route-count | ||
1)))) | ||
(testing "Actions should generate every methods" | ||
(let [index-generated-methods-by-sample (-> | ||
generated-swagger-data | ||
:paths | ||
(get "/") | ||
keys | ||
set)] | ||
(is (= | ||
index-generated-methods-by-sample | ||
(set sut/all-methods))))))))) |