diff --git a/README.md b/README.md index 1db02408..b24a7559 100644 --- a/README.md +++ b/README.md @@ -27,16 +27,17 @@ See the [full documentation](https://metosin.github.io/reitit/) for details. All bundled: ```clj -[metosin/reitit "0.1.0"] +[metosin/reitit "0.1.1-SNAPSHOT"] ``` Optionally, the parts can be required separately: ```clj -[metosin/reitit-core "0.1.0"] ; routing core -[metosin/reitit-ring "0.1.0"] ; ring-router -[metosin/reitit-spec "0.1.0"] ; spec coercion -[metosin/reitit-schema "0.1.0"] ; schema coercion +[metosin/reitit-core "0.1.1-SNAPSHOT"] ; routing core +[metosin/reitit-ring "0.1.1-SNAPSHOT"] ; ring-router +[metosin/reitit-spec "0.1.1-SNAPSHOT"] ; spec coercion +[metosin/reitit-schema "0.1.1-SNAPSHOT"] ; schema coercion +[metosin/reitit-swagger "0.1.1-SNAPSHOT"] ; swagger docs ``` ## Quick start diff --git a/doc/README.md b/doc/README.md index 4c10988a..450c7f7b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -19,16 +19,17 @@ The following higher-level routers are also available as separate modules: To use Reitit, add the following dependecy to your project: ```clj -[metosin/reitit "0.1.0"] +[metosin/reitit "0.1.1-SNAPSHOT"] ``` Optionally, the parts can be required separately: ```clj -[metosin/reitit-core "0.1.0"] ; routing core -[metosin/reitit-ring "0.1.0"] ; ring-router -[metosin/reitit-spec "0.1.0"] ; spec coercion -[metosin/reitit-schema "0.1.0"] ; schema coercion +[metosin/reitit-core "0.1.1-SNAPSHOT"] ; routing core +[metosin/reitit-ring "0.1.1-SNAPSHOT"] ; ring-router +[metosin/reitit-spec "0.1.1-SNAPSHOT"] ; spec coercion +[metosin/reitit-schema "0.1.1-SNAPSHOT"] ; schema coercion +[metosin/reitit-swagger "0.1.1-SNAPSHOT"] ; swagger docs ``` For discussions, there is a [#reitit](https://clojurians.slack.com/messages/reitit/) channel in [Clojurians slack](http://clojurians.net/). diff --git a/doc/ring/ring.md b/doc/ring/ring.md index 3b4ca9c1..f85afac7 100644 --- a/doc/ring/ring.md +++ b/doc/ring/ring.md @@ -3,7 +3,7 @@ [Ring](https://github.com/ring-clojure/ring) is a Clojure web applications library inspired by Python's WSGI and Ruby's Rack. By abstracting the details of HTTP into a simple, unified API, Ring allows web applications to be constructed of modular components that can be shared among a variety of applications, web servers, and web frameworks. ```clj -[metosin/reitit-ring "0.1.0-SNAPSHOT"] +[metosin/reitit-ring "0.1.1-SNAPSHOT"] ``` Ring-router adds support for [handlers](https://github.com/ring-clojure/ring/wiki/Concepts#handlers), [middleware](https://github.com/ring-clojure/ring/wiki/Concepts#middleware) and routing based on `:request-method`. Ring-router is created with `reitit.ring/router` function. It uses a custom route compiler, creating a optimized data structure for handling route matches, with compiled middleware chain & handlers for all request methods. It also ensures that all routes have a `:handler` defined. `reitit.ring/ring-handler` is used to create a Ring handler out of ring-router. diff --git a/examples/just-coercion-with-ring/project.clj b/examples/just-coercion-with-ring/project.clj index 77a971ee..d77b03cc 100644 --- a/examples/just-coercion-with-ring/project.clj +++ b/examples/just-coercion-with-ring/project.clj @@ -3,4 +3,4 @@ :dependencies [[org.clojure/clojure "1.9.0"] [ring "1.6.3"] [metosin/muuntaja "0.4.1"] - [metosin/reitit "0.1.0"]]) + [metosin/reitit "0.1.1-SNAPSHOT"]]) diff --git a/examples/ring-example/project.clj b/examples/ring-example/project.clj index 2a46b31e..a4783cf9 100644 --- a/examples/ring-example/project.clj +++ b/examples/ring-example/project.clj @@ -3,4 +3,4 @@ :dependencies [[org.clojure/clojure "1.9.0"] [ring "1.6.3"] [metosin/muuntaja "0.4.1"] - [metosin/reitit "0.1.0"]]) + [metosin/reitit "0.1.1-SNAPSHOT"]]) diff --git a/modules/reitit-core/project.clj b/modules/reitit-core/project.clj index 67b52b30..b49f1317 100644 --- a/modules/reitit-core/project.clj +++ b/modules/reitit-core/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-core "0.1.0" +(defproject metosin/reitit-core "0.1.1-SNAPSHOT" :description "Snappy data-driven router for Clojure(Script)" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-core/src/reitit/coercion.cljc b/modules/reitit-core/src/reitit/coercion.cljc index 4560db84..5c833dca 100644 --- a/modules/reitit-core/src/reitit/coercion.cljc +++ b/modules/reitit-core/src/reitit/coercion.cljc @@ -12,7 +12,7 @@ "Pluggable coercion protocol" (-get-name [this] "Keyword name for the coercion") (-get-options [this] "Coercion options") - (-get-apidocs [this model data] "???") + (-get-apidocs [this spesification data] "Returns api documentation") (-compile-model [this model name] "Compiles a model") (-open-model [this model] "Returns a new model which allows extra keys in maps") (-encode-error [this error] "Converts error in to a serializable format") diff --git a/modules/reitit-ring/project.clj b/modules/reitit-ring/project.clj index 7a38772c..45fb455b 100644 --- a/modules/reitit-ring/project.clj +++ b/modules/reitit-ring/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-ring "0.1.0" +(defproject metosin/reitit-ring "0.1.1-SNAPSHOT" :description "Reitit: Ring routing" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-schema/project.clj b/modules/reitit-schema/project.clj index a71566f7..4327d3de 100644 --- a/modules/reitit-schema/project.clj +++ b/modules/reitit-schema/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-schema "0.1.0" +(defproject metosin/reitit-schema "0.1.1-SNAPSHOT" :description "Reitit: Plumatic Schema coercion" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-schema/src/reitit/coercion/schema.cljc b/modules/reitit-schema/src/reitit/coercion/schema.cljc index 741f08a7..1f626886 100644 --- a/modules/reitit-schema/src/reitit/coercion/schema.cljc +++ b/modules/reitit-schema/src/reitit/coercion/schema.cljc @@ -5,7 +5,9 @@ [schema.coerce :as sc] [schema.utils :as su] [schema-tools.coerce :as stc] - [reitit.coercion :as coercion])) + [schema-tools.swagger.core :as swagger] + [reitit.coercion :as coercion] + [clojure.set :as set])) (def string-coercion-matcher stc/string-coercion-matcher) @@ -44,10 +46,28 @@ (reify coercion/Coercion (-get-name [_] :schema) (-get-options [_] opts) - (-get-apidocs [_ _ {:keys [parameters responses] :as info}] - (cond-> (dissoc info :parameters :responses) - parameters (assoc ::parameters parameters) - responses (assoc ::responses responses))) + (-get-apidocs [this spesification {:keys [parameters responses]}] + ;; TODO: this looks identical to spec, refactor when schema is done. + (condp = spesification + :swagger (swagger/swagger-spec + (merge + (if parameters + {::swagger/parameters + (into + (empty parameters) + (for [[k v] parameters] + [k (coercion/-compile-model this v nil)]))}) + (if responses + {::swagger/responses + (into + (empty responses) + (for [[k response] responses + :let [response (set/rename-keys response {:body :schema})]] + [k (update response :schema #(coercion/-compile-model this % nil))]))}))) + (throw + (ex-info + (str "Can't produce Schema apidocs for " spesification) + {:type spesification, :coercion :schema})))) (-compile-model [_ model _] model) (-open-model [_ schema] (st/open-schema schema)) (-encode-error [_ error] diff --git a/modules/reitit-spec/project.clj b/modules/reitit-spec/project.clj index 34678751..545408e9 100644 --- a/modules/reitit-spec/project.clj +++ b/modules/reitit-spec/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-spec "0.1.0" +(defproject metosin/reitit-spec "0.1.1-SNAPSHOT" :description "Reitit: clojure.spec coercion" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" diff --git a/modules/reitit-spec/src/reitit/coercion/spec.cljc b/modules/reitit-spec/src/reitit/coercion/spec.cljc index 52fb65e8..72e9dc84 100644 --- a/modules/reitit-spec/src/reitit/coercion/spec.cljc +++ b/modules/reitit-spec/src/reitit/coercion/spec.cljc @@ -4,7 +4,8 @@ [spec-tools.data-spec :as ds] [spec-tools.conform :as conform] [spec-tools.swagger.core :as swagger] - [reitit.coercion :as coercion]) + [reitit.coercion :as coercion] + [clojure.set :as set]) #?(:clj (:import (spec_tools.core Spec)))) @@ -67,20 +68,27 @@ (reify coercion/Coercion (-get-name [_] :spec) (-get-options [_] opts) - (-get-apidocs [this _ {:keys [parameters responses] :as info}] - (cond-> (dissoc info :parameters :responses) - parameters (assoc - ::swagger/parameters - (into - (empty parameters) - (for [[k v] parameters] - [k (coercion/-compile-model this v nil)]))) - responses (assoc - ::swagger/responses - (into - (empty responses) - (for [[k response] responses] - [k (update response :body #(coercion/-compile-model this % nil))]))))) + (-get-apidocs [this spesification {:keys [parameters responses]}] + (condp = spesification + :swagger (swagger/swagger-spec + (merge + (if parameters + {::swagger/parameters + (into + (empty parameters) + (for [[k v] parameters] + [k (coercion/-compile-model this v nil)]))}) + (if responses + {::swagger/responses + (into + (empty responses) + (for [[k response] responses + :let [response (set/rename-keys response {:body :schema})]] + [k (update response :schema #(coercion/-compile-model this % nil))]))}))) + (throw + (ex-info + (str "Can't produce Spec apidocs for " spesification) + {:type spesification, :coercion :spec})))) (-compile-model [_ model name] (into-spec model name)) (-open-model [_ spec] spec) diff --git a/modules/reitit-swagger/project.clj b/modules/reitit-swagger/project.clj new file mode 100644 index 00000000..384e6c5f --- /dev/null +++ b/modules/reitit-swagger/project.clj @@ -0,0 +1,9 @@ +(defproject metosin/reitit-swagger "0.1.1-SNAPSHOT" + :description "Reitit: Swagger-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-core]]) diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc new file mode 100644 index 00000000..ee232a99 --- /dev/null +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -0,0 +1,88 @@ +(ns reitit.swagger + (:require [reitit.core :as r] + [meta-merge.core :refer [meta-merge]] + [clojure.spec.alpha :as s] + [clojure.set :as set] + [reitit.coercion :as coercion])) + +(s/def ::id keyword?) +(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 ::swagger (s/keys :req-un [::id])) +(s/def ::spec (s/keys :opt-un [::swagger ::no-doc ::tags ::summary ::description])) + +(def swagger-feature + "Feature for handling swagger-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 + [[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: + + | key | description | + | --------------|-------------| + | :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 + | :description | optional long description of an endpoint. Supports http://spec.commonmark.org/ + + Also the coercion keys contribute to swagger spec: + + | :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 + + Example: + + [\"/api\" + {:swagger {:id :my-api} + :middleware [reitit.swagger/swagger-feature]} + + [\"/swagger.json\" + {:get {:no-doc true + :swagger {:info {:title \"my-api\"}} + :handler reitit.swagger/swagger-spec-handler}}] + + [\"/plus\" + {:get {: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 ::swagger + :spec ::spec}) + +(defn swagger-spec-handler + "Ring handler to emit swagger spec." + [{: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}) + this-swagger? #(-> % second :swagger :id (= id)) + transform-endpoint (fn [[method endpoint]] + (let [coercion (-> endpoint :data :coercion)] + (if (and endpoint (-> endpoint :data :no-doc not)) + [method (meta-merge + (if coercion + (coercion/-get-apidocs coercion :swagger (-> endpoint :data))) + (-> endpoint :data (select-keys [:tags :summary :description])) + (-> endpoint :data :swagger (dissoc :id)))]))) + transform-path (fn [[p _ c]] + (if-let [endpoint (some->> c (keep transform-endpoint) (seq) (into {}))] + [p endpoint]))] + (if id + (let [paths (->> router (r/routes) (filter this-swagger?) (map transform-path) (into {}))] + {:status 200 + :body (meta-merge swagger {:paths paths})})))) diff --git a/modules/reitit/project.clj b/modules/reitit/project.clj index 9b4af0af..9263d98b 100644 --- a/modules/reitit/project.clj +++ b/modules/reitit/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit "0.1.0" +(defproject metosin/reitit "0.1.1-SNAPSHOT" :description "Snappy data-driven router for Clojure(Script)" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" @@ -9,4 +9,5 @@ :dependencies [[metosin/reitit-core] [metosin/reitit-ring] [metosin/reitit-spec] - [metosin/reitit-schema]]) + [metosin/reitit-schema] + [metosin/reitit-swagger]]) diff --git a/project.clj b/project.clj index 16cbcbf3..58396d62 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject metosin/reitit-parent "0.1.0" +(defproject metosin/reitit-parent "0.1.1-SNAPSHOT" :description "Snappy data-driven router for Clojure(Script)" :url "https://github.com/metosin/reitit" :license {:name "Eclipse Public License" @@ -9,15 +9,16 @@ :source-uri "https://github.com/metosin/reitit/{version}/{filepath}#L{line}" :metadata {:doc/format :markdown}} - :managed-dependencies [[metosin/reitit "0.1.0"] - [metosin/reitit-core "0.1.0"] - [metosin/reitit-ring "0.1.0"] - [metosin/reitit-spec "0.1.0"] - [metosin/reitit-schema "0.1.0"] + :managed-dependencies [[metosin/reitit "0.1.1-SNAPSHOT"] + [metosin/reitit-core "0.1.1-SNAPSHOT"] + [metosin/reitit-ring "0.1.1-SNAPSHOT"] + [metosin/reitit-spec "0.1.1-SNAPSHOT"] + [metosin/reitit-schema "0.1.1-SNAPSHOT"] + [metosin/reitit-swagger "0.1.1-SNAPSHOT"] [meta-merge "1.0.0"] [metosin/spec-tools "0.6.1"] - [metosin/schema-tools "0.10.0"]] + [metosin/schema-tools "0.10.1-SNAPSHOT"]] :plugins [[jonase/eastwood "0.2.5"] [lein-doo "0.1.9"] @@ -33,14 +34,15 @@ "modules/reitit-core/src" "modules/reitit-ring/src" "modules/reitit-spec/src" - "modules/reitit-schema/src"] + "modules/reitit-schema/src" + "modules/reitit-swagger/src"] :dependencies [[org.clojure/clojure "1.9.0"] [org.clojure/clojurescript "1.9.946"] ;; modules dependencies [metosin/reitit] - [metosin/schema-tools "0.10.0"] + [metosin/schema-tools] [expound "0.5.0"] [orchestra "2017.11.12-1"] diff --git a/scripts/lein-modules b/scripts/lein-modules index 8ae825fd..e93284b9 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; do +for ext in reitit-core reitit-ring reitit-spec reitit-schema reitit-swagger reitit; do cd modules/$ext; lein "$@"; cd ../..; done diff --git a/scripts/set-version b/scripts/set-version index ec30ad58..c4c47b31 100755 --- a/scripts/set-version +++ b/scripts/set-version @@ -1,9 +1,8 @@ -#!/bin/sh +#!/bin/zsh ext="sedbak$$" find . -name project.clj -exec sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" '{}' \; find . -name project.clj -exec sed -i.$ext "s/defproject metosin\/reitit\(.*\) \".*\"/defproject metosin\/reitit\1 \"$1\"/g" '{}' \; -sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" doc/*.md -sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" *.md +sed -i.$ext "s/\[metosin\/reitit\(.*\) \".*\"\]/[metosin\/reitit\1 \"$1\"\]/g" **/*.md find . -name "*.$ext" -exec rm '{}' \; diff --git a/test/cljc/reitit/swagger_test.clj b/test/cljc/reitit/swagger_test.clj new file mode 100644 index 00000000..e7ee183e --- /dev/null +++ b/test/cljc/reitit/swagger_test.clj @@ -0,0 +1,81 @@ +(ns reitit.swagger-test + (:require [clojure.test :refer :all] + [reitit.ring :as ring] + [reitit.swagger :as swagger] + [reitit.ring.coercion :as rrc] + [reitit.coercion.spec :as spec] + [reitit.coercion.schema :as schema] + [schema.core :refer [Int]])) + +(def app + (ring/ring-handler + (ring/router + ["/api" + {:swagger {:id ::math}} + + ["/swagger.json" + {:get {:no-doc true + :swagger {:info {:title "my-api"}} + :handler swagger/swagger-spec-handler}}] + + ["/spec" {:coercion spec/coercion} + ["/plus" + {:get {:summary "plus" + :parameters {:query {:x int?, :y int?}} + :responses {200 {:body {:total int?}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (+ x y)}})}}]] + + ["/schema" {:coercion schema/coercion} + ["/plus" + {:get {:summary "plus" + :parameters {:query {:x Int, :y Int}} + :responses {200 {:body {:total Int}}} + :handler (fn [{{{:keys [x y]} :query} :parameters}] + {:status 200, :body {:total (+ x y)}})}}]]] + + {:data {:middleware [swagger/swagger-feature + rrc/coerce-exceptions-middleware + rrc/coerce-request-middleware + rrc/coerce-response-middleware]}}))) + +(deftest swagger-test + (testing "endpoints work" + (testing "spec" + (is (= {:body {:total 3}, :status 200} + (app + {:request-method :get + :uri "/api/spec/plus" + :query-params {:x "2", :y "1"}})))) + (testing "schema" + (is (= {:body {:total 3}, :status 200} + (app + {:request-method :get + :uri "/api/schema/plus" + :query-params {:x "2", :y "1"}}))))) + (testing "swagger-spec" + (let [spec (:body (app + {:request-method :get + :uri "/api/swagger.json"}))] + (is (= {:x-id ::math + :info {:title "my-api"} + :paths {"/api/schema/plus" {:get {:summary "plus"}} ;; TODO: implement! + "/api/spec/plus" {:get {:parameters [{:description "" + :format "int64" + :in "query" + :name "x" + :required true + :type "integer"} + {:description "" + :format "int64" + :in "query" + :name "y" + :required true + :type "integer"}] + :responses {200 {:description "" + :schema {:properties {"total" {:format "int64" + :type "integer"}} + :required ["total"] + :type "object"}}} + :summary "plus"}}}} + spec)))))