Skip to content
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

Swagger Integration #240

Merged
merged 41 commits into from Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
f1d0212
Implementing the helpers of Swagger Data generation.
LeaveNhA Oct 19, 2022
aa01c18
Implementing the core functionality of swagger data generetion.
LeaveNhA Oct 19, 2022
1704915
Writing base tests for swagger data generation.
LeaveNhA Oct 19, 2022
b185a5f
Clearing the test file from dev expressions.
LeaveNhA Oct 19, 2022
4cc1ac6
Fix on json sym -> keyw
LeaveNhA Oct 19, 2022
9b8a981
Add Swagger config keys.
LeaveNhA Oct 20, 2022
79631b6
Update and re-factor Swagger implementation.
LeaveNhA Oct 20, 2022
81b407a
Fix styling with a new-line.
LeaveNhA Oct 20, 2022
8985777
Improve functionality with arbitrary compilation map of routes.
LeaveNhA Oct 24, 2022
c0c2f6d
Kondo fixes.
LeaveNhA Oct 25, 2022
fd94310
Fix on Styling.
LeaveNhA Oct 25, 2022
f380b25
Fix over render? flag.
LeaveNhA Oct 25, 2022
95650c9
Removing the logic from it since it's not the right place now.
LeaveNhA Oct 25, 2022
833ca1e
Multiple fixes and changes. Track on PR.
LeaveNhA Oct 25, 2022
a5d952b
Fix on tests.
LeaveNhA Oct 25, 2022
b31b8c4
Providing new swagger/ui values on config.
LeaveNhA Oct 27, 2022
5a096cb
A new dependency, hiccup added.
LeaveNhA Oct 27, 2022
05857bc
Re-factore the Swagger/UI implementation on Xiana;
LeaveNhA Oct 27, 2022
e796a9a
Removing left-over require.
LeaveNhA Oct 27, 2022
e7fc386
Formatted internally.
LeaveNhA Oct 27, 2022
2e24cd5
Update swagger.clj
LeaveNhA Nov 15, 2022
d5075f4
Changes from re-factoring session with Core team.
LeaveNhA Jan 20, 2023
7343fe7
Fixing the re-factored function usage.
LeaveNhA Jan 20, 2023
5719018
Fix bugs WIP
gmsvalente Sep 6, 2023
f6f8e60
Refactor nonsense code
gmsvalente Sep 7, 2023
b17e9f8
Clean ->swagger-data function
gmsvalente Sep 13, 2023
fff1388
Clean routes->swagger-data function
gmsvalente Sep 14, 2023
1318c6e
Refactor routes->swagger-data
gmsvalente Sep 14, 2023
15d65f9
Add new docstrings
gmsvalente Sep 27, 2023
79f91ba
Fix clj-kondo
gmsvalente Sep 27, 2023
0b90796
Move swagger tests to proper namespace
gmsvalente Sep 29, 2023
06579a2
Fix code to pass tests
gmsvalente Sep 29, 2023
0d74665
Remove unused namespace
gmsvalente Sep 29, 2023
7d2f9db
Change functions name and refactor code
gmsvalente Oct 5, 2023
b7a79eb
Cleanup swagger.json response function
gmsvalente Oct 6, 2023
69a32a3
cleanup names
soulflyer Oct 10, 2023
b9366dc
Fix swagger
gmsvalente Oct 18, 2023
7aff810
Apply cljstyle to tests
gmsvalente Oct 18, 2023
12b4c1c
Apply cljstyle to route_test
gmsvalente Oct 18, 2023
44af66d
Add missing reitit.swagger namespace
gmsvalente Oct 19, 2023
891f658
Update Readme
soulflyer Oct 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Expand Up @@ -16,6 +16,9 @@ Xiana has its own Leiningen template, so you can create a skeleton project with
```shell
lein new xiana app
```

It also has a deps.edn template. Instructions for using it are [here](https://github.com/Flexiana/templates)

[getting-started](./doc/getting-started.md) explains how to use this to create a very simple app with a db, a backend offering an API, and a frontend that displays something from the database.

### As a dependency
Expand Down
10 changes: 10 additions & 0 deletions config/dev/config.edn
Expand Up @@ -18,6 +18,16 @@

:xiana/web-server {:port 3000
:join? false}
:xiana/swagger {:uri-path "/swagger/swagger.json"
:path :swagger.json
:data {:coercion (reitit.coercion.malli/create
{:error-keys #{:coercion :in :schema :value :errors :humanized}
:compile malli.util/closed-schema
:strip-extra-keys true
:default-values true
:options nil})
:middleware [reitit.swagger/swagger-feature]}}
:xiana/swagger-ui {:uri-path "/swagger/swagger-ui"}
:xiana/migration {:store :database
:migration-dir "resources/migrations"
:init-in-transaction? false
Expand Down
4 changes: 3 additions & 1 deletion deps.edn
Expand Up @@ -26,7 +26,9 @@
yogthos/config {:mvn/version "1.2.0"}
hikari-cp/hikari-cp {:mvn/version "3.0.1"}
org.slf4j/slf4j-simple {:mvn/version "2.0.7"}
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}}
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"}
ring/ring {:mvn/version "1.9.6"}
hiccup/hiccup {:mvn/version "1.0.5"}}

:aliases
{:dev
Expand Down
2 changes: 1 addition & 1 deletion src/xiana/route/helpers.clj
@@ -1,5 +1,5 @@
(ns xiana.route.helpers
"The default not found, unauthorized and action functions")
"The default not found and action functions")

(defn not-found
"Default not-found response handler helper."
Expand Down
255 changes: 255 additions & 0 deletions src/xiana/swagger.clj
@@ -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)))
9 changes: 9 additions & 0 deletions test/xiana/route_test.clj
Expand Up @@ -15,10 +15,18 @@
"Sample routes structure."
{:routes [["/" {:action :action}]]})

(def sample-routes-with-no-doc
"Sample routes structure with no-documentation meta flag."
{:routes [^{:no-doc true} ["/" {:action :action}]]})

(def sample-routes-with-handler
"Sample routes structure."
{:routes [["/" {:handler :handler}]]})

(def sample-routes-with-handler-and-no-doc
"Sample routes structure with no-documentation meta flag."
{:routes [^{:no-doc true} ["/" {:handler :handler}]]})

(def sample-routes-without-action
"Sample routes structure (without action or handler)."
{:routes [["/" {}]]})
Expand Down Expand Up @@ -89,3 +97,4 @@
expected helpers/not-found]
;; verify if action has the expected value
(is (= action expected))))

69 changes: 69 additions & 0 deletions test/xiana/swagger_test.clj
@@ -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)))))))))