diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 946cce48..4665c66a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,7 +14,7 @@ jobs:
test:
strategy:
matrix:
- jdk: [8, 11, 17, 21]
+ jdk: [8, 11, 17, 21, 22]
name: Java ${{ matrix.jdk }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e93d4be7..bfd87589 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
See also: [compojure-api 1.1.x changelog](./CHANGELOG-1.1.x.md)
+## Next
+* Lazily load spec and schema coercion
+* bump spec-tools to 0.10.6
+ * notable changes: swagger `:name` defaults to `"body"` instead of `""` ([diff](https://github.com/metosin/spec-tools/compare/0.10.2...0.10.3))
+
## 2.0.0-alpha34-SNAPSHOT
* **BREAKING CHANGE**: `:formatter :muuntaja` sometimes required for `api{-middleware}` options
* to prepare for 1.x compatibility, :muuntaja must be explicitly configured
diff --git a/project.clj b/project.clj
index e77fe64e..e08a7716 100644
--- a/project.clj
+++ b/project.clj
@@ -12,7 +12,7 @@
[com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"]
[ring/ring-core "1.8.0"]
[compojure "1.6.1" ]
- [metosin/spec-tools "0.10.0"]
+ [metosin/spec-tools "0.10.6"]
[metosin/ring-http-response "0.9.1"]
[metosin/ring-swagger-ui "3.24.3"]
[metosin/ring-swagger "1.0.0"]
@@ -37,7 +37,6 @@
[org.clojure/core.async "0.6.532"]
[javax.servlet/javax.servlet-api "4.0.1"]
[peridot "0.5.2"]
- [com.rpl/specter "1.1.3"]
[com.stuartsierra/component "0.4.0"]
[expound "0.8.2"]
[metosin/jsonista "0.2.5"]
@@ -61,6 +60,9 @@
[org.slf4j/jul-to-slf4j "1.7.30"]
[org.slf4j/log4j-over-slf4j "1.7.30"]
[ch.qos.logback/logback-classic "1.2.3" ]]}
+ :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}
+ :1.11 {:dependencies [[org.clojure/clojure "1.11.3"]]}
+ :1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha11"]]}
:async {:jvm-opts ["-Dcompojure-api.test.async=true"]
:dependencies [[manifold "0.1.8" :exclusions [org.clojure/tools.logging]]]}}
:eastwood {:namespaces [:source-paths]
@@ -86,7 +88,7 @@
["change" "version" "leiningen.release/bump-version"]
["vcs" "commit"]
["vcs" "push"]]
- :aliases {"all" ["with-profile" "dev:dev,async"]
+ :aliases {"all" ["with-profile" "dev:dev,async:dev,1.10:dev,1.11:dev,1.12"]
"start-thingie" ["run"]
"aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"]
"test-ancient" ["test"]
diff --git a/src/compojure/api/coerce.clj b/src/compojure/api/coerce.clj
new file mode 100644
index 00000000..5a147a14
--- /dev/null
+++ b/src/compojure/api/coerce.clj
@@ -0,0 +1,67 @@
+;; 1.1.x
+(ns compojure.api.coerce
+ (:require [schema.coerce :as sc]
+ [compojure.api.middleware :as mw]
+ [compojure.api.exception :as ex]
+ [clojure.walk :as walk]
+ [schema.utils :as su]
+ [linked.core :as linked]))
+
+(defn memoized-coercer
+ "Returns a memoized version of a referentially transparent coercer fn. The
+ memoized version of the function keeps a cache of the mapping from arguments
+ to results and, when calls with the same arguments are repeated often, has
+ higher performance at the expense of higher memory use. FIFO with 10000 entries.
+ Cache will be filled if anonymous coercers are used (does not match the cache)"
+ []
+ (let [cache (atom (linked/map))
+ cache-size 10000]
+ (fn [& args]
+ (or (@cache args)
+ (let [coercer (apply sc/coercer args)]
+ (swap! cache (fn [mem]
+ (let [mem (assoc mem args coercer)]
+ (if (>= (count mem) cache-size)
+ (dissoc mem (-> mem first first))
+ mem))))
+ coercer)))))
+
+(defn cached-coercer [request]
+ (or (-> request mw/get-options :coercer) sc/coercer))
+
+(defn coerce-response! [request {:keys [status] :as response} responses]
+ (-> (when-let [schema (or (:schema (get responses status))
+ (:schema (get responses :default)))]
+ (when-let [matchers (mw/coercion-matchers request)]
+ (when-let [matcher (matchers :response)]
+ (let [coercer (cached-coercer request)
+ coerce (coercer schema matcher)
+ body (coerce (:body response))]
+ (if (su/error? body)
+ (throw (ex-info
+ (str "Response validation failed: " (su/error-val body))
+ (assoc body :type ::ex/response-validation
+ :response response)))
+ (assoc response
+ :compojure.api.meta/serializable? true
+ :body body))))))
+ (or response)))
+
+(defn body-coercer-middleware [handler responses]
+ (fn [request]
+ (coerce-response! request (handler request) responses)))
+
+(defn coerce! [schema key type request]
+ (let [value (walk/keywordize-keys (key request))]
+ (if-let [matchers (mw/coercion-matchers request)]
+ (if-let [matcher (matchers type)]
+ (let [coercer (cached-coercer request)
+ coerce (coercer schema matcher)
+ result (coerce value)]
+ (if (su/error? result)
+ (throw (ex-info
+ (str "Request validation failed: " (su/error-val result))
+ (assoc result :type ::ex/request-validation)))
+ result))
+ value)
+ value)))
diff --git a/src/compojure/api/coercion.clj b/src/compojure/api/coercion.clj
index a83a7082..0dd26b04 100644
--- a/src/compojure/api/coercion.clj
+++ b/src/compojure/api/coercion.clj
@@ -3,8 +3,9 @@
[compojure.api.exception :as ex]
[compojure.api.request :as request]
[compojure.api.coercion.core :as cc]
- [compojure.api.coercion.schema]
- [compojure.api.coercion.spec])
+ ;; side effects
+ compojure.api.coercion.register-schema
+ compojure.api.coercion.register-spec)
(:import (compojure.api.coercion.core CoercionError)))
(def default-coercion :schema)
diff --git a/src/compojure/api/coercion/register_schema.clj b/src/compojure/api/coercion/register_schema.clj
new file mode 100644
index 00000000..e1e8f993
--- /dev/null
+++ b/src/compojure/api/coercion/register_schema.clj
@@ -0,0 +1,8 @@
+(ns compojure.api.coercion.register-schema
+ (:require [compojure.api.coercion.core :as cc]))
+
+(defmethod cc/named-coercion :schema [_]
+ (deref
+ (or (resolve 'compojure.api.coercion.schema/default-coercion)
+ (do (require 'compojure.api.coercion.schema)
+ (resolve 'compojure.api.coercion.schema/default-coercion)))))
diff --git a/src/compojure/api/coercion/register_spec.clj b/src/compojure/api/coercion/register_spec.clj
new file mode 100644
index 00000000..143320fb
--- /dev/null
+++ b/src/compojure/api/coercion/register_spec.clj
@@ -0,0 +1,8 @@
+(ns compojure.api.coercion.register-spec
+ (:require [compojure.api.coercion.core :as cc]))
+
+(defmethod cc/named-coercion :spec [_]
+ (deref
+ (or (resolve 'compojure.api.coercion.spec/default-coercion)
+ (do (require 'compojure.api.coercion.spec)
+ (resolve 'compojure.api.coercion.spec/default-coercion)))))
diff --git a/src/compojure/api/coercion/schema.clj b/src/compojure/api/coercion/schema.clj
index b308d0c2..9a7e01b0 100644
--- a/src/compojure/api/coercion/schema.clj
+++ b/src/compojure/api/coercion/schema.clj
@@ -5,7 +5,9 @@
[compojure.api.coercion.core :as cc]
[clojure.walk :as walk]
[schema.core :as s]
- [compojure.api.common :as common])
+ [compojure.api.common :as common]
+ ;; side effects
+ compojure.api.coercion.register-schema)
(:import (java.io File)
(schema.core OptionalKey RequiredKey)
(schema.utils ValidationError NamedError)))
@@ -84,5 +86,3 @@
(->SchemaCoercion :schema options))
(def default-coercion (create-coercion default-options))
-
-(defmethod cc/named-coercion :schema [_] default-coercion)
diff --git a/src/compojure/api/coercion/spec.clj b/src/compojure/api/coercion/spec.clj
index 9b20481a..b5d6ad31 100644
--- a/src/compojure/api/coercion/spec.clj
+++ b/src/compojure/api/coercion/spec.clj
@@ -6,7 +6,9 @@
[clojure.walk :as walk]
[compojure.api.coercion.core :as cc]
[spec-tools.swagger.core :as swagger]
- [compojure.api.common :as common])
+ [compojure.api.common :as common]
+ ;; side effects
+ compojure.api.coercion.register-spec)
(:import (clojure.lang IPersistentMap)
(schema.core RequiredKey OptionalKey)
(spec_tools.core Spec)
@@ -149,5 +151,3 @@
(->SpecCoercion :spec options))
(def default-coercion (create-coercion default-options))
-
-(defmethod cc/named-coercion :spec [_] default-coercion)
diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj
index ebc603b2..3d6ba644 100644
--- a/src/compojure/api/middleware.clj
+++ b/src/compojure/api/middleware.clj
@@ -8,6 +8,7 @@
[ring.middleware.keyword-params :refer [wrap-keyword-params]]
[ring.middleware.nested-params :refer [wrap-nested-params]]
[ring.middleware.params :refer [wrap-params]]
+ [ring.swagger.coerce :as coerce]
[muuntaja.middleware]
[muuntaja.core :as m]
@@ -88,6 +89,12 @@
;; Options
;;
+;; 1.1.x
+(defn get-options
+ "Extracts compojure-api options from the request."
+ [request]
+ (::options request))
+
(defn wrap-inject-data
"Injects data into the request."
[handler data]
@@ -108,6 +115,20 @@
([request respond raise]
(handler (coercion/set-request-coercion request coercion) respond raise))))
+;; 1.1.x
+(def default-coercion-matchers
+ {:body coerce/json-schema-coercion-matcher
+ :string coerce/query-schema-coercion-matcher
+ :response coerce/json-schema-coercion-matcher})
+
+;; 1.1.x
+(defn coercion-matchers [request]
+ (let [options (get-options request)]
+ (if (contains? options :coercion)
+ (if-let [provider (:coercion options)]
+ (provider request))
+ default-coercion-matchers)))
+
;;
;; Muuntaja
;;
diff --git a/test-suites/compojure1/.gitignore b/test-suites/compojure1/.gitignore
new file mode 100644
index 00000000..eb5a316c
--- /dev/null
+++ b/test-suites/compojure1/.gitignore
@@ -0,0 +1 @@
+target
diff --git a/test-suites/compojure1/.lein-repl-history b/test-suites/compojure1/.lein-repl-history
new file mode 100644
index 00000000..e69de29b
diff --git a/test-suites/compojure1/project.clj b/test-suites/compojure1/project.clj
new file mode 100644
index 00000000..ca1d8bd7
--- /dev/null
+++ b/test-suites/compojure1/project.clj
@@ -0,0 +1,93 @@
+(defproject metosin/compojure-api "1.1.14-SNAPSHOT"
+ :description "Compojure Api"
+ :url "https://github.com/metosin/compojure-api"
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"
+ :distribution :repo
+ :comments "same as Clojure"}
+ :scm {:name "git"
+ :url "https://github.com/metosin/compojure-api"}
+ :source-paths ["../../src"]
+ :dependencies [[prismatic/schema "1.1.12"]
+ [prismatic/plumbing "0.5.5"]
+ [ikitommi/linked "1.3.1-alpha1"] ;; waiting for the original
+ [metosin/muuntaja "0.6.6"]
+ [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"]
+ [ring/ring-core "1.8.0"]
+ [compojure "1.6.1" ]
+ [org.clojure/core.memoize "0.8.2"]
+ [clj-commons/clj-yaml "0.7.0"]
+ [org.yaml/snakeyaml "1.24"]
+ [ring-middleware-format "0.7.4"]
+ [metosin/spec-tools "0.10.0"]
+ [metosin/ring-http-response "0.9.1"]
+ [metosin/ring-swagger-ui "3.24.3"]
+ [metosin/ring-swagger "0.26.2"]
+
+ ;; Fix dependency conflicts
+ [clj-time "0.15.2"]
+ [joda-time "2.10.5"]
+ [riddley "0.2.0"]]
+ :profiles {:uberjar {:aot :all
+ :ring {:handler examples.thingie/app}
+ :source-paths ["examples/thingie/src"]
+ :dependencies [[org.clojure/clojure "1.10.1"]
+ [http-kit "2.3.0"]
+ [reloaded.repl "0.2.4"]
+ [com.stuartsierra/component "0.4.0"]]}
+ :dev {:jvm-opts ["-Dcompojure.api.core.allow-dangerous-middleware=true"]
+ :repl-options {:init-ns user}
+ :plugins [[lein-clojars "0.9.1"]
+ [lein-midje "3.2.1"]
+ [lein-ring "0.12.0"]
+ [funcool/codeina "0.5.0"]]
+ :dependencies [[org.clojure/clojure "1.10.1"]
+ [slingshot "0.12.2"]
+ [peridot "0.5.1"]
+ [javax.servlet/servlet-api "2.5"]
+ [midje "1.9.9"]
+ [com.stuartsierra/component "0.4.0"]
+ [reloaded.repl "0.2.4"]
+ [http-kit "2.3.0"]
+ [criterium "0.4.5"]]
+ :ring {:handler examples.thingie/app
+ :reload-paths ["src" "examples/thingie/src"]}
+ :source-paths ["examples/thingie/src" "examples/thingie/dev-src"]
+ :main examples.server}
+ :perf {:jvm-opts ^:replace ["-server"
+ "-Xmx4096m"
+ "-Dclojure.compiler.direct-linking=true"]}
+ :logging {:dependencies [[org.clojure/tools.logging "0.5.0"]]}
+ :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
+ :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
+ :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}}
+ :eastwood {:namespaces [:source-paths]
+ :add-linters [:unused-namespaces]}
+ :codeina {:sources ["src"]
+ :target "gh-pages/doc"
+ :src-uri "http://github.com/metosin/compojure-api/blob/master/"
+ :src-uri-prefix "#L"}
+ :deploy-repositories [["snapshot" {:url "https://clojars.org/repo"
+ :username [:gpg :env/clojars_user]
+ :password [:gpg :env/clojars_token]
+ :sign-releases false}]
+ ["releases" {:url "https://clojars.org/repo"
+ :username [:gpg :env/clojars_user]
+ :password [:gpg :env/clojars_token]
+ :sign-releases false}]]
+ :release-tasks [["clean"]
+ ["vcs" "assert-committed"]
+ ["change" "version" "leiningen.release/bump-version" "release"]
+ ["vcs" "commit"]
+ ["vcs" "tag" "--no-sign"]
+ ["deploy" "release"]
+ ["change" "version" "leiningen.release/bump-version"]
+ ["vcs" "commit"]
+ ["vcs" "push"]]
+ :aliases {"all" ["with-profile" "dev:dev,logging:dev,1.10"]
+ "start-thingie" ["run"]
+ "aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"]
+ "test-ancient" ["midje"]
+ "perf" ["with-profile" "default,dev,perf"]
+ "deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."}
+ ["do" ["clean"] ["midje"] ["deploy" "clojars"]]})
diff --git a/test-suites/compojure1/test/compojure/api/coercion_test.clj b/test-suites/compojure1/test/compojure/api/coercion_test.clj
new file mode 100644
index 00000000..7a16f523
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/coercion_test.clj
@@ -0,0 +1,200 @@
+(ns compojure.api.coercion-test
+ (:require [compojure.api.sweet :refer :all]
+ [compojure.api.test-utils :refer :all]
+ [midje.sweet :refer :all]
+ [ring.util.http-response :refer :all]
+ [schema.core :as s]
+ [compojure.api.middleware :as mw]))
+
+(defn has-body [expected]
+ (fn [value]
+ (= (second value) expected)))
+
+(defn fails-with [expected-status]
+ (fn [[status body]]
+ (and (= status expected-status) (contains? body :errors))))
+
+(fact "response schemas"
+ (let [r-200 (GET "/" []
+ :query-params [{value :- s/Int nil}]
+ :responses {200 {:schema {:value s/Str}}}
+ (ok {:value (or value "123")}))
+ r-default (GET "/" []
+ :query-params [{value :- s/Int nil}]
+ :responses {:default {:schema {:value s/Str}}}
+ (ok {:value (or value "123")}))
+ r-200-default (GET "/" []
+ :query-params [{value :- s/Int nil}]
+ :responses {200 {:schema {:value s/Str}}
+ :default {:schema {:value s/Int}}}
+ (ok {:value (or value "123")}))]
+ (fact "200"
+ (get* (api r-200) "/") => (has-body {:value "123"})
+ (get* (api r-200) "/" {:value 123}) => (fails-with 500))
+
+ (fact ":default"
+ (get* (api r-default) "/") => (has-body {:value "123"})
+ (get* (api r-default) "/" {:value 123}) => (fails-with 500))
+
+ (fact ":default"
+ (get* (api r-200-default) "/") => (has-body {:value "123"})
+ (get* (api r-200-default) "/" {:value 123}) => (fails-with 500))))
+
+(fact "custom coercion"
+
+ (fact "response coercion"
+ (let [ping-route (GET "/ping" []
+ :return {:pong s/Str}
+ (ok {:pong 123}))]
+
+ (fact "by default, applies response coercion"
+ (let [app (api
+ ping-route)]
+ (get* app "/ping") => (fails-with 500)))
+
+ (fact "response-coercion can be disabled"
+ (fact "separately"
+ (let [app (api
+ {:coercion mw/no-response-coercion}
+ ping-route)]
+ (let [[status body] (get* app "/ping")]
+ status => 200
+ body => {:pong 123})))
+ (fact "all coercion"
+ (let [app (api
+ {:coercion nil}
+ ping-route)]
+ (let [[status body] (get* app "/ping")]
+ status => 200
+ body => {:pong 123}))))))
+
+ (fact "body coersion"
+ (let [beer-route (POST "/beer" []
+ :body [body {:beers #{(s/enum "ipa" "apa")}}]
+ (ok body))]
+
+ (fact "by default, applies body coercion (to set)"
+ (let [app (api
+ beer-route)]
+ (let [[status body] (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]}))]
+ status => 200
+ body => {:beers ["ipa" "apa"]})))
+
+ (fact "body-coercion can be disabled"
+ (let [no-body-coercion (constantly (dissoc mw/default-coercion-matchers :body))
+ app (api
+ {:coercion no-body-coercion}
+ beer-route)]
+ (let [[status body] (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]}))]
+ status => 200
+ body => {:beers ["ipa" "apa" "ipa"]}))
+ (let [app (api
+ {:coercion nil}
+ beer-route)]
+ (let [[status body] (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]}))]
+ status => 200
+ body => {:beers ["ipa" "apa" "ipa"]})))
+
+ (fact "body-coercion can be changed"
+ (let [nop-body-coercion (constantly (assoc mw/default-coercion-matchers :body (constantly nil)))
+ app (api
+ {:coercion nop-body-coercion}
+ beer-route)]
+ (post* app "/beer" (json {:beers ["ipa" "apa" "ipa"]})) => (fails-with 400)))))
+
+ (fact "query coercion"
+ (let [query-route (GET "/query" []
+ :query-params [i :- s/Int]
+ (ok {:i i}))]
+
+ (fact "by default, applies query coercion (string->int)"
+ (let [app (api
+ query-route)]
+ (let [[status body] (get* app "/query" {:i 10})]
+ status => 200
+ body => {:i 10})))
+
+ (fact "query-coercion can be disabled"
+ (let [no-query-coercion (constantly (dissoc mw/default-coercion-matchers :string))
+ app (api
+ {:coercion no-query-coercion}
+ query-route)]
+ (let [[status body] (get* app "/query" {:i 10})]
+ status => 200
+ body => {:i "10"})))
+
+ (fact "query-coercion can be changed"
+ (let [nop-query-coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
+ app (api
+ {:coercion nop-query-coercion}
+ query-route)]
+ (get* app "/query" {:i 10}) => (fails-with 400)))))
+
+ (fact "route-specific coercion"
+ (let [app (api
+ (GET "/default" []
+ :query-params [i :- s/Int]
+ (ok {:i i}))
+ (GET "/disabled-coercion" []
+ :coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
+ :query-params [i :- s/Int]
+ (ok {:i i}))
+ (GET "/no-coercion" []
+ :coercion (constantly nil)
+ :query-params [i :- s/Int]
+ (ok {:i i}))
+ (GET "/nil-coercion" []
+ :coercion nil
+ :query-params [i :- s/Int]
+ (ok {:i i})))]
+
+ (fact "default coercion"
+ (let [[status body] (get* app "/default" {:i 10})]
+ status => 200
+ body => {:i 10}))
+
+ (fact "disabled coercion"
+ (get* app "/disabled-coercion" {:i 10}) => (fails-with 400))
+
+ (fact "no coercion"
+ (let [[status body] (get* app "/no-coercion" {:i 10})]
+ status => 200
+ body => {:i "10"})
+ (let [[status body] (get* app "/nil-coercion" {:i 10})]
+ status => 200
+ body => {:i "10"})))))
+
+(facts "apiless coercion"
+
+ (fact "use default-coercion-matchers by default"
+ (let [app (context "/api" []
+ :query-params [{y :- Long 0}]
+ (GET "/ping" []
+ :query-params [x :- Long]
+ (ok [x y])))]
+ (app {:request-method :get :uri "/api/ping" :query-params {}}) => throws
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "abba"}}) => throws
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "1"}}) => (contains {:body [1 0]})
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y 2}}) => (contains {:body [1 2]})
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y "abba"}}) => throws))
+
+ (fact "coercion can be overridden"
+ (let [app (context "/api" []
+ :query-params [{y :- Long 0}]
+ (GET "/ping" []
+ :coercion (constantly nil)
+ :query-params [x :- Long]
+ (ok [x y])))]
+ (app {:request-method :get :uri "/api/ping" :query-params {}}) => throws
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "abba"}}) => (contains {:body ["abba" 0]})
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "1"}}) => (contains {:body ["1" 0]})
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y 2}}) => (contains {:body ["1" 2]})
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y "abba"}}) => throws))
+
+ (fact "context coercion is used for subroutes"
+ (let [app (context "/api" []
+ :coercion nil
+ (GET "/ping" []
+ :query-params [x :- Long]
+ (ok x)))]
+ (app {:request-method :get :uri "/api/ping" :query-params {:x "abba"}}) => (contains {:body "abba"}))))
diff --git a/test-suites/compojure1/test/compojure/api/common_test.clj b/test-suites/compojure1/test/compojure/api/common_test.clj
new file mode 100644
index 00000000..04a1a411
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/common_test.clj
@@ -0,0 +1,28 @@
+(ns compojure.api.common-test
+ (:require [compojure.api.common :as common]
+ [midje.sweet :refer :all]))
+
+(fact "group-with"
+ (common/group-with pos? [1 -10 2 -4 -1 999]) => [[1 2 999] [-10 -4 -1]]
+ (common/group-with pos? [1 2 999]) => [[1 2 999] nil])
+
+(fact "extract-parameters"
+
+ (facts "expect body"
+ (common/extract-parameters [] true) => [{} nil]
+ (common/extract-parameters [{:a 1}] true) => [{} [{:a 1}]]
+ (common/extract-parameters [:a 1] true) => [{:a 1} nil]
+ (common/extract-parameters [{:a 1} {:b 2}] true) => [{:a 1} [{:b 2}]]
+ (common/extract-parameters [:a 1 {:b 2}] true) => [{:a 1} [{:b 2}]])
+
+ (facts "don't expect body"
+ (common/extract-parameters [] false) => [{} nil]
+ (common/extract-parameters [{:a 1}] false) => [{:a 1} nil]
+ (common/extract-parameters [:a 1] false) => [{:a 1} nil]
+ (common/extract-parameters [{:a 1} {:b 2}] false) => [{:a 1} [{:b 2}]]
+ (common/extract-parameters [:a 1 {:b 2}] false) => [{:a 1} [{:b 2}]]))
+
+(fact "merge-vector"
+ (common/merge-vector nil) => nil
+ (common/merge-vector [{:a 1}]) => {:a 1}
+ (common/merge-vector [{:a 1} {:b 2}]) => {:a 1 :b 2})
diff --git a/test-suites/compojure1/test/compojure/api/compojure_perf_test.clj b/test-suites/compojure1/test/compojure/api/compojure_perf_test.clj
new file mode 100644
index 00000000..06fa9475
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/compojure_perf_test.clj
@@ -0,0 +1,126 @@
+(ns compojure.api.compojure-perf-test
+ (:require [compojure.core :as c]
+ [compojure.api.sweet :as s]
+ [criterium.core :as cc]
+ [ring.util.http-response :refer :all]))
+
+;;
+;; start repl with `lein perf repl`
+;; perf measured with the following setup:
+;;
+;; Model Name: MacBook Pro
+;; Model Identifier: MacBookPro11,3
+;; Processor Name: Intel Core i7
+;; Processor Speed: 2,5 GHz
+;; Number of Processors: 1
+;; Total Number of Cores: 4
+;; L2 Cache (per Core): 256 KB
+;; L3 Cache: 6 MB
+;; Memory: 16 GB
+;;
+
+(defn title [s]
+ (println
+ (str "\n\u001B[35m"
+ (apply str (repeat (+ 6 (count s)) "#"))
+ "\n## " s " ##\n"
+ (apply str (repeat (+ 6 (count s)) "#"))
+ "\u001B[0m\n")))
+
+(defn compojure-bench []
+
+ (let [app (c/routes
+ (c/GET "/a/b/c/1" [] "ok")
+ (c/GET "/a/b/c/2" [] "ok")
+ (c/GET "/a/b/c/3" [] "ok")
+ (c/GET "/a/b/c/4" [] "ok")
+ (c/GET "/a/b/c/5" [] "ok"))
+
+ call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+ (title "Compojure - GET flattened")
+ (assert (-> (call) :body (= "ok")))
+ (cc/quick-bench (call)))
+
+ ;; 3.8µs
+
+ (let [app (c/context "/a" []
+ (c/context "/b" []
+ (c/context "/c" []
+ (c/GET "/1" [] "ok")
+ (c/GET "/2" [] "ok")
+ (c/GET "/3" [] "ok")
+ (c/GET "/4" [] "ok")
+ (c/GET "/5" [] "ok"))
+ (c/GET "/1" [] "ok")
+ (c/GET "/2" [] "ok")
+ (c/GET "/3" [] "ok")
+ (c/GET "/4" [] "ok")
+ (c/GET "/5" [] "ok"))
+ (c/GET "/1" [] "ok")
+ (c/GET "/2" [] "ok")
+ (c/GET "/3" [] "ok")
+ (c/GET "/4" [] "ok")
+ (c/GET "/5" [] "ok"))
+
+ call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+ (title "Compojure - GET with context")
+ (assert (-> (call) :body (= "ok")))
+ (cc/quick-bench (call)))
+
+ ;; 15.9µs
+
+ )
+
+(defn compojure-api-bench []
+
+ (let [app (s/routes
+ (s/GET "/a/b/c/1" [] "ok")
+ (s/GET "/a/b/c/2" [] "ok")
+ (s/GET "/a/b/c/3" [] "ok")
+ (s/GET "/a/b/c/4" [] "ok")
+ (s/GET "/a/b/c/5" [] "ok"))
+
+ call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+ (title "Compojure API - GET flattened")
+ (assert (-> (call) :body (= "ok")))
+ (cc/quick-bench (call)))
+
+ ;; 3.8µs
+
+ (let [app (s/context "/a" []
+ (s/context "/b" []
+ (s/context "/c" []
+ (s/GET "/1" [] "ok")
+ (s/GET "/2" [] "ok")
+ (s/GET "/3" [] "ok")
+ (s/GET "/4" [] "ok")
+ (s/GET "/5" [] "ok"))
+ (s/GET "/1" [] "ok")
+ (s/GET "/2" [] "ok")
+ (s/GET "/3" [] "ok")
+ (s/GET "/4" [] "ok")
+ (s/GET "/5" [] "ok"))
+ (s/GET "/1" [] "ok")
+ (s/GET "/2" [] "ok")
+ (s/GET "/3" [] "ok")
+ (s/GET "/4" [] "ok")
+ (s/GET "/5" [] "ok"))
+
+ call #(app {:request-method :get :uri "/a/b/c/5"})]
+
+ (title "Compojure API - GET with context")
+ (assert (-> (call) :body (= "ok")))
+ (cc/quick-bench (call)))
+
+ ;; 20.0µs
+ )
+
+(defn bench []
+ (compojure-bench)
+ (compojure-api-bench))
+
+(comment
+ (bench))
diff --git a/test-suites/compojure1/test/compojure/api/exception_test.clj b/test-suites/compojure1/test/compojure/api/exception_test.clj
new file mode 100644
index 00000000..1432ddc4
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/exception_test.clj
@@ -0,0 +1,14 @@
+(ns compojure.api.exception-test
+ (:require [compojure.api.exception :refer :all]
+ [midje.sweet :refer :all]
+ [schema.core :as s])
+ (:import [schema.utils ValidationError NamedError]))
+
+(fact "stringify-error"
+ (fact "ValidationError"
+ (class (s/check s/Int "foo")) => ValidationError
+ (stringify-error (s/check s/Int "foo")) => "(not (integer? \"foo\"))"
+ (stringify-error (s/check {:foo s/Int} {:foo "foo"})) => {:foo "(not (integer? \"foo\"))"})
+ (fact "NamedError"
+ (class (s/check (s/named s/Int "name") "foo")) => NamedError
+ (stringify-error (s/check (s/named s/Int "name") "foo")) => "(named (not (integer? \"foo\")) \"name\")"))
diff --git a/test-suites/compojure1/test/compojure/api/integration_test.clj b/test-suites/compojure1/test/compojure/api/integration_test.clj
new file mode 100644
index 00000000..1c907ded
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/integration_test.clj
@@ -0,0 +1,1511 @@
+(ns compojure.api.integration-test
+ (:require [compojure.api.sweet :refer :all]
+ [compojure.api.test-utils :refer :all]
+ [compojure.api.exception :as ex]
+ [compojure.api.swagger :as swagger]
+ [midje.sweet :refer :all]
+ [ring.util.http-response :refer :all]
+ [schema.core :as s]
+ [ring.swagger.core :as rsc]
+ [ring.util.http-status :as status]
+ [compojure.api.middleware :as mw]
+ [ring.swagger.middleware :as rsm]
+ [compojure.api.validator :as validator]
+ [compojure.api.routes :as routes]
+
+ [ring.middleware.format-response :as format-response]
+ [cheshire.core :as json]))
+
+;;
+;; Data
+;;
+
+(s/defschema User {:id Long
+ :name String})
+
+(def pertti {:id 1 :name "Pertti"})
+
+(def invalid-user {:id 1 :name "Jorma" :age 50})
+
+; Headers contain extra keys, so make the schema open
+(s/defschema UserHeaders
+ (assoc User
+ s/Keyword s/Any))
+
+;;
+;; Middleware setup
+;;
+
+(def mw* "mw")
+
+(defn middleware*
+ "This middleware appends given value or 1 to a header in request and response."
+ ([handler] (middleware* handler 1))
+ ([handler value]
+ (fn [request]
+ (let [append #(str % value)
+ request (update-in request [:headers mw*] append)
+ response (handler request)]
+ (update-in response [:headers mw*] append)))))
+
+(defn constant-middleware
+ "This middleware rewrites all responses with a constant response."
+ [_ res]
+ (constantly res))
+
+(defn reply-mw*
+ "Handler which replies with response where a header contains copy
+ of the headers value from request and 7"
+ [request]
+ (-> (ok "true")
+ (header mw* (str (get-in request [:headers mw*]) "/"))))
+
+(defn middleware-x
+ "If request has query-param x, presume it's a integer and multiply it by two
+ before passing request to next handler."
+ [handler]
+ (fn [req]
+ (handler (update-in req [:query-params "x"] #(* (Integer. %) 2)))))
+
+(defn custom-validation-error-handler [ex data request]
+ (let [error-body {:custom-error (:uri request)}]
+ (case (:type data)
+ ::ex/response-validation (not-implemented error-body)
+ (bad-request error-body))))
+
+(defn custom-exception-handler [^Exception ex data request]
+ (ok {:custom-exception (str ex)}))
+
+(defn custom-error-handler [ex data request]
+ (ok {:custom-error (:data data)}))
+
+;;
+;; Facts
+;;
+
+(facts "core routes"
+
+ (fact "keyword options"
+ (let [route (GET "/ping" []
+ :return String
+ (ok "kikka"))]
+ (route {:request-method :get :uri "/ping"}) => (contains {:body "kikka"})))
+
+ (fact "map options"
+ (let [route (GET "/ping" []
+ {:return String}
+ (ok "kikka"))]
+ (route {:request-method :get :uri "/ping"}) => (contains {:body "kikka"})))
+
+ (fact "map return"
+ (let [route (GET "/ping" []
+ {:body "kikka"})]
+ (route {:request-method :get :uri "/ping"}) => (contains {:body "kikka"}))))
+
+(facts "middleware ordering"
+ (let [app (api
+ (middleware [middleware* [middleware* 2]]
+ (context "/middlewares" []
+ :middleware [(fn [handler] (middleware* handler 3)) [middleware* 4]]
+ (GET "/simple" req (reply-mw* req))
+ (middleware [#(middleware* % 5) [middleware* 6]]
+ (GET "/nested" req (reply-mw* req))
+ (GET "/nested-declared" req
+ :middleware [(fn [handler] (middleware* handler 7)) [middleware* 8]]
+ (reply-mw* req))))))]
+
+ (fact "are applied left-to-right"
+ (let [[status _ headers] (get* app "/middlewares/simple" {})]
+ status => 200
+ (get headers mw*) => "1234/4321"))
+
+ (fact "are applied left-to-right closest one first"
+ (let [[status _ headers] (get* app "/middlewares/nested" {})]
+ status => 200
+ (get headers mw*) => "123456/654321"))
+
+ (fact "are applied left-to-right for both nested & declared closest one first"
+ (let [[status _ headers] (get* app "/middlewares/nested-declared" {})]
+ status => 200
+ (get headers mw*) => "12345678/87654321"))))
+
+(facts "middleware - multiple routes"
+ (let [app (api
+ (GET "/first" []
+ (ok {:value "first"}))
+ (GET "/second" []
+ :middleware [[constant-middleware (ok {:value "foo"})]]
+ (ok {:value "second"}))
+ (GET "/third" []
+ (ok {:value "third"})))]
+ (fact "first returns first"
+ (let [[status body] (get* app "/first" {})]
+ status => 200
+ body => {:value "first"}))
+ (fact "second returns foo"
+ (let [[status body] (get* app "/second" {})]
+ status => 200
+ body => {:value "foo"}))
+ (fact "third returns third"
+ (let [[status body] (get* app "/third" {})]
+ status => 200
+ body => {:value "third"}))))
+
+(facts "middleware - editing request"
+ (let [app (api
+ (GET "/first" []
+ :query-params [x :- Long]
+ :middleware [middleware-x]
+ (ok {:value x})))]
+ (fact "middleware edits the parameter before route body"
+ (let [[status body] (get* app "/first?x=5" {})]
+ status => 200
+ body => {:value 10}))))
+
+(fact ":body, :query, :headers and :return"
+ (let [app (api
+ (context "/models" []
+ (GET "/pertti" []
+ :return User
+ (ok pertti))
+ (GET "/user" []
+ :return User
+ :query [user User]
+ (ok user))
+ (GET "/invalid-user" []
+ :return User
+ (ok invalid-user))
+ (GET "/not-validated" []
+ (ok invalid-user))
+ (POST "/user" []
+ :return User
+ :body [user User]
+ (ok user))
+ (POST "/user_list" []
+ :return [User]
+ :body [users [User]]
+ (ok users))
+ (POST "/user_set" []
+ :return #{User}
+ :body [users #{User}]
+ (ok users))
+ (POST "/user_headers" []
+ :return User
+ :headers [user UserHeaders]
+ (ok (select-keys user [:id :name])))
+ (POST "/user_legacy" {user :body-params}
+ :return User
+ (ok user))))]
+
+ (fact "GET"
+ (let [[status body] (get* app "/models/pertti")]
+ status => 200
+ body => pertti))
+
+ (fact "GET with smart destructuring"
+ (let [[status body] (get* app "/models/user" pertti)]
+ status => 200
+ body => pertti))
+
+ (fact "POST with smart destructuring"
+ (let [[status body] (post* app "/models/user" (json pertti))]
+ status => 200
+ body => pertti))
+
+ (fact "POST with smart destructuring - lists"
+ (let [[status body] (post* app "/models/user_list" (json [pertti]))]
+ status => 200
+ body => [pertti]))
+
+ (fact "POST with smart destructuring - sets"
+ (let [[status body] (post* app "/models/user_set" (json #{pertti}))]
+ status => 200
+ body => [pertti]))
+
+ (fact "POST with compojure destructuring"
+ (let [[status body] (post* app "/models/user_legacy" (json pertti))]
+ status => 200
+ body => pertti))
+
+ (fact "POST with smart destructuring - headers"
+ (let [[status body] (headers-post* app "/models/user_headers" pertti)]
+ status => 200
+ body => pertti))
+
+ (fact "Validation of returned data"
+ (let [[status] (get* app "/models/invalid-user")]
+ status => 500))
+
+ (fact "Routes without a :return parameter aren't validated"
+ (let [[status body] (get* app "/models/not-validated")]
+ status => 200
+ body => invalid-user))
+
+ (fact "Invalid json in body causes 400 with error message in json"
+ (let [[status body] (post* app "/models/user" "{INVALID}")]
+ status => 400
+ (:message body) => (contains "Unexpected character")))))
+
+(fact ":responses"
+ (fact "normal cases"
+ (let [app (api
+ (swagger-routes)
+ (GET "/lotto/:x" []
+ :path-params [x :- Long]
+ :responses {403 {:schema [String]}
+ 440 {:schema [String]}}
+ :return [Long]
+ (case x
+ 1 (ok [1])
+ 2 (ok ["two"])
+ 3 (forbidden ["error"])
+ 4 (forbidden [1])
+ (not-found {:message "not-found"}))))]
+
+ (fact "return case"
+ (let [[status body] (get* app "/lotto/1")]
+ status => 200
+ body => [1]))
+
+ (fact "return case, non-matching model"
+ (let [[status body] (get* app "/lotto/2")]
+ status => 500
+ body => (contains {:errors vector?})))
+
+ (fact "error case"
+ (let [[status body] (get* app "/lotto/3")]
+ status => 403
+ body => ["error"]))
+
+ (fact "error case, non-matching model"
+ (let [[status body] (get* app "/lotto/4")]
+ status => 500
+ body => (contains {:errors vector?})))
+
+ (fact "returning non-predefined http-status code works"
+ (let [[status body] (get* app "/lotto/5")]
+ body => {:message "not-found"}
+ status => 404))
+
+ (fact "swagger-docs for multiple returns"
+ (-> app get-spec :paths vals first :get :responses keys set))))
+
+ (fact ":responses 200 and :return"
+ (let [app (api
+ (GET "/lotto/:x" []
+ :path-params [x :- Long]
+ :return {:return String}
+ :responses {200 {:schema {:value String}}}
+ (case x
+ 1 (ok {:return "ok"})
+ 2 (ok {:value "ok"}))))]
+
+ (fact "return case"
+ (let [[status body] (get* app "/lotto/1")]
+ status => 500
+ body => (contains {:errors {:return "disallowed-key"
+ :value "missing-required-key"}})))
+
+ (fact "return case"
+ (let [[status body] (get* app "/lotto/2")]
+ status => 200
+ body => {:value "ok"}))))
+
+ (fact ":responses 200 and :return - other way around"
+ (let [app (api
+ (GET "/lotto/:x" []
+ :path-params [x :- Long]
+ :responses {200 {:schema {:value String}}}
+ :return {:return String}
+ (case x
+ 1 (ok {:return "ok"})
+ 2 (ok {:value "ok"}))))]
+
+ (fact "return case"
+ (let [[status body] (get* app "/lotto/1")]
+ status => 200
+ body => {:return "ok"}))
+
+ (fact "return case"
+ (let [[status body] (get* app "/lotto/2")]
+ status => 500
+ body => (contains {:errors {:return "missing-required-key"
+ :value "disallowed-key"}}))))))
+
+(fact ":query-params, :path-params, :header-params , :body-params and :form-params"
+ (let [app (api
+ (context "/smart" []
+ (GET "/plus" []
+ :query-params [x :- Long y :- Long]
+ (ok {:total (+ x y)}))
+ (GET "/multiply/:x/:y" []
+ :path-params [x :- Long y :- Long]
+ (ok {:total (* x y)}))
+ (GET "/power" []
+ :header-params [x :- Long y :- Long]
+ (ok {:total (long (Math/pow x y))}))
+ (POST "/minus" []
+ :body-params [x :- Long {y :- Long 1}]
+ (ok {:total (- x y)}))
+ (POST "/divide" []
+ :form-params [x :- Long y :- Long]
+ (ok {:total (/ x y)}))))]
+
+ (fact "query-parameters"
+ (let [[status body] (get* app "/smart/plus" {:x 2 :y 3})]
+ status => 200
+ body => {:total 5}))
+
+ (fact "path-parameters"
+ (let [[status body] (get* app "/smart/multiply/2/3")]
+ status => 200
+ body => {:total 6}))
+
+ (fact "header-parameters"
+ (let [[status body] (get* app "/smart/power" {} {:x 2 :y 3})]
+ status => 200
+ body => {:total 8}))
+
+ (fact "form-parameters"
+ (let [[status body] (form-post* app "/smart/divide" {:x 6 :y 3})]
+ status => 200
+ body => {:total 2}))
+
+ (fact "body-parameters"
+ (let [[status body] (post* app "/smart/minus" (json {:x 2 :y 3}))]
+ status => 200
+ body => {:total -1}))
+
+ (fact "default parameters"
+ (let [[status body] (post* app "/smart/minus" (json {:x 2}))]
+ status => 200
+ body => {:total 1}))))
+
+(fact "primitive support"
+ (let [app (api
+ {:swagger {:spec "/swagger.json"}}
+ (context "/primitives" []
+ (GET "/return-long" []
+ :return Long
+ (ok 1))
+ (GET "/long" []
+ (ok 1))
+ (GET "/return-string" []
+ :return String
+ (ok "kikka"))
+ (POST "/arrays" []
+ :return [Long]
+ :body [longs [Long]]
+ (ok longs))))]
+
+ (fact "when :return is set, longs can be returned"
+ (let [[status body] (raw-get* app "/primitives/return-long")]
+ status => 200
+ body => "1"))
+
+ (fact "when :return is not set, longs won't be encoded"
+ (let [[status body] (raw-get* app "/primitives/long")]
+ status => 200
+ body => number?))
+
+ (fact "when :return is set, raw strings can be returned"
+ (let [[status body] (raw-get* app "/primitives/return-string")]
+ status => 200
+ body => "\"kikka\""))
+
+ (fact "primitive arrays work"
+ (let [[status body] (raw-post* app "/primitives/arrays" (json/generate-string [1 2 3]))]
+ status => 200
+ body => "[1,2,3]"))
+
+ (fact "swagger-spec is valid"
+ (validator/validate app))
+
+ (fact "primitive array swagger-docs are good"
+
+ (-> app get-spec :paths (get "/primitives/arrays") :post :parameters)
+ => [{:description ""
+ :in "body"
+ :name ""
+ :required true
+ :schema {:items {:format "int64"
+ :type "integer"}
+ :type "array"}}]
+
+ (-> app get-spec :paths (get "/primitives/arrays") :post :responses :200 :schema)
+ => {:items {:format "int64",
+ :type "integer"},
+ :type "array"})))
+
+(fact "compojure destructuring support"
+ (let [app (api
+ (context "/destructuring" []
+ (GET "/regular" {{:keys [a]} :params}
+ (ok {:a a
+ :b (-> +compojure-api-request+ :params :b)}))
+ (GET "/regular2" {:as req}
+ (ok {:a (-> req :params :a)
+ :b (-> +compojure-api-request+ :params :b)}))
+ (GET "/vector" [a]
+ (ok {:a a
+ :b (-> +compojure-api-request+ :params :b)}))
+ (GET "/vector2" [:as req]
+ (ok {:a (-> req :params :a)
+ :b (-> +compojure-api-request+ :params :b)}))
+ (GET "/symbol" req
+ (ok {:a (-> req :params :a)
+ :b (-> +compojure-api-request+ :params :b)}))
+ (GET "/integrated" [a] :query-params [b]
+ (ok {:a a
+ :b b}))))]
+
+ (doseq [uri ["regular" "regular2" "vector" "vector2" "symbol" "integrated"]]
+ (fact {:midje/description uri}
+ (let [[status body] (get* app (str "/destructuring/" uri) {:a "a" :b "b"})]
+ status => 200
+ body => {:a "a" :b "b"})))))
+
+(fact "counting execution times, issue #19"
+ (let [execution-times (atom 0)
+ app (api
+ (GET "/user" []
+ :return User
+ :query [user User]
+ (swap! execution-times inc)
+ (ok user)))]
+
+ (fact "body is executed one"
+ @execution-times => 0
+ (let [[status body] (get* app "/user" pertti)]
+ status => 200
+ body => pertti)
+ @execution-times => 1)))
+
+(fact "swagger-docs"
+ (let [app (api
+ {:format {:formats [:json-kw :edn :UNKNOWN]}}
+ (swagger-routes)
+ (GET "/user" []
+ (continue)))]
+
+ (fact "api-listing shows produces & consumes for known types"
+ (get-spec app) => {:swagger "2.0"
+ :info {:title "Swagger API"
+ :version "0.0.1"}
+ :basePath "/"
+ :consumes ["application/json" "application/edn"]
+ :produces ["application/json" "application/edn"]
+ :definitions {}
+ :paths {"/user" {:get {:responses {:default {:description ""}}}}}}))
+
+ (fact "swagger-routes"
+
+ (fact "with defaults"
+ (let [app (api (swagger-routes))]
+
+ (fact "api-docs are mounted to /"
+ (let [[status body] (raw-get* app "/")]
+ status => 200
+ body => #"
Swagger UI"))
+
+ (fact "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ status => 200
+ body => (contains {:swagger "2.0"})))))
+
+ (fact "with partial overridden values"
+ (let [app (api (swagger-routes {:ui "/api-docs"
+ :data {:info {:title "Kikka"}
+ :paths {"/ping" {:get {}}}}}))]
+
+ (fact "api-docs are mounted"
+ (let [[status body] (raw-get* app "/api-docs")]
+ status => 200
+ body => #"Swagger UI"))
+
+ (fact "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ status => 200
+ body => (contains
+ {:swagger "2.0"
+ :info (contains
+ {:title "Kikka"})
+ :paths (contains
+ {(keyword "/ping") anything})}))))))
+
+ (fact "swagger via api-options"
+
+ (fact "with defaults"
+ (let [app (api)]
+
+ (fact "api-docs are not mounted"
+ (let [[status body] (raw-get* app "/")]
+ status => nil))
+
+ (fact "spec is not mounted"
+ (let [[status body] (get* app "/swagger.json")]
+ status => nil))))
+
+ (fact "with spec"
+ (let [app (api {:swagger {:spec "/swagger.json"}})]
+
+ (fact "api-docs are not mounted"
+ (let [[status body] (raw-get* app "/")]
+ status => nil))
+
+ (fact "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ status => 200
+ body => (contains {:swagger "2.0"}))))))
+
+ (fact "with ui"
+ (let [app (api {:swagger {:ui "/api-docs"}})]
+
+ (fact "api-docs are mounted"
+ (let [[status body] (raw-get* app "/api-docs")]
+ status => 200
+ body => #"Swagger UI"))
+
+ (fact "spec is not mounted"
+ (let [[status body] (get* app "/swagger.json")]
+ status => nil))))
+
+ (fact "with ui and spec"
+ (let [app (api {:swagger {:spec "/swagger.json", :ui "/api-docs"}})]
+
+ (fact "api-docs are mounted"
+ (let [[status body] (raw-get* app "/api-docs")]
+ status => 200
+ body => #"Swagger UI"))
+
+ (fact "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ status => 200
+ body => (contains {:swagger "2.0"}))))))
+
+(facts "swagger-docs with anonymous Return and Body models"
+ (let [app (api
+ (swagger-routes)
+ (POST "/echo" []
+ :return (s/either {:a String})
+ :body [_ (s/maybe {:a String})]
+ identity))]
+
+ (fact "api-docs"
+ (let [spec (get-spec app)]
+
+ (let [operation (some-> spec :paths vals first :post)
+ body-ref (some-> operation :parameters first :schema :$ref)
+ return-ref (get-in operation [:responses :200 :schema :$ref])]
+
+ (fact "generated body-param is found in Definitions"
+ (find-definition spec body-ref) => truthy)
+
+ (fact "generated return-param is found in Definitions"
+ return-ref => truthy
+ (find-definition spec body-ref) => truthy))))))
+
+(def Boundary
+ {:type (s/enum "MultiPolygon" "Polygon" "MultiPoint" "Point")
+ :coordinates [s/Any]})
+
+(def ReturnValue
+ {:boundary (s/maybe Boundary)})
+
+(facts "https://github.com/metosin/compojure-api/issues/53"
+ (let [app (api
+ (swagger-routes)
+ (POST "/" []
+ :return ReturnValue
+ :body [_ Boundary]
+ identity))]
+
+ (fact "api-docs"
+ (let [spec (get-spec app)]
+
+ (let [operation (some-> spec :paths vals first :post)
+ body-ref (some-> operation :parameters first :schema :$ref)
+ return-ref (get-in operation [:responses :200 :schema :$ref])]
+
+ (fact "generated body-param is found in Definitions"
+ (find-definition spec body-ref) => truthy)
+
+ (fact "generated return-param is found in Definitions"
+ return-ref => truthy
+ (find-definition spec body-ref) => truthy))))))
+
+(s/defschema Urho {:kaleva {:kekkonen {s/Keyword s/Any}}})
+(s/defschema Olipa {:kerran {:avaruus {s/Keyword s/Any}}})
+
+; https://github.com/metosin/compojure-api/issues/94
+(facts "preserves deeply nested schema names"
+ (let [app (api
+ (swagger-routes)
+ (POST "/" []
+ :return Urho
+ :body [_ Olipa]
+ identity))]
+
+ (fact "api-docs"
+ (let [spec (get-spec app)]
+
+ (fact "nested models are discovered correctly"
+ (-> spec :definitions keys set)
+
+ => #{:Urho :UrhoKaleva :UrhoKalevaKekkonen
+ :Olipa :OlipaKerran :OlipaKerranAvaruus})))))
+
+(fact "swagger-docs works with the :middleware"
+ (let [app (api
+ (swagger-routes)
+ (GET "/middleware" []
+ :query-params [x :- String]
+ :middleware [[constant-middleware (ok 1)]]
+ (ok 2)))]
+
+ (fact "api-docs"
+ (-> app get-spec :paths vals first)
+ => {:get {:parameters [{:description ""
+ :in "query"
+ :name "x"
+ :required true
+ :type "string"}]
+ :responses {:default {:description ""}}}})))
+
+(fact "sub-context paths"
+ (let [response {:ping "pong"}
+ ok (ok response)
+ ok? (fn [[status body]]
+ (and (= status 200)
+ (= body response)))
+ not-ok? (comp not ok?)
+ app (api
+ (swagger-routes {:ui nil})
+ (GET "/" [] ok)
+ (GET "/a" [] ok)
+ (context "/b" []
+ (context "/b1" []
+ (GET "/" [] ok))
+ (context "/" []
+ (GET "/" [] ok)
+ (GET "/b2" [] ok))))]
+
+ (fact "valid routes"
+ (get* app "/") => ok?
+ (get* app "/a") => ok?
+ (get* app "/b/b1") => ok?
+ (get* app "/b") => ok?
+ (get* app "/b/b2") => ok?)
+
+ (fact "undocumented compojure easter eggs"
+ (get* app "/b/b1/") => ok?
+ (get* app "/b/") => ok?
+ (fact "this is fixed in compojure 1.5.1"
+ (get* app "/b//") =not=> ok?))
+
+ (fact "swagger-docs have trailing slashes removed"
+ (->> app get-spec :paths keys)
+ => ["/" "/a" "/b/b1" "/b" "/b/b2"])))
+
+(fact "formats supported by ring-middleware-format"
+ (let [app (api
+ (POST "/echo" []
+ :body-params [foo :- String]
+ (ok {:foo foo})))]
+
+ (tabular
+ (facts
+ (fact {:midje/description (str ?content-type " to json")}
+ (let [[status body]
+ (raw-post* app "/echo" ?body ?content-type {:accept "application/json"})]
+ status => 200
+ body => "{\"foo\":\"bar\"}"))
+ (fact {:midje/description (str "json to " ?content-type)}
+ (let [[status body]
+ (raw-post* app "/echo" "{\"foo\":\"bar\"}" "application/json" {:accept ?content-type})]
+ status => 200
+ body => ?body)))
+
+ ?content-type ?body
+ "application/json" "{\"foo\":\"bar\"}"
+ "application/x-yaml" "{foo: bar}\n"
+ "application/edn" "{:foo \"bar\"}"
+ "application/transit+json" "[\"^ \",\"~:foo\",\"bar\"]")))
+
+(fact "multiple routes in context"
+ (let [app (api
+ (context "/foo" []
+ (GET "/bar" [] (ok ["bar"]))
+ (GET "/baz" [] (ok ["baz"]))))]
+
+ (fact "first route works"
+ (let [[status body] (get* app "/foo/bar")]
+ status => 200
+ body => ["bar"]))
+ (fact "second route works"
+ (let [[status body] (get* app "/foo/baz")]
+ status => 200
+ body => ["baz"]))))
+
+(require '[compojure.api.test-domain :refer [Pizza burger-routes]])
+
+(fact "external deep schemas"
+ (let [app (api
+ (swagger-routes)
+ burger-routes
+ (POST "/pizza" []
+ :return Pizza
+ :body [body Pizza]
+ (ok body)))]
+
+ (fact "direct route with nested named schema works when called"
+ (let [pizza {:toppings [{:name "cheese"}]}
+ [status body] (post* app "/pizza" (json pizza))]
+ status => 200
+ body => pizza))
+
+ (fact "defroute*'d route with nested named schema works when called"
+ (let [burger {:ingredients [{:name "beef"}, {:name "egg"}]}
+ [status body] (post* app "/burger" (json burger))]
+ status => 200
+ body => burger))
+
+ (fact "generates correct swagger-spec"
+ (-> app get-spec :definitions keys set) => #{:Topping :Pizza :Burger :Beef})))
+
+(fact "multiple routes with same path & method in same file"
+ (let [app (api
+ (swagger-routes)
+ (GET "/ping" []
+ :summary "active-ping"
+ (ok {:ping "active"}))
+ (GET "/ping" []
+ :summary "passive-ping"
+ (ok {:ping "passive"})))]
+
+ (fact "first route matches with Compojure"
+ (let [[status body] (get* app "/ping" {})]
+ status => 200
+ body => {:ping "active"}))
+
+ (fact "generates correct swagger-spec"
+ (-> app get-spec :paths vals first :get :summary) => "active-ping")))
+
+(fact "multiple routes with same path & method over context"
+ (let [app (api
+ (swagger-routes)
+ (context "/api" []
+ (context "/ipa" []
+ (GET "/ping" []
+ :summary "active-ping"
+ (ok {:ping "active"}))))
+ (context "/api" []
+ (context "/ipa" []
+ (GET "/ping" []
+ :summary "passive-ping"
+ (ok {:ping "passive"})))))]
+
+ (fact "first route matches with Compojure"
+ (let [[status body] (get* app "/api/ipa/ping" {})]
+ status => 200
+ body => {:ping "active"}))
+
+ (fact "generates correct swagger-spec"
+ (-> app get-spec :paths vals first :get :summary) => "active-ping")))
+
+(fact "multiple routes with same overall path (with different path sniplets & method over context"
+ (let [app (api
+ (swagger-routes)
+ (context "/api/ipa" []
+ (GET "/ping" []
+ :summary "active-ping"
+ (ok {:ping "active"})))
+ (context "/api" []
+ (context "/ipa" []
+ (GET "/ping" []
+ :summary "passive-ping"
+ (ok {:ping "passive"})))))]
+
+ (fact "first route matches with Compojure"
+ (let [[status body] (get* app "/api/ipa/ping" {})]
+ status => 200
+ body => {:ping "active"}))
+
+ (fact "generates correct swagger-spec"
+ (-> app get-spec :paths vals first :get :summary) => "active-ping")))
+
+; https://github.com/metosin/compojure-api/issues/98
+; https://github.com/metosin/compojure-api/issues/134
+(fact "basePath"
+ (let [app (api (swagger-routes))]
+
+ (fact "no context"
+ (-> app get-spec :basePath) => "/")
+
+ (fact "app-servers with given context"
+ (against-background (rsc/context anything) => "/v2")
+ (-> app get-spec :basePath) => "/v2"))
+
+ (let [app (api (swagger-routes {:data {:basePath "/serve/from/here"}}))]
+ (fact "override it"
+ (-> app get-spec :basePath) => "/serve/from/here"))
+
+ (let [app (api (swagger-routes {:data {:basePath "/"}}))]
+ (fact "can set it to the default"
+ (-> app get-spec :basePath) => "/")))
+
+(fact "multiple different models with same name"
+
+ (fact "schemas with same regexps are not equal"
+ {:d #"\D"} =not=> {:d #"\D"})
+
+ (fact "api-spec with 2 schemas with non-equal contents"
+ (let [app (api
+ (swagger-routes)
+ (GET "/" []
+ :responses {200 {:schema (s/schema-with-name {:a {:d #"\D"}} "Kikka")}
+ 201 {:schema (s/schema-with-name {:a {:d #"\D"}} "Kikka")}}
+ identity))]
+ (fact "api spec doesn't fail (#102)"
+ (get-spec app) => anything))))
+
+(def over-the-hills-and-far-away
+ (POST "/" []
+ :body-params [a :- s/Str]
+ identity))
+
+(fact "anonymous body models over defined routes"
+ (let [app (api
+ (swagger-routes)
+ over-the-hills-and-far-away)]
+ (fact "generated model doesn't have namespaced keys"
+ (-> app get-spec :definitions vals first :properties keys first) => :a)))
+
+(def foo
+ (GET "/foo" []
+ (let [foo {:foo "bar"}]
+ (ok foo))))
+
+(fact "defroutes with local symbol usage with same name (#123)"
+ (let [app (api
+ foo)]
+ (let [[status body] (get* app "/foo")]
+ status => 200
+ body => {:foo "bar"})))
+
+(def response-descriptions-routes
+ (GET "/x" []
+ :responses {500 {:schema {:code String}
+ :description "Horror"}}
+ identity))
+
+(fact "response descriptions"
+ (let [app (api
+ (swagger-routes)
+ response-descriptions-routes)]
+ (-> app get-spec :paths vals first :get :responses :500 :description) => "Horror"))
+
+(fact "exceptions options with custom validation error handler"
+ (let [app (api
+ {:exceptions {:handlers {::ex/request-validation custom-validation-error-handler
+ ::ex/request-parsing custom-validation-error-handler
+ ::ex/response-validation custom-validation-error-handler}}}
+ (swagger-routes)
+ (POST "/get-long" []
+ :body [body {:x Long}]
+ :return Long
+ (case (:x body)
+ 1 (ok 1)
+ (ok "not a number"))))]
+
+ (fact "return case, valid request & valid model"
+ (let [[status body] (post* app "/get-long" "{\"x\": 1}")]
+ status => 200
+ body => 1))
+
+ (fact "return case, not schema valid request"
+ (let [[status body] (post* app "/get-long" "{\"x\": \"1\"}")]
+ status => 400
+ body => (contains {:custom-error "/get-long"})))
+
+ (fact "return case, invalid json request"
+ (let [[status body] (post* app "/get-long" "{x: 1}")]
+ status => 400
+ body => (contains {:custom-error "/get-long"})))
+
+ (fact "return case, valid request & invalid model"
+ (let [[status body] (post* app "/get-long" "{\"x\": 2}")]
+ status => 501
+ body => (contains {:custom-error "/get-long"})))))
+
+(fact "exceptions options with custom exception and error handler"
+ (let [app (api
+ {:exceptions {:handlers {::ex/default custom-exception-handler
+ ::custom-error custom-error-handler}}}
+ (swagger-routes)
+ (GET "/some-exception" []
+ (throw (new RuntimeException)))
+ (GET "/some-error" []
+ (throw (ex-info "some ex info" {:data "some error" :type ::some-error})))
+ (GET "/specific-error" []
+ (throw (ex-info "my ex info" {:data "my error" :type ::custom-error}))))]
+
+ (fact "uses default exception handler for unknown exceptions"
+ (let [[status body] (get* app "/some-exception")]
+ status => 200
+ body => {:custom-exception "java.lang.RuntimeException"}))
+
+ (fact "uses default exception handler for unknown errors"
+ (let [[status body] (get* app "/some-error")]
+ status => 200
+ (:custom-exception body) => (contains ":data \"some error\"")))
+
+ (fact "uses specific error handler for ::custom-errors"
+ (let [[status body] (get* app "/specific-error")]
+ body => {:custom-error "my error"}))))
+
+(fact "exception handling can be disabled"
+ (let [app (api
+ {:exceptions nil}
+ (GET "/throw" []
+ (throw (new RuntimeException))))]
+ (get* app "/throw") => throws))
+
+(defn old-ex-handler [e]
+ {:status 500
+ :body {:type "unknown-exception"
+ :class (.getName (.getClass e))}})
+
+(fact "Deprecated options"
+ (facts "Old options throw assertion error"
+ (api {:validation-errors {:error-handler identity}} nil) => (throws AssertionError)
+ (api {:validation-errors {:catch-core-errors? true}} nil) => (throws AssertionError)
+ (api {:exceptions {:exception-handler identity}} nil) => (throws AssertionError))
+ (facts "Old handler functions work, with a warning"
+ (let [app (api
+ {:exceptions {:handlers {::ex/default old-ex-handler}}}
+ (GET "/" []
+ (throw (RuntimeException.))))]
+ (with-out-str
+ (let [[status body] (get* app "/")]
+ status => 500
+ body => {:type "unknown-exception"
+ :class "java.lang.RuntimeException"}))
+ (with-out-str
+ (get* app "/")) => "WARN Error-handler arity has been changed.\n")))
+
+(s/defn schema-error [a :- s/Int]
+ {:bar a})
+
+(fact "handling schema.core/error"
+ (let [app (api
+ {:exceptions {:handlers {:schema.core/error ex/schema-error-handler}}}
+ (GET "/:a" []
+ :path-params [a :- s/Str]
+ (ok (s/with-fn-validation (schema-error a)))))]
+ (let [[status body] (get* app "/foo")]
+ status => 400
+ body => (contains {:errors vector?}))))
+
+(fact "ring-swagger options"
+ (let [app (api
+ {:ring-swagger {:default-response-description-fn status/get-description}}
+ (swagger-routes)
+ (GET "/ping" []
+ :responses {500 nil}
+ identity))]
+ (-> app get-spec :paths vals first :get :responses :500 :description)
+ => "There was an internal server error."))
+
+(fact "path-for"
+ (fact "simple case"
+ (let [app (api
+ (GET "/api/pong" []
+ :name :pong
+ (ok {:pong "pong"}))
+ (GET "/api/ping" []
+ (moved-permanently (path-for :pong))))]
+ (fact "path-for works"
+ (let [[status body] (get* app "/api/ping" {})]
+ status => 200
+ body => {:pong "pong"}))))
+
+ (fact "with path parameters"
+ (let [app (api
+ (GET "/lost-in/:country/:zip" []
+ :name :lost
+ :path-params [country :- (s/enum :FI :EN), zip :- s/Int]
+ (ok {:country country
+ :zip zip}))
+ (GET "/api/ping" []
+ (moved-permanently
+ (path-for :lost {:country :FI, :zip 33200}))))]
+ (fact "path-for resolution"
+ (let [[status body] (get* app "/api/ping" {})]
+ status => 200
+ body => {:country "FI"
+ :zip 33200}))))
+
+ (fact "https://github.com/metosin/compojure-api/issues/150"
+ (let [app (api
+ (GET "/companies/:company-id/refresh" []
+ :path-params [company-id :- s/Int]
+ :name :refresh-company
+ :return String
+ (ok (path-for :refresh-company {:company-id company-id}))))]
+ (fact "path-for resolution"
+ (let [[status body] (get* app "/companies/4/refresh")]
+ status => 200
+ body => "/companies/4/refresh"))))
+
+ (fact "multiple routes with same name fail at compile-time"
+ (let [app' `(api
+ (GET "/api/pong" []
+ :name :pong
+ identity)
+ (GET "/api/ping" []
+ :name :pong
+ identity))]
+ (eval app') => (throws RuntimeException))))
+
+
+(fact "swagger-spec-path"
+ (fact "defaults to /swagger.json"
+ (let [app (api (swagger-routes))]
+ (swagger/swagger-spec-path app) => "/swagger.json"))
+ (fact "follows defined path"
+ (let [app (api (swagger-routes {:spec "/api/api-docs/swagger.json"}))]
+ (swagger/swagger-spec-path app) => "/api/api-docs/swagger.json")))
+
+(defrecord NonSwaggerRecord [data])
+
+(fact "api validation"
+
+ (fact "a swagger api with valid swagger records"
+ (let [app (api
+ (swagger-routes)
+ (GET "/ping" []
+ :return {:data s/Str}
+ (ok {:data "ping"})))]
+
+ (fact "works"
+ (let [[status body] (get* app "/ping")]
+ status => 200
+ body => {:data "ping"}))
+
+ (fact "the api is valid"
+ (validator/validate app) => app)))
+
+ (fact "a swagger api with invalid swagger records"
+ (let [app (api
+ (swagger-routes)
+ (GET "/ping" []
+ :return NonSwaggerRecord
+ (ok (->NonSwaggerRecord "ping"))))]
+
+ (fact "works"
+ (let [[status body] (get* app "/ping")]
+ status => 200
+ body => {:data "ping"}))
+
+ (fact "the api is invalid"
+ (validator/validate app)
+ => (throws
+ IllegalArgumentException
+ (str
+ "don't know how to convert class compojure.api.integration_test.NonSwaggerRecord "
+ "into a Swagger Schema. Check out ring-swagger docs for details.")))))
+
+ (fact "a non-swagger api with invalid swagger records"
+ (let [app (api
+ (GET "/ping" []
+ :return NonSwaggerRecord
+ (ok (->NonSwaggerRecord "ping"))))]
+
+ (fact "works"
+ (let [[status body] (get* app "/ping")]
+ status => 200
+ body => {:data "ping"}))
+
+ (fact "the api is valid"
+ (validator/validate app) => app))))
+
+(fact "component integration"
+ (let [system {:magic 42}]
+ (fact "via options"
+ (let [app (api
+ {:components system}
+ (GET "/magic" []
+ :components [magic]
+ (ok {:magic magic})))]
+ (let [[status body] (get* app "/magic")]
+ status => 200
+ body => {:magic 42})))
+
+ (fact "via middleware"
+ (let [handler (api
+ (GET "/magic" []
+ :components [magic]
+ (ok {:magic magic})))
+ app (mw/wrap-components handler system)]
+ (let [[status body] (get* app "/magic")]
+ status => 200
+ body => {:magic 42})))))
+
+(fact "sequential string parameters"
+ (let [app (api
+ (GET "/ints" []
+ :query-params [i :- [s/Int]]
+ (ok {:i i})))]
+ (fact "multiple values"
+ (let [[status body] (get* app "/ints?i=1&i=2&i=3")]
+ status => 200
+ body => {:i [1, 2, 3]}))
+ (fact "single value"
+ (let [[status body] (get* app "/ints?i=42")]
+ status => 200
+ body => {:i [42]}))))
+
+(fact ":swagger params just for ducumentation"
+ (fact "compile-time values"
+ (let [app (api
+ (swagger-routes)
+ (GET "/route" [q]
+ :swagger {:x-name :boolean
+ :operationId "echoBoolean"
+ :description "Ehcoes a boolean"
+ :parameters {:query {:q s/Bool}}}
+ (ok {:q q})))]
+
+ (fact "there is no coercion"
+ (let [[status body] (get* app "/route" {:q "kikka"})]
+ status => 200
+ body => {:q "kikka"}))
+
+ (fact "swagger-docs are generated"
+ (-> app get-spec :paths vals first :get)
+ => (contains
+ {:x-name "boolean"
+ :operationId "echoBoolean"
+ :description "Ehcoes a boolean"
+ :parameters [{:description ""
+ :in "query"
+ :name "q"
+ :required true
+ :type "boolean"}]}))))
+ (fact "run-time values"
+ (let [runtime-data {:x-name :boolean
+ :operationId "echoBoolean"
+ :description "Ehcoes a boolean"
+ :parameters {:query {:q s/Bool}}}
+ app (api
+ (swagger-routes)
+ (GET "/route" [q]
+ :swagger runtime-data
+ (ok {:q q})))]
+
+ (fact "there is no coercion"
+ (let [[status body] (get* app "/route" {:q "kikka"})]
+ status => 200
+ body => {:q "kikka"}))
+
+ (fact "swagger-docs are generated"
+ (-> app get-spec :paths vals first :get)
+ => (contains
+ {:x-name "boolean"
+ :operationId "echoBoolean"
+ :description "Ehcoes a boolean"
+ :parameters [{:description ""
+ :in "query"
+ :name "q"
+ :required true
+ :type "boolean"}]})))))
+
+(fact "swagger-docs via api options, #218"
+ (let [routes (routes
+ (context "/api" []
+ (GET "/ping" []
+ :summary "ping"
+ (ok {:message "pong"}))
+ (POST "/pong" []
+ :summary "pong"
+ (ok {:message "ping"})))
+ (ANY "*" []
+ (ok {:message "404"})))
+ api1 (api {:swagger {:spec "/swagger.json", :ui "/"}} routes)
+ api2 (api (swagger-routes) routes)]
+
+ (fact "both generate same swagger-spec"
+ (get-spec api1) => (get-spec api2))
+
+ (fact "not-found handler works"
+ (second (get* api1 "/missed")) => {:message "404"}
+ (second (get* api2 "/missed")) => {:message "404"})))
+
+(fact "more swagger-data can be (deep-)merged in - either via swagger-docs at runtime via mws, fixes #170"
+ (let [app (api
+ (middleware [[rsm/wrap-swagger-data {:paths {"/runtime" {:get {}}}}]]
+ (swagger-routes
+ {:data
+ {:info {:version "2.0.0"}
+ :paths {"/extra" {:get {}}}}})
+ (GET "/normal" [] (ok))))]
+ (get-spec app) => (contains
+ {:paths (just
+ {"/normal" irrelevant
+ "/extra" irrelevant
+ "/runtime" irrelevant})})))
+
+
+(s/defschema Foo {:a [s/Keyword]})
+
+(defapi with-defapi
+ (swagger-routes)
+ (GET "/foo" []
+ :return Foo
+ (ok {:a "foo"})))
+
+(defn with-api []
+ (api
+ (swagger-routes)
+ (GET "/foo" []
+ :return Foo
+ (ok {:a "foo"}))))
+
+(fact "defapi & api define same results, #159"
+ (get-spec with-defapi) => (get-spec (with-api)))
+
+(fact "coercion api change in 1.0.0 migration test"
+
+ (fact "with defaults"
+ (let [app (api
+ (GET "/ping" []
+ :return s/Bool
+ (ok 1)))]
+ (let [[status] (get* app "/ping")]
+ status => 500)))
+
+ (fact "with pre 1.0.0 syntax, api can't be created (with a nice error message)"
+ (let [app' `(api
+ {:coercion (dissoc mw/default-coercion-matchers :response)}
+ (GET "/ping" []
+ :return s/Bool
+ (ok 1)))]
+ (eval app') => (throws AssertionError)))
+
+ (fact "with post 1.0.0 syntax, works ok"
+ (let [app (api
+ {:coercion (constantly (dissoc mw/default-coercion-matchers :response))}
+ (GET "/ping" []
+ :return s/Bool
+ (ok 1)))]
+ (let [[status body] (get* app "/ping")]
+ status => 200
+ body => 1))))
+
+(fact "handling invalid routes with api"
+ (let [invalid-routes (routes (constantly nil))]
+
+ (fact "by default, logs the exception"
+ (api invalid-routes) => truthy
+ (provided
+ (compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 1))
+
+ (fact "ignoring invalid routes doesn't log"
+ (api {:api {:invalid-routes-fn nil}} invalid-routes) => truthy
+ (provided
+ (compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 0))
+
+ (fact "throwing exceptions"
+ (api {:api {:invalid-routes-fn routes/fail-on-invalid-child-routes}} invalid-routes)) => throws))
+
+(defmethod compojure.api.meta/restructure-param ::deprecated-middlewares-test [_ _ acc]
+ (assoc acc :middlewares [(constantly nil)]))
+
+(defmethod compojure.api.meta/restructure-param ::deprecated-parameters-test [_ _ acc]
+ (assoc-in acc [:parameters :parameters :query] {:a String}))
+
+(defn msg-or-cause-msg [msg-re]
+ (fn [e]
+ ;; In Clojure 1.10+, macroexpansion exceptions get wrapped in another exception.
+ ;; In that case we will look at the cause.
+ (boolean (or (re-find msg-re (.getMessage e))
+ (re-find msg-re (.getMessage (.getCause e)))))))
+
+(fact "old middlewares restructuring"
+
+ (fact ":middlewares"
+ (eval '(GET "/foo" []
+ ::deprecated-middlewares-test true
+ (ok)))
+ => (throws (msg-or-cause-msg #":middlewares is deprecated with 1.0.0, use :middleware instead.")))
+ (fact ":parameters"
+ (eval '(GET "/foo" []
+ ::deprecated-parameters-test true
+ (ok)))
+ => (throws (msg-or-cause-msg #":parameters is deprecated with 1.0.0, use :swagger instead."))))
+
+(fact "using local symbols for restructuring params"
+ (let [responses {400 {:schema {:fail s/Str}}}
+ app (api
+ {:swagger {:spec "/swagger.json"
+ :data {:info {:version "2.0.0"}}}}
+ (GET "/a" []
+ :responses responses
+ :return {:ok s/Str}
+ (ok))
+ (GET "/b" []
+ :responses (assoc responses 500 {:schema {:m s/Str}})
+ :return {:ok s/Str}
+ (ok)))
+ paths (:paths (get-spec app))]
+
+ (get-in paths ["/a" :get :responses])
+ => (just {:400 (just {:schema anything :description ""})
+ :200 (just {:schema anything :description ""})})
+
+ (get-in paths ["/b" :get :responses])
+ => (just {:400 (just {:schema anything :description ""})
+ :200 (just {:schema anything :description ""})
+ :500 (just {:schema anything :description ""})})))
+
+(fact "when functions are returned"
+ (let [wrap-mw-params (fn [handler value]
+ (fn [request]
+ (handler
+ (update request ::mw #(str % value)))))]
+ (fact "from endpoint"
+ (let [app (GET "/ping" []
+ :middleware [[wrap-mw-params "1"]]
+ :query-params [{a :- s/Str "a"}]
+ (fn [req] (str (::mw req) a)))]
+
+ (app {:request-method :get, :uri "/ping", :query-params {}}) => (contains {:body "1a"})
+ (app {:request-method :get, :uri "/ping", :query-params {:a "A"}}) => (contains {:body "1A"})))
+
+ (fact "from endpoint under context"
+ (let [app (context "/api" []
+ :middleware [[wrap-mw-params "1"]]
+ :query-params [{a :- s/Str "a"}]
+ (GET "/ping" []
+ :middleware [[wrap-mw-params "2"]]
+ :query-params [{b :- s/Str "b"}]
+ (fn [req] (str (::mw req) a b))))]
+
+ (app {:request-method :get, :uri "/api/ping", :query-params {}}) => (contains {:body "12ab"})
+ (app {:request-method :get, :uri "/api/ping", :query-params {:a "A"}}) => (contains {:body "12Ab"})
+ (app {:request-method :get, :uri "/api/ping", :query-params {:a "A", :b "B"}}) => (contains {:body "12AB"})))))
+
+(defn check-for-response-handler
+ "This response-validation handler checks for the existence of :response in its input. If it's there, it
+ returns status 200, including the value that was origingally returned. Otherwise it returns 404."
+ [^Exception e data request]
+ (if (:response data)
+ (ok {:message "Found :response in data!" :attempted-body (get-in data [:response :body])})
+ (not-found "No :response key present in data!")))
+
+(fact "response-validation handler has access to response value that failed coercion"
+ (let [incorrect-return-value {:incorrect "response"}
+ app (api
+ {:exceptions {:handlers {::ex/response-validation check-for-response-handler}}}
+ (swagger-routes)
+ (GET "/test-response" []
+ :return {:correct s/Str}
+ ; This should fail and trigger our error handler
+ (ok incorrect-return-value)))]
+
+ (fact "return case, valid request & valid model"
+ (let [[status body] (get* app "/test-response")]
+ status => 200
+ (:attempted-body body) => incorrect-return-value))))
+
+(fact "correct swagger parameter order with small number or parameters, #224"
+ (let [app (api
+ (swagger-routes)
+ (GET "/ping" []
+ :query-params [a b c d e]
+ (ok {:a a, :b b, :c c, :d d, :e e})))]
+ (fact "api works"
+ (let [[status body] (get* app "/ping" {:a "A" :b "B" :c "C" :d "D" :e "E"})]
+ status => 200
+ body => {:a "A" :b "B" :c "C" :d "D" :e "E"}))
+ (fact "swagger parameters are in correct order"
+ (-> app get-spec :paths (get "/ping") :get :parameters (->> (map (comp keyword :name)))) => [:a :b :c :d :e])))
+
+(fact "empty top-level route, #https://github.com/metosin/ring-swagger/issues/92"
+ (let [app (api
+ {:swagger {:spec "/swagger.json"}}
+ (GET "/" [] (ok {:kikka "kukka"})))]
+ (fact "api works"
+ (let [[status body] (get* app "/")]
+ status => 200
+ body => {:kikka "kukka"}))
+ (fact "swagger docs"
+ (-> app get-spec :paths keys) => ["/"])))
+
+(fact "describe works on anonymous bodys, #168"
+ (let [app (api
+ (swagger-routes)
+ (POST "/" []
+ :body [body (describe {:kikka [{:kukka String}]} "kikkas")]
+ (ok body)))]
+ (fact "description is in place"
+ (-> app get-spec :paths (get "/") :post :parameters first)
+ => (contains {:description "kikkas"}))))
+
+(facts "swagger responses headers are mapped correctly, #232"
+ (let [app (api
+ (swagger-routes)
+ (context "/resource" []
+ (resource
+ {:get {:responses {200 {:schema {:size s/Str}
+ :description "size"
+ :headers {"X-men" (describe s/Str "mutant")}}}}})))]
+ (-> app get-spec :paths vals first :get :responses :200 :headers)
+ => {:X-men {:description "mutant", :type "string"}}))
+
+(facts "api-middleware can be disabled"
+ (let [app (api
+ {:api {:disable-api-middleware? true}}
+ (swagger-routes)
+ (GET "/params" [x] (ok {:x x}))
+ (GET "/throw" [] (throw (RuntimeException. "kosh"))))]
+
+ (fact "json-parsing & wrap-params is off"
+ (let [[status body] (raw-get* app "/params" {:x 1})]
+ status => 200
+ body => {:x nil}))
+
+ (fact "exceptions are not caught"
+ (raw-get* app "/throw") => throws)))
+
+(facts "custom formats contribute to Swagger :consumes & :produces"
+ (let [custom-json (format-response/make-encoder json "application/vnd.vendor.v1+json")
+ app (api
+ {:swagger {:spec "/swagger.json"}
+ :format {:formats [custom-json :json]}}
+ (POST "/echo" []
+ :body [data {:kikka s/Str}]
+ (ok data)))]
+
+ (fact "it works"
+ (let [response (app {:uri "/echo"
+ :request-method :post
+ :body (json-stream {:kikka "kukka"})
+ :headers {"content-type" "application/vnd.vendor.v1+json"
+ "accept" "application/vnd.vendor.v1+json"}})]
+
+ (-> response :body slurp) => (json {:kikka "kukka"})
+ (-> response :headers) => (contains {"Content-Type" "application/vnd.vendor.v1+json; charset=utf-8"})))
+
+ (fact "spec is correct"
+ (get-spec app) => (contains
+ {:produces ["application/vnd.vendor.v1+json" "application/json"]
+ :consumes ["application/vnd.vendor.v1+json" "application/json"]}))))
+
+(fact "static contexts work"
+ (let [app (context "/:a" [a]
+ (GET "/:b" [b]
+ (ok [a b])))]
+ (app {:request-method :get, :uri "/a/b"}) => (contains {:body ["a" "b"]})
+ (app {:request-method :get, :uri "/a/c"}) => (contains {:body ["a" "c"]})))
diff --git a/test-suites/compojure1/test/compojure/api/meta_test.clj b/test-suites/compojure1/test/compojure/api/meta_test.clj
new file mode 100644
index 00000000..68c1a03d
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/meta_test.clj
@@ -0,0 +1,7 @@
+(ns compojure.api.meta-test
+ (:require [compojure.api.meta :refer :all]
+ [midje.sweet :refer :all]))
+
+(fact "src-coerce! with deprecated types"
+ (src-coerce! nil nil :query) => (throws AssertionError)
+ (src-coerce! nil nil :json) => (throws AssertionError))
diff --git a/test-suites/compojure1/test/compojure/api/middleware_test.clj b/test-suites/compojure1/test/compojure/api/middleware_test.clj
new file mode 100644
index 00000000..5e1fc64e
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/middleware_test.clj
@@ -0,0 +1,88 @@
+(ns compojure.api.middleware-test
+ (:require [compojure.api.middleware :refer :all]
+ [compojure.api.exception :as ex]
+ [midje.sweet :refer :all]
+ [ring.util.http-response :refer [ok]]
+ [ring.util.http-status :as status]
+ ring.util.test
+ [slingshot.slingshot :refer [throw+]])
+ (:import [java.io PrintStream ByteArrayOutputStream]))
+
+(defmacro without-err
+ "Evaluates exprs in a context in which *err* is bound to a fresh
+ StringWriter. Returns the string created by any nested printing
+ calls."
+ [& body]
+ `(let [s# (PrintStream. (ByteArrayOutputStream.))
+ err# (System/err)]
+ (System/setErr s#)
+ (try
+ ~@body
+ (finally
+ (System/setErr err#)))))
+
+(facts serializable?
+ (tabular
+ (fact
+ (serializable? nil
+ {:body ?body
+ :compojure.api.meta/serializable? ?serializable?}) => ?res)
+ ?body ?serializable? ?res
+ 5 true true
+ 5 false false
+ "foobar" true true
+ "foobar" false false
+
+ {:foobar "1"} false true
+ {:foobar "1"} true true
+ [1 2 3] false true
+ [1 2 3] true true
+
+ (ring.util.test/string-input-stream "foobar") false false))
+
+(def default-options (:exceptions api-middleware-defaults))
+
+(facts "wrap-exceptions"
+ (with-out-str
+ (without-err
+ (let [exception (RuntimeException. "kosh")
+ exception-class (.getName (.getClass exception))
+ handler (-> (fn [_] (throw exception))
+ (wrap-exceptions default-options))]
+
+ (fact "converts exceptions into safe internal server errors"
+ (handler {}) => (contains {:status status/internal-server-error
+ :body (contains {:class exception-class
+ :type "unknown-exception"})})))))
+
+ (with-out-str
+ (without-err
+ (fact "Slingshot exception map type can be matched"
+ (let [handler (-> (fn [_] (throw+ {:type ::test} (RuntimeException. "kosh")))
+ (wrap-exceptions (assoc-in default-options [:handlers ::test] (fn [ex _ _] {:status 500 :body "hello"}))))]
+ (handler {}) => (contains {:status status/internal-server-error
+ :body "hello"})))))
+
+ (without-err
+ (fact "Default handler logs exceptions to console"
+ (let [handler (-> (fn [_] (throw (RuntimeException. "kosh")))
+ (wrap-exceptions default-options))]
+ (with-out-str (handler {})) => "ERROR kosh\n")))
+
+ (without-err
+ (fact "Default request-parsing handler does not log messages"
+ (let [handler (-> (fn [_] (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} (RuntimeException. "Kosh"))))
+ (wrap-exceptions default-options))]
+ (with-out-str (handler {})) => "")))
+
+ (without-err
+ (fact "Logging can be added to a exception handler"
+ (let [handler (-> (fn [_] (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} (RuntimeException. "Kosh"))))
+ (wrap-exceptions (assoc-in default-options [:handlers ::ex/request-parsing] (ex/with-logging ex/request-parsing-handler :info))))]
+ (with-out-str (handler {})) => "INFO Error parsing request\n"))))
+
+(facts "compose-middeleware strips nils aways. #228"
+ (let [times2-mw (fn [handler]
+ (fn [request]
+ (* 2 (handler request))))]
+ (((compose-middleware [nil times2-mw nil]) (constantly 3)) anything) => 6))
diff --git a/test-suites/compojure1/test/compojure/api/perf_test.clj b/test-suites/compojure1/test/compojure/api/perf_test.clj
new file mode 100644
index 00000000..ce960b2a
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/perf_test.clj
@@ -0,0 +1,237 @@
+(ns compojure.api.perf-test
+ (:require [compojure.api.sweet :refer :all]
+ [compojure.api.test-utils :as h]
+ [criterium.core :as cc]
+ [ring.util.http-response :refer :all]
+ [schema.core :as s]
+ [clojure.java.io :as io]
+ [cheshire.core :as json]
+ [cheshire.core :as cheshire])
+ (:import (java.io ByteArrayInputStream)))
+
+;;
+;; start repl with `lein perf repl`
+;; perf measured with the following setup:
+;;
+;; Model Name: MacBook Pro
+;; Model Identifier: MacBookPro11,3
+;; Processor Name: Intel Core i7
+;; Processor Speed: 2,5 GHz
+;; Number of Processors: 1
+;; Total Number of Cores: 4
+;; L2 Cache (per Core): 256 KB
+;; L3 Cache: 6 MB
+;; Memory: 16 GB
+;;
+
+(defn title [s]
+ (println
+ (str "\n\u001B[35m"
+ (apply str (repeat (+ 6 (count s)) "#"))
+ "\n## " s " ##\n"
+ (apply str (repeat (+ 6 (count s)) "#"))
+ "\u001B[0m\n")))
+
+(defn post* [app uri json]
+ (->
+ (app {:uri uri
+ :request-method :post
+ :content-type "application/json"
+ :body (io/input-stream (.getBytes json))})
+ :body
+ slurp))
+
+(defn parse [s] (json/parse-string s true))
+
+(s/defschema Order {:id s/Str
+ :name s/Str
+ (s/optional-key :description) s/Str
+ :address (s/maybe {:street s/Str
+ :country (s/enum "FI" "PO")})
+ :orders [{:name #"^k"
+ :price s/Any
+ :shipping s/Bool}]})
+
+(defn bench []
+
+ ; 27µs
+ ; 27µs (-0%)
+ ; 25µs (1.0.0)
+ (let [app (api
+ (GET "/30" []
+ (ok {:result 30})))
+ call #(h/get* app "/30")]
+
+ (title "GET JSON")
+ (assert (= {:result 30} (second (call))))
+ (cc/bench (call)))
+
+ ;; 73µs
+ ;; 53µs (-27%)
+ ;; 50µs (1.0.0)
+ (let [app (api
+ (POST "/plus" []
+ :return {:result s/Int}
+ :body-params [x :- s/Int, y :- s/Int]
+ (ok {:result (+ x y)})))
+ data (h/json {:x 10, :y 20})
+ call #(post* app "/plus" data)]
+
+ (title "JSON POST with 2-way coercion")
+ (assert (= {:result 30} (parse (call))))
+ (cc/bench (call)))
+
+ ;; 85µs
+ ;; 67µs (-21%)
+ ;; 66µs (1.0.0)
+ (let [app (api
+ (context "/a" []
+ (context "/b" []
+ (context "/c" []
+ (POST "/plus" []
+ :return {:result s/Int}
+ :body-params [x :- s/Int, y :- s/Int]
+ (ok {:result (+ x y)}))))))
+ data (h/json {:x 10, :y 20})
+ call #(post* app "/a/b/c/plus" data)]
+
+ (title "JSON POST with 2-way coercion + contexts")
+ (assert (= {:result 30} (parse (call))))
+ (cc/bench (call)))
+
+ ;; 266µs
+ ;; 156µs (-41%)
+ ;; 146µs (1.0.0)
+ (let [app (api
+ (POST "/echo" []
+ :return Order
+ :body [order Order]
+ (ok order)))
+ data (h/json {:id "123"
+ :name "Tommi's order"
+ :description "Totally great order"
+ :address {:street "Randomstreet 123"
+ :country "FI"}
+ :orders [{:name "k1"
+ :price 123.0
+ :shipping true}
+ {:name "k2"
+ :price 42.0
+ :shipping false}]})
+ call #(post* app "/echo" data)]
+
+ (title "JSON POST with nested data")
+ (s/validate Order (parse (call)))
+ (cc/bench (call))))
+
+(defn resource-bench []
+
+ (let [resource-map {:post {:responses {200 {:schema {:result s/Int}}}
+ :parameters {:body-params {:x s/Int, :y s/Int}}
+ :handler (fn [{{:keys [x y]} :body-params}]
+ (ok {:result (+ x y)}))}}]
+
+ ;; 62µs
+ (let [my-resource (resource resource-map)
+ app (api
+ (context "/plus" []
+ my-resource))
+ data (h/json {:x 10, :y 20})
+ call #(post* app "/plus" data)]
+
+ (title "JSON POST to pre-defined resource with 2-way coercion")
+ (assert (= {:result 30} (parse (call))))
+ (cc/bench (call)))
+
+ ;; 68µs
+ (let [app (api
+ (context "/plus" []
+ (resource resource-map)))
+ data (h/json {:x 10, :y 20})
+ call #(post* app "/plus" data)]
+
+ (title "JSON POST to inlined resource with 2-way coercion")
+ (assert (= {:result 30} (parse (call))))
+ (cc/bench (call)))
+
+ ;; 26µs
+ (let [my-resource (resource resource-map)
+ app my-resource
+ data {:x 10, :y 20}
+ call #(app {:request-method :post :uri "/irrelevant" :body-params data})]
+
+ (title "direct POST to pre-defined resource with 2-way coercion")
+ (assert (= {:result 30} (:body (call))))
+ (cc/bench (call)))
+
+ ;; 30µs
+ (let [my-resource (resource resource-map)
+ app (context "/plus" []
+ my-resource)
+ data {:x 10, :y 20}
+ call #(app {:request-method :post :uri "/plus" :body-params data})]
+
+ (title "POST to pre-defined resource with 2-way coercion")
+ (assert (= {:result 30} (:body (call))))
+ (cc/bench (call)))
+
+ ;; 40µs
+ (let [app (context "/plus" []
+ (resource resource-map))
+ data {:x 10, :y 20}
+ call #(app {:request-method :post :uri "/plus" :body-params data})]
+
+ (title "POST to inlined resource with 2-way coercion")
+ (assert (= {:result 30} (:body (call))))
+ (cc/bench (call)))))
+
+(defn e2e-json-comparison-different-payloads []
+ (let [json-request (fn [data]
+ {:uri "/echo"
+ :request-method :post
+ :headers {"content-type" "application/json"
+ "accept" "application/json"}
+ :body (cheshire/generate-string data)})
+ request-stream (fn [request]
+ (let [b (.getBytes ^String (:body request))]
+ (fn []
+ (assoc request :body (ByteArrayInputStream. b)))))
+ app (api
+ (POST "/echo" []
+ :body [body s/Any]
+ (ok body)))]
+ (doseq [file ["dev-resources/json/json10b.json"
+ "dev-resources/json/json100b.json"
+ "dev-resources/json/json1k.json"
+ "dev-resources/json/json10k.json"
+ "dev-resources/json/json100k.json"]
+ :let [data (cheshire/parse-string (slurp file))
+ request (json-request data)
+ request! (request-stream request)]]
+
+ "10b"
+ ;; 42µs
+
+ "100b"
+ ;; 79µs
+
+ "1k"
+ ;; 367µs
+
+ "10k"
+ ;; 2870µs
+
+ "100k"
+ ;; 10800µs
+
+ (title file)
+ (cc/bench (-> (request!) app :body slurp)))))
+
+(comment
+ (bench)
+ (resource-bench)
+ (e2e-json-comparison-different-payloads))
+
+(comment
+ (bench)
+ (resource-bench))
diff --git a/test-suites/compojure1/test/compojure/api/resource_test.clj b/test-suites/compojure1/test/compojure/api/resource_test.clj
new file mode 100644
index 00000000..fbe6ccb5
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/resource_test.clj
@@ -0,0 +1,206 @@
+(ns compojure.api.resource-test
+ (:require [compojure.api.sweet :refer :all]
+ [compojure.api.test-utils :refer :all]
+ [plumbing.core :refer [fnk]]
+ [midje.sweet :refer :all]
+ [ring.util.http-response :refer :all]
+ [schema.core :as s])
+ (:import [clojure.lang ExceptionInfo]))
+
+(defn has-body [expected]
+ (fn [{:keys [body]}]
+ (= body expected)))
+
+(def request-validation-failed?
+ (throws ExceptionInfo #"Request validation failed"))
+
+(def response-validation-failed?
+ (throws ExceptionInfo #"Response validation failed"))
+
+(facts "resource definitions"
+
+ (fact "only top-level handler"
+ (let [handler (resource
+ {:handler (constantly (ok {:total 10}))})]
+
+ (fact "paths and methods don't matter"
+ (handler {:request-method :get, :uri "/"}) => (has-body {:total 10})
+ (handler {:request-method :head, :uri "/kikka"}) => (has-body {:total 10}))))
+
+ (fact "top-level parameter coercions"
+ (let [handler (resource
+ {:parameters {:query-params {:x Long}}
+ :handler (fnk [[:query-params x]]
+ (ok {:total x}))})]
+
+ (handler {:request-method :get}) => request-validation-failed?
+ (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:total 1})
+ (handler {:request-method :get, :query-params {:x "1", :y "2"}}) => (has-body {:total 1})))
+
+ (fact "top-level and operation-level parameter coercions"
+ (let [handler (resource
+ {:parameters {:query-params {:x Long}}
+ :get {:parameters {:query-params {(s/optional-key :y) Long}}}
+ :handler (fnk [[:query-params x {y 0}]]
+ (ok {:total (+ x y)}))})]
+
+ (handler {:request-method :get}) => request-validation-failed?
+ (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:total 1})
+ (handler {:request-method :get, :query-params {:x "1", :y "a"}}) => request-validation-failed?
+ (handler {:request-method :get, :query-params {:x "1", :y "2"}}) => (has-body {:total 3})
+
+ (fact "non-matching operation level parameters are not used"
+ (handler {:request-method :post, :query-params {:x "1"}}) => (has-body {:total 1})
+ (handler {:request-method :post, :query-params {:x "1", :y "2"}}) => (throws ClassCastException))))
+
+ (fact "operation-level handlers"
+ (let [handler (resource
+ {:parameters {:query-params {:x Long}}
+ :get {:parameters {:query-params {(s/optional-key :y) Long}}
+ :handler (fnk [[:query-params x {y 0}]]
+ (ok {:total (+ x y)}))}
+ :post {:parameters {:query-params {:z Long}}}})]
+
+ (handler {:request-method :get}) => request-validation-failed?
+ (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:total 1})
+ (handler {:request-method :get, :query-params {:x "1", :y "a"}}) => request-validation-failed?
+ (handler {:request-method :get, :query-params {:x "1", :y "2"}}) => (has-body {:total 3})
+
+ (fact "if no handler is found, nil is returned"
+ (handler {:request-method :post, :query-params {:x "1"}}) => nil)))
+
+ (fact "handler preference"
+ (let [handler (resource
+ {:get {:handler (constantly (ok {:from "get"}))}
+ :handler (constantly (ok {:from "top"}))})]
+
+ (handler {:request-method :get}) => (has-body {:from "get"})
+ (handler {:request-method :post}) => (has-body {:from "top"})))
+
+ (fact "resource without coercion"
+ (let [handler (resource
+ {:get {:parameters {:query-params {(s/optional-key :y) Long
+ (s/optional-key :x) Long}}
+ :handler (fn [{{:keys [x y]} :query-params}]
+ (ok {:x x
+ :y y}))}}
+ {:coercion (constantly nil)})]
+
+ (handler {:request-method :get}) => (has-body {:x nil, :y nil})
+ (handler {:request-method :get, :query-params {:x "1"}}) => (has-body {:x "1", :y nil})
+ (handler {:request-method :get, :query-params {:x "1", :y "a"}}) => (has-body {:x "1", :y "a"})
+ (handler {:request-method :get, :query-params {:x 1, :y 2}}) => (has-body {:x 1, :y 2})))
+
+ (fact "parameter mappings"
+ (let [handler (resource
+ {:get {:parameters {:query-params {:q s/Str}
+ :body-params {:b s/Str}
+ :form-params {:f s/Str}
+ :header-params {:h s/Str}
+ :path-params {:p s/Str}}
+ :handler (fn [request]
+ (ok (select-keys request [:query-params
+ :body-params
+ :form-params
+ :header-params
+ :path-params])))}})]
+
+ (handler {:request-method :get
+ :query-params {:q "q"}
+ :body-params {:b "b"}
+ :form-params {:f "f"}
+ ;; the ring headers
+ :headers {"h" "h"}
+ ;; compojure routing
+ :route-params {:p "p"}}) => (has-body {:query-params {:q "q"}
+ :body-params {:b "b"}
+ :form-params {:f "f"}
+ :header-params {:h "h"}
+ :path-params {:p "p"}})))
+
+ (fact "response coercion"
+ (let [handler (resource
+ {:responses {200 {:schema {:total (s/constrained Long pos? 'pos)}}}
+ :parameters {:query-params {:x Long}}
+ :get {:responses {200 {:schema {:total (s/constrained Long #(>= % 10) 'gte10)}}}
+ :handler (fnk [[:query-params x]]
+ (ok {:total x}))}
+ :handler (fnk [[:query-params x]]
+ (ok {:total x}))})]
+
+ (handler {:request-method :get}) => request-validation-failed?
+ (handler {:request-method :get, :query-params {:x "-1"}}) => response-validation-failed?
+ (handler {:request-method :get, :query-params {:x "1"}}) => response-validation-failed?
+ (handler {:request-method :get, :query-params {:x "10"}}) => (has-body {:total 10})
+ (handler {:request-method :post, :query-params {:x "1"}}) => (has-body {:total 1}))))
+
+(fact "compojure-api routing integration"
+ (let [handler (context "/rest" []
+
+ (GET "/no" request
+ (ok (select-keys request [:uri :path-info])))
+
+ (context "/context" []
+ (resource
+ {:handler (constantly (ok "CONTEXT"))}))
+
+ ;; does not work
+ (ANY "/any" []
+ (resource
+ {:handler (constantly (ok "ANY"))}))
+
+ (context "/path/:id" []
+ (resource
+ {:parameters {:path-params {:id s/Int}}
+ :handler (fn [request]
+ (ok (select-keys request [:path-params :route-params])))}))
+
+ (resource
+ {:get {:handler (fn [request]
+ (ok (select-keys request [:uri :path-info])))}}))]
+
+ (fact "normal endpoint works"
+ (handler {:request-method :get, :uri "/rest/no"}) => (has-body {:uri "/rest/no", :path-info "/no"}))
+
+ (fact "wrapped in ANY fails at runtime"
+ (handler {:request-method :get, :uri "/rest/any"}) => throws)
+
+ (fact "wrapped in context works"
+ (handler {:request-method :get, :uri "/rest/context"}) => (has-body "CONTEXT"))
+
+ (fact "path-parameters work: route-params are left untoucehed, path-params are coerced"
+ (handler {:request-method :get, :uri "/rest/path/12"}) => (has-body {:path-params {:id 12}
+ :route-params {:id "12"}}))
+
+ (fact "top-level GET works"
+ (handler {:request-method :get, :uri "/rest/in-peaces"}) => (has-body {:uri "/rest/in-peaces"
+ :path-info "/in-peaces"}))
+
+ (fact "top-level POST misses"
+ (handler {:request-method :post, :uri "/rest/in-peaces"}) => nil)))
+
+(fact "swagger-integration"
+ (let [app (api
+ (swagger-routes)
+ (context "/rest" []
+ (resource
+ {:parameters {:query-params {:x Long}}
+ :responses {400 {:schema (s/schema-with-name {:code s/Str} "Error")}}
+ :get {:parameters {:query-params {:y Long}}
+ :responses {200 {:schema (s/schema-with-name {:total Long} "Total")}}}
+ :post {}
+ :handler (constantly (ok {:total 1}))})))
+ spec (get-spec app)]
+
+ spec => (contains
+ {:definitions (just
+ {:Error irrelevant
+ :Total irrelevant})
+ :paths (just
+ {"/rest" (just
+ {:get (just
+ {:parameters (two-of irrelevant)
+ :responses (just {:200 irrelevant, :400 irrelevant})})
+ :post (just
+ {:parameters (one-of irrelevant)
+ :responses (just {:400 irrelevant})})})})})))
diff --git a/test-suites/compojure1/test/compojure/api/routes_test.clj b/test-suites/compojure1/test/compojure/api/routes_test.clj
new file mode 100644
index 00000000..b44e100b
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/routes_test.clj
@@ -0,0 +1,152 @@
+(ns compojure.api.routes-test
+ (:require [midje.sweet :refer :all]
+ [compojure.api.sweet :refer :all]
+ [compojure.api.routes :as routes]
+ [ring.util.http-response :refer :all]
+ [ring.util.http-predicates :refer :all]
+ [compojure.api.test-utils :refer :all]
+ [schema.core :as s])
+ (:import [java.security SecureRandom]
+ [org.joda.time LocalDate]
+ [com.fasterxml.jackson.core JsonGenerationException]))
+
+(facts "path-string"
+
+ (fact "missing path parameter"
+ (#'routes/path-string "/api/:kikka" {})
+ => (throws IllegalArgumentException))
+
+ (fact "missing serialization"
+ (#'routes/path-string "/api/:kikka" {:kikka (SecureRandom.)})
+ => (throws JsonGenerationException))
+
+ (fact "happy path"
+ (#'routes/path-string "/a/:b/:c/d/:e/f" {:b (LocalDate/parse "2015-05-22")
+ :c 12345
+ :e :kikka})
+ => "/a/2015-05-22/12345/d/kikka/f"))
+
+(fact "string-path-parameters"
+ (#'routes/string-path-parameters "/:foo.json") => {:foo String})
+
+(facts "nested routes"
+ (let [mw (fn [handler] (fn [request] (handler request)))
+ more-routes (fn [version]
+ (routes
+ (GET "/more" []
+ (ok {:message version}))))
+ routes (context "/api/:version" []
+ :path-params [version :- String]
+ (GET "/ping" []
+ (ok {:message (str "pong - " version)}))
+ (POST "/ping" []
+ (ok {:message (str "pong - " version)}))
+ (middleware [mw]
+ (GET "/hello" []
+ :return {:message String}
+ :summary "cool ping"
+ :query-params [name :- String]
+ (ok {:message (str "Hello, " name)}))
+ (more-routes version)))
+ app (api
+ (swagger-routes)
+ routes)]
+
+ (fact "all routes can be invoked"
+ (let [[status body] (get* app "/api/v1/hello" {:name "Tommi"})]
+ status = 200
+ body => {:message "Hello, Tommi"})
+
+ (let [[status body] (get* app "/api/v1/ping")]
+ status = 200
+ body => {:message "pong - v1"})
+
+ (let [[status body] (get* app "/api/v2/ping")]
+ status = 200
+ body => {:message "pong - v2"})
+
+ (let [[status body] (get* app "/api/v3/more")]
+ status => 200
+ body => {:message "v3"}))
+
+ (fact "routes can be extracted at runtime"
+ (routes/get-routes app)
+ => [["/swagger.json" :get {:x-no-doc true, :x-name :compojure.api.swagger/swagger}]
+ ["/api/:version/ping" :get {:parameters {:path {:version String, s/Keyword s/Any}}}]
+ ["/api/:version/ping" :post {:parameters {:path {:version String, s/Keyword s/Any}}}]
+ ["/api/:version/hello" :get {:parameters {:query {:name String, s/Keyword s/Any}
+ :path {:version String, s/Keyword s/Any}}
+ :responses {200 {:description "", :schema {:message String}}}
+ :summary "cool ping"}]
+ ["/api/:version/more" :get {:parameters {:path {:version String, s/Keyword s/Any}}}]])
+
+ (fact "swagger-docs can be generated"
+ (-> app get-spec :paths keys)
+ => ["/api/{version}/ping"
+ "/api/{version}/hello"
+ "/api/{version}/more"])))
+
+(def more-routes
+ (routes
+ (GET "/more" []
+ (ok {:gary "moore"}))))
+
+(facts "following var-routes, #219"
+ (let [routes (context "/api" [] #'more-routes)]
+ (routes/get-routes routes) => [["/api/more" :get {}]]))
+
+;; TODO: should this do something different?
+(facts "dynamic routes"
+ (let [more-routes (fn [version]
+ (GET (str "/" version) []
+ (ok {:message version})))
+ routes (context "/api/:version" []
+ :path-params [version :- String]
+ (more-routes version))
+ app (api
+ (swagger-routes)
+ routes)]
+
+ (fact "all routes can be invoked"
+ (let [[status body] (get* app "/api/v3/v3")]
+ status => 200
+ body => {:message "v3"})
+
+ (let [[status body] (get* app "/api/v6/v6")]
+ status => 200
+ body => {:message "v6"}))
+
+ (fact "routes can be extracted at runtime"
+ (routes/get-routes app)
+ => [["/swagger.json" :get {:x-no-doc true, :x-name :compojure.api.swagger/swagger}]
+ ["/api/:version/[]" :get {:parameters {:path {:version String, s/Keyword s/Any}}}]])
+
+ (fact "swagger-docs can be generated"
+ (-> app get-spec :paths keys)
+ => ["/api/{version}/[]"])))
+
+(fact "route merging"
+ (routes/get-routes (routes (routes))) => []
+ (routes/get-routes (routes (swagger-routes {:spec nil}))) => []
+ (routes/get-routes (routes (routes (GET "/ping" [] "pong")))) => [["/ping" :get {}]])
+
+(fact "invalid route options"
+ (let [r (routes (constantly nil))]
+
+ (fact "ignore 'em all"
+ (routes/get-routes r) => []
+ (routes/get-routes r nil) => []
+ (routes/get-routes r {:invalid-routes-fn nil}) => [])
+
+ (fact "log warnings"
+ (routes/get-routes r {:invalid-routes-fn routes/log-invalid-child-routes}) => []
+ (provided
+ (compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 1))
+
+ (fact "throw exception"
+ (routes/get-routes r {:invalid-routes-fn routes/fail-on-invalid-child-routes})) => throws))
+
+(fact "context routes with compojure destructuring"
+ (let [app (context "/api" req
+ (GET "/ping" [] (ok (:magic req))))]
+ (app {:request-method :get :uri "/api/ping" :magic {:just "works"}}) => (contains {:body {:just "works"}})))
diff --git a/test-suites/compojure1/test/compojure/api/swagger_ordering_test.clj b/test-suites/compojure1/test/compojure/api/swagger_ordering_test.clj
new file mode 100644
index 00000000..55112ecf
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/swagger_ordering_test.clj
@@ -0,0 +1,36 @@
+(ns compojure.api.swagger-ordering-test
+ (:require [midje.sweet :refer :all]
+ [compojure.api.sweet :refer :all]
+ [compojure.api.test-utils :refer :all]))
+
+(def more-routes
+ (routes
+ (GET "/6" [] identity)
+ (GET "/7" [] identity)
+ (GET "/8" [] identity)))
+
+(facts "with 10+ routes"
+ (let [app (api
+ (context "/a" []
+ (GET "/1" [] identity)
+ (GET "/2" [] identity)
+ (GET "/3" [] identity)
+ (context "/b" []
+ (GET "/4" [] identity)
+ (GET "/5" [] identity))
+ (context "/c" []
+ more-routes
+ (GET "/9" [] identity)
+ (GET "/10" [] identity))))]
+
+ (fact "swagger-api order is maintained"
+ (keys (extract-paths app)) => ["/a/1"
+ "/a/2"
+ "/a/3"
+ "/a/b/4"
+ "/a/b/5"
+ "/a/c/6"
+ "/a/c/7"
+ "/a/c/8"
+ "/a/c/9"
+ "/a/c/10"])))
diff --git a/test-suites/compojure1/test/compojure/api/swagger_test.clj b/test-suites/compojure1/test/compojure/api/swagger_test.clj
new file mode 100644
index 00000000..e47cfd63
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/swagger_test.clj
@@ -0,0 +1,187 @@
+(ns compojure.api.swagger-test
+ (:require [schema.core :as s]
+ [compojure.api.sweet :refer :all]
+ compojure.core
+ [compojure.api.test-utils :refer :all]
+ [midje.sweet :refer :all]))
+
+(defmacro optional-routes [p & body] (when p `(routes ~@body)))
+(defmacro GET+ [p & body] `(GET ~(str "/xxx" p) ~@body))
+
+(fact "extracting compojure paths"
+
+ (fact "all compojure.api.core macros are interpreted"
+ (let [app (context "/a" []
+ (routes
+ (context "/b" []
+ (let-routes []
+ (GET "/c" [] identity)
+ (POST "/d" [] identity)
+ (PUT "/e" [] identity)
+ (DELETE "/f" [] identity)
+ (OPTIONS "/g" [] identity)
+ (PATCH "/h" [] identity)))
+ (context "/:i/:j" []
+ (GET "/k/:l/m/:n" [] identity))))]
+
+ (extract-paths app)
+ => {"/a/b/c" {:get {}}
+ "/a/b/d" {:post {}}
+ "/a/b/e" {:put {}}
+ "/a/b/f" {:delete {}}
+ "/a/b/g" {:options {}}
+ "/a/b/h" {:patch {}}
+ "/a/:i/:j/k/:l/m/:n" {:get {:parameters {:path {:i String
+ :j String
+ :l String
+ :n String}}}}}))
+
+ (fact "runtime code in route is NOT ignored"
+ (extract-paths
+ (context "/api" []
+ (if false
+ (GET "/true" [] identity)
+ (PUT "/false" [] identity)))) => {"/api/false" {:put {}}})
+
+ (fact "route-macros are expanded"
+ (extract-paths
+ (context "/api" []
+ (optional-routes true (GET "/true" [] identity))
+ (optional-routes false (PUT "/false" [] identity)))) => {"/api/true" {:get {}}})
+
+ (fact "endpoint-macros are expanded"
+ (extract-paths
+ (context "/api" []
+ (GET+ "/true" [] identity))) => {"/api/xxx/true" {:get {}}})
+
+ (fact "Vanilla Compojure defroutes are NOT followed"
+ (compojure.core/defroutes even-more-routes (GET "/even" [] identity))
+ (compojure.core/defroutes more-routes (context "/more" [] even-more-routes))
+ (extract-paths
+ (context "/api" []
+ (GET "/true" [] identity)
+ more-routes)) => {"/api/true" {:get {}}})
+
+ (fact "Compojure Api defroutes and def routes are followed"
+ (def even-more-routes (GET "/even" [] identity))
+ (defroutes more-routes (context "/more" [] even-more-routes))
+ (extract-paths
+ (context "/api" []
+ (GET "/true" [] identity)
+ more-routes)) => {"/api/true" {:get {}}
+ "/api/more/even" {:get {}}})
+
+ (fact "Parameter regular expressions are discarded"
+ (extract-paths
+ (context "/api" []
+ (GET ["/:param" :param #"[a-z]+"] [] identity)))
+
+ => {"/api/:param" {:get {:parameters {:path {:param String}}}}}))
+
+(fact "context meta-data"
+ (extract-paths
+ (context "/api/:id" []
+ :summary "top-summary"
+ :path-params [id :- String]
+ :tags [:kiss]
+ (GET "/kikka" []
+ identity)
+ (context "/ipa" []
+ :summary "mid-summary"
+ :tags [:wasp]
+ (GET "/kukka/:kukka" []
+ :summary "bottom-summary"
+ :path-params [kukka :- String]
+ :tags [:venom])
+ (GET "/kakka" []
+ identity))))
+
+ => {"/api/:id/kikka" {:get {:summary "top-summary"
+ :tags #{:kiss}
+ :parameters {:path {:id String}}}}
+ "/api/:id/ipa/kukka/:kukka" {:get {:summary "bottom-summary"
+ :tags #{:venom}
+ :parameters {:path {:id String
+ :kukka String}}}}
+ "/api/:id/ipa/kakka" {:get {:summary "mid-summary"
+ :tags #{:wasp}
+ :parameters {:path {:id String}}}}})
+
+(facts "duplicate context merge"
+ (let [app (routes
+ (context "/api" []
+ :tags [:kiss]
+ (GET "/kakka" []
+ identity))
+ (context "/api" []
+ :tags [:kiss]
+ (GET "/kukka" []
+ identity)))]
+ (extract-paths app)
+ => {"/api/kukka" {:get {:tags #{:kiss}}}
+ "/api/kakka" {:get {:tags #{:kiss}}}}))
+
+(def r1
+ (GET "/:id" []
+ :path-params [id :- s/Str]
+ identity))
+(def r2
+ (GET "/kukka/:id" []
+ :path-params [id :- Long]
+ identity))
+
+(facts "defined routes path-params"
+ (extract-paths (routes r1 r2))
+ => {"/:id" {:get {:parameters {:path {:id String}}}}
+ "/kukka/:id" {:get {:parameters {:path {:id Long}}}}})
+
+(fact "context meta-data"
+ (extract-paths
+ (context "/api/:id" []
+ :summary "top-summary"
+ :path-params [id :- String]
+ :tags [:kiss]
+ (GET "/kikka" []
+ identity)
+ (context "/ipa" []
+ :summary "mid-summary"
+ :tags [:wasp]
+ (GET "/kukka/:kukka" []
+ :summary "bottom-summary"
+ :path-params [kukka :- String]
+ :tags [:venom])
+ (GET "/kakka" []
+ identity))))
+
+ => {"/api/:id/kikka" {:get {:summary "top-summary"
+ :tags #{:kiss}
+ :parameters {:path {:id String}}}}
+ "/api/:id/ipa/kukka/:kukka" {:get {:summary "bottom-summary"
+ :tags #{:venom}
+ :parameters {:path {:id String
+ :kukka String}}}}
+ "/api/:id/ipa/kakka" {:get {:summary "mid-summary"
+ :tags #{:wasp}
+ :parameters {:path {:id String}}}}})
+
+(fact "path params followed by an extension"
+ (extract-paths
+ (GET "/:foo.json" []
+ :path-params [foo :- String]
+ identity))
+ => {"/:foo.json" {:get {:parameters {:path {:foo String}}}}})
+
+(facts
+ (tabular
+ (fact "swagger-routes basePath can be changed"
+ (let [app (api (swagger-routes ?given-options))]
+ (->
+ (get* app "/swagger.json")
+ (nth 1)
+ :basePath)
+ => ?expected-base-path
+ (nth (raw-get* app "/conf.js") 1) => (str "window.API_CONF = {\"url\":\"" ?expected-swagger-docs-path "\"};")))
+ ?given-options ?expected-swagger-docs-path ?expected-base-path
+ {} "/swagger.json" "/"
+ {:data {:basePath "/app"}} "/app/swagger.json" "/app"
+ {:data {:basePath "/app"} :options {:ui {:swagger-docs "/imaginary.json"}}} "/imaginary.json" "/app"))
diff --git a/test-suites/compojure1/test/compojure/api/sweet_test.clj b/test-suites/compojure1/test/compojure/api/sweet_test.clj
new file mode 100644
index 00000000..c9605bc3
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/sweet_test.clj
@@ -0,0 +1,209 @@
+(ns compojure.api.sweet-test
+ (:require [compojure.api.sweet :refer :all]
+ [compojure.api.test-utils :refer :all]
+ [midje.sweet :refer :all]
+ [ring.mock.request :refer :all]
+ [schema.core :as s]
+ [ring.swagger.validator :as v]))
+
+(s/defschema Band {:id s/Int
+ :name s/Str
+ (s/optional-key :description) (s/maybe s/Str)
+ :toppings [(s/enum :cheese :olives :ham :pepperoni :habanero)]})
+
+(s/defschema NewBand (dissoc Band :id))
+
+(def ping-route
+ (GET "/ping" [] identity))
+
+(def app
+ (api
+ {:swagger {:spec "/swagger.json"
+ :data {:info {:version "1.0.0"
+ :title "Sausages"
+ :description "Sausage description"
+ :termsOfService "http://helloreverb.com/terms/"
+ :contact {:name "My API Team"
+ :email "foo@example.com"
+ :url "http://www.metosin.fi"}
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"}}}}}
+ ping-route
+ (context "/api" []
+ ping-route
+ (GET "/bands" []
+ :name :bands
+ :return [Band]
+ :summary "Gets all Bands"
+ :description "bands bands bands"
+ :operationId "getBands"
+ identity)
+ (GET "/bands/:id" [id]
+ :return Band
+ :summary "Gets a Band"
+ :operationId "getBand"
+ identity)
+ (POST "/bands" []
+ :return Band
+ :body [band [NewBand]]
+ :summary "Adds a Band"
+ :operationId "addBand"
+ identity)
+ (GET "/query" []
+ :query-params [qp :- Boolean]
+ identity)
+ (GET "/header" []
+ :header-params [hp :- Boolean]
+ identity)
+ (POST "/form" []
+ :form-params [fp :- Boolean]
+ identity)
+ (GET "/primitive" []
+ :return String
+ identity)
+ (GET "/primitiveArray" []
+ :return [String]
+ identity))))
+
+(facts "api documentation"
+ (fact "details are generated"
+
+ (extract-paths app)
+
+ => {"/swagger.json" {:get {:x-name :compojure.api.swagger/swagger,
+ :x-no-doc true}}
+ "/ping" {:get {}}
+ "/api/ping" {:get {}}
+ "/api/bands" {:get {:x-name :bands
+ :operationId "getBands"
+ :description "bands bands bands"
+ :responses {200 {:schema [Band]
+ :description ""}}
+ :summary "Gets all Bands"}
+ :post {:operationId "addBand"
+ :parameters {:body [NewBand]}
+ :responses {200 {:schema Band
+ :description ""}}
+ :summary "Adds a Band"}}
+ "/api/bands/:id" {:get {:operationId "getBand"
+ :responses {200 {:schema Band
+ :description ""}}
+ :summary "Gets a Band"
+ :parameters {:path {:id String}}}}
+ "/api/query" {:get {:parameters {:query {:qp Boolean
+ s/Keyword s/Any}}}}
+ "/api/header" {:get {:parameters {:header {:hp Boolean
+ s/Keyword s/Any}}}}
+ "/api/form" {:post {:parameters {:formData {:fp Boolean}}
+ :consumes ["application/x-www-form-urlencoded"]}}
+ "/api/primitive" {:get {:responses {200 {:schema String
+ :description ""}}}}
+ "/api/primitiveArray" {:get {:responses {200 {:schema [String]
+ :description ""}}}}})
+
+ (fact "api-listing works"
+ (let [spec (get-spec app)]
+
+ spec => {:swagger "2.0"
+ :info {:version "1.0.0"
+ :title "Sausages"
+ :description "Sausage description"
+ :termsOfService "http://helloreverb.com/terms/"
+ :contact {:name "My API Team"
+ :email "foo@example.com"
+ :url "http://www.metosin.fi"}
+ :license {:name "Eclipse Public License"
+ :url "http://www.eclipse.org/legal/epl-v10.html"}}
+ :basePath "/"
+ :consumes ["application/json"
+ "application/x-yaml"
+ "application/edn"
+ "application/transit+json"
+ "application/transit+msgpack"],
+ :produces ["application/json"
+ "application/x-yaml"
+ "application/edn"
+ "application/transit+json"
+ "application/transit+msgpack"]
+ :paths {"/api/bands" {:get {:x-name "bands"
+ :operationId "getBands"
+ :description "bands bands bands"
+ :responses {:200 {:description ""
+ :schema {:items {:$ref "#/definitions/Band"}
+ :type "array"}}}
+ :summary "Gets all Bands"}
+ :post {:operationId "addBand"
+ :parameters [{:description ""
+ :in "body"
+ :name "NewBand"
+ :required true
+ :schema {:items {:$ref "#/definitions/NewBand"}
+ :type "array"}}]
+ :responses {:200 {:description ""
+ :schema {:$ref "#/definitions/Band"}}}
+ :summary "Adds a Band"}}
+ "/api/bands/{id}" {:get {:operationId "getBand"
+ :parameters [{:description ""
+ :in "path"
+ :name "id"
+ :required true
+ :type "string"}]
+ :responses {:200 {:description ""
+ :schema {:$ref "#/definitions/Band"}}}
+ :summary "Gets a Band"}}
+ "/api/query" {:get {:parameters [{:in "query"
+ :name "qp"
+ :description ""
+ :required true
+ :type "boolean"}]
+ :responses {:default {:description ""}}}}
+ "/api/header" {:get {:parameters [{:in "header"
+ :name "hp"
+ :description ""
+ :required true
+ :type "boolean"}]
+ :responses {:default {:description ""}}}}
+ "/api/form" {:post {:parameters [{:in "formData"
+ :name "fp"
+ :description ""
+ :required true
+ :type "boolean"}]
+ :responses {:default {:description ""}}
+ :consumes ["application/x-www-form-urlencoded"]}}
+ "/api/ping" {:get {:responses {:default {:description ""}}}}
+ "/api/primitive" {:get {:responses {:200 {:description ""
+ :schema {:type "string"}}}}}
+ "/api/primitiveArray" {:get {:responses {:200 {:description ""
+ :schema {:items {:type "string"}
+ :type "array"}}}}}
+ "/ping" {:get {:responses {:default {:description ""}}}}}
+ :definitions {:Band {:type "object"
+ :properties {:description {:type "string"
+ :x-nullable true}
+ :id {:format "int64", :type "integer"}
+ :name {:type "string"}
+ :toppings {:items {:enum ["olives"
+ "pepperoni"
+ "ham"
+ "cheese"
+ "habanero"]
+ :type "string"}
+ :type "array"}}
+ :required ["id" "name" "toppings"]
+ :additionalProperties false}
+ :NewBand {:type "object"
+ :properties {:description {:type "string"
+ :x-nullable true}
+ :name {:type "string"}
+ :toppings {:items {:enum ["olives"
+ "pepperoni"
+ "ham"
+ "cheese"
+ "habanero"]
+ :type "string"}
+ :type "array"}}
+ :required ["name" "toppings"]
+ :additionalProperties false}}}
+
+ (fact "spec is valid"
+ (v/validate spec) => nil))))
diff --git a/test-suites/compojure1/test/compojure/api/test_domain.clj b/test-suites/compojure1/test/compojure/api/test_domain.clj
new file mode 100644
index 00000000..cd9eee65
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/test_domain.clj
@@ -0,0 +1,17 @@
+(ns compojure.api.test-domain
+ (:require [schema.core :as s]
+ [compojure.api.sweet :refer :all]
+ [ring.util.http-response :refer [ok]]))
+
+(s/defschema Topping {:name s/Str})
+(s/defschema Pizza {:toppings (s/maybe [Topping])})
+
+(s/defschema Beef {:name s/Str})
+(s/defschema Burger {:ingredients (s/maybe [Beef])})
+
+(def burger-routes
+ (routes
+ (POST "/burger" []
+ :return Burger
+ :body [burger Burger]
+ (ok burger))))
diff --git a/test-suites/compojure1/test/compojure/api/test_utils.clj b/test-suites/compojure1/test/compojure/api/test_utils.clj
new file mode 100644
index 00000000..60d24a87
--- /dev/null
+++ b/test-suites/compojure1/test/compojure/api/test_utils.clj
@@ -0,0 +1,103 @@
+(ns compojure.api.test-utils
+ (:require [cheshire.core :as cheshire]
+ [clojure.string :as str]
+ [peridot.core :as p]
+ [clojure.java.io :as io]
+ [compojure.api.routes :as routes])
+ (:import [java.io InputStream]))
+
+(defn read-body [body]
+ (if (instance? InputStream body)
+ (slurp body)
+ body))
+
+(defn parse-body [body]
+ (let [body (read-body body)
+ body (if (instance? String body)
+ (cheshire/parse-string body true)
+ body)]
+ body))
+
+(defn extract-schema-name [ref-str]
+ (last (str/split ref-str #"/")))
+
+(defn find-definition [spec ref]
+ (let [schema-name (keyword (extract-schema-name ref))]
+ (get-in spec [:definitions schema-name])))
+
+;;
+;; integration tests
+;;
+
+;;
+;; common
+;;
+
+(defn json [x] (cheshire/generate-string x))
+
+(defn json-stream [x] (io/input-stream (.getBytes (json x))))
+
+(defn follow-redirect [state]
+ (if (some-> state :response :headers (get "Location"))
+ (p/follow-redirect state)
+ state))
+
+(defn raw-get* [app uri & [params headers]]
+ (let [{{:keys [status body headers]} :response}
+ (-> (p/session app)
+ (p/request uri
+ :request-method :get
+ :params (or params {})
+ :headers (or headers {}))
+ follow-redirect)]
+ [status (read-body body) headers]))
+
+(defn get* [app uri & [params headers]]
+ (let [[status body headers]
+ (raw-get* app uri params headers)]
+ [status (parse-body body) headers]))
+
+(defn form-post* [app uri params]
+ (let [{{:keys [status body]} :response}
+ (-> (p/session app)
+ (p/request uri
+ :request-method :post
+ :params params))]
+ [status (parse-body body)]))
+
+(defn raw-post* [app uri & [data content-type headers]]
+ (let [{{:keys [status body]} :response}
+ (-> (p/session app)
+ (p/request uri
+ :request-method :post
+ :headers (or headers {})
+ :content-type (or content-type "application/json")
+ :body (.getBytes data)))]
+ [status (read-body body)]))
+
+(defn post* [app uri & [data]]
+ (let [[status body] (raw-post* app uri data)]
+ [status (parse-body body)]))
+
+(defn headers-post* [app uri headers]
+ (let [[status body] (raw-post* app uri "" nil headers)]
+ [status (parse-body body)]))
+
+;;
+;; get-spec
+;;
+
+(defn extract-paths [app]
+ (-> app routes/get-routes routes/ring-swagger-paths :paths))
+
+(defn get-spec [app]
+ (let [[status spec] (get* app "/swagger.json" {})]
+ (assert (= status 200))
+ (if (:paths spec)
+ (update-in spec [:paths] (fn [paths]
+ (into
+ (empty paths)
+ (for [[k v] paths]
+ [(if (= k (keyword "/"))
+ "/" (str "/" (name k))) v]))))
+ spec)))
diff --git a/test/compojure/api/integration_test.clj b/test/compojure/api/integration_test.clj
index 29c46384..e8a022da 100644
--- a/test/compojure/api/integration_test.clj
+++ b/test/compojure/api/integration_test.clj
@@ -535,7 +535,107 @@
(is (= pertti body)))
(is (= 1 @execution-times)))))
-(deftest swagger-docs-test
+(deftest ring-middleware-format-swagger-docs
+ (let [app (api
+ {:format {:formats [:json-kw :edn :UNKNOWN]}}
+ (swagger-routes)
+ (GET "/user" []
+ (continue)))]
+
+ (testing "api-listing shows produces & consumes for known types"
+ (is (= (get-spec app)
+ {:swagger "2.0"
+ :info {:title "Swagger API"
+ :version "0.0.1"}
+ :basePath "/"
+ :consumes ["application/json" "application/edn"]
+ :produces ["application/json" "application/edn"]
+ :definitions {}
+ :paths {"/user" {:get {:responses {:default {:description ""}}}}}}))))
+
+ (testing "swagger-routes"
+
+ (testing "with defaults"
+ (let [app (api (swagger-routes))]
+
+ (testing "api-docs are mounted to /"
+ (let [[status body] (raw-get* app "/")]
+ (is (= 200 status))
+ (is (str/includes? body "Swagger UI"))))
+
+ (testing "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ (is (= 200 status))
+ (is (= "2.0" (:swagger body)))))))
+
+ (testing "with partial overridden values"
+ (let [app (api (swagger-routes {:ui "/api-docs"
+ :data {:info {:title "Kikka"}
+ :paths {"/ping" {:get {}}}}}))]
+
+ (testing "api-docs are mounted"
+ (let [[status body] (raw-get* app "/api-docs")]
+ (is (= 200 status))
+ (is (str/includes? body "Swagger UI"))))
+
+ (testing "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ (is (= 200 status))
+ (is (= "2.0" (:swagger body)))
+ (is (= "Kikka" (-> body :info :title)))
+ (is (some? (-> body :paths (get (keyword "/ping"))))))))))
+
+ (testing "swagger via api-options"
+
+ (testing "with defaults"
+ (let [app (api)]
+
+ (testing "api-docs are not mounted"
+ (let [[status body] (raw-get* app "/")]
+ (is (nil? status))))
+
+ (testing "spec is not mounted"
+ (let [[status body] (get* app "/swagger.json")]
+ (is (nil? status))))))
+
+ (testing "with spec"
+ (let [app (api {:swagger {:spec "/swagger.json"}})]
+
+ (testing "api-docs are not mounted"
+ (let [[status body] (raw-get* app "/")]
+ (is (nil? status))))
+
+ (testing "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ (is (nil? status))
+ (is (= "2.0" (:swagger body))))))))
+
+ (testing "with ui"
+ (let [app (api {:swagger {:ui "/api-docs"}})]
+
+ (testing "api-docs are mounted"
+ (let [[status body] (raw-get* app "/api-docs")]
+ (is-200-status status)
+ (is (str/includes? body "Swagger UI"))))
+
+ (testing "spec is not mounted"
+ (let [[status body] (get* app "/swagger.json")]
+ (is (nil? status))))))
+
+ (testing "with ui and spec"
+ (let [app (api {:swagger {:spec "/swagger.json", :ui "/api-docs"}})]
+
+ (testing "api-docs are mounted"
+ (let [[status body] (raw-get* app "/api-docs")]
+ (is-200-status status)
+ (is (str/includes? body "Swagger UI"))))
+
+ (testing "spec is mounted to /swagger.json"
+ (let [[status body] (get* app "/swagger.json")]
+ (is-200-status status)
+ (is (= "2.0" (:swagger body))))))))
+
+(deftest muuntaja-swagger-docs-test
(let [app (api
{:formats (m/select-formats
m/default-options
diff --git a/test19/compojure/api/coercion/spec_coercion_test.clj b/test19/compojure/api/coercion/spec_coercion_test.clj
index 53412eac..b19c0762 100644
--- a/test19/compojure/api/coercion/spec_coercion_test.clj
+++ b/test19/compojure/api/coercion/spec_coercion_test.clj
@@ -393,7 +393,7 @@
:responses {:default {:description ""}}}}
"/body-map" {:post {:parameters [{:description ""
:in "body"
- :name ""
+ :name "body"
:required true
:schema {:properties {:x {:format "int64"
:type "integer"}
@@ -404,7 +404,7 @@
:responses {:default {:description ""}}}}
"/body-params" {:post {:parameters [{:description ""
:in "body"
- :name ""
+ :name "body"
:required true
:schema {:properties {:x {:format "int64"
:type "integer"}
@@ -415,7 +415,7 @@
:responses {:default {:description ""}}}}
"/body-string" {:post {:parameters [{:description ""
:in "body"
- :name ""
+ :name "body"
:required true
:schema {:type "string"}}]
:responses {:default {:description ""}}}}
@@ -476,7 +476,7 @@
:default {:description ""}}}
:post {:parameters [{:description ""
:in "body"
- :name ""
+ :name "body"
:required true
:schema {:properties {:x {:format "int64"
:type "integer"}