From 39b9d724e73185a7c57f730c1fe372a2110b4e0e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 23 Apr 2024 02:01:50 -0500 Subject: [PATCH 01/18] wip --- project.clj | 4 + src/compojure/api/exception.clj | 16 +- src/compojure/api/meta.clj | 44 +- src/compojure/api/middleware.clj | 221 ++- src/compojure/api/resource.clj | 37 +- src/compojure/api/routes.clj | 64 +- test-suites/compojure1/.lein-repl-history | 0 test-suites/compojure1/project.clj | 93 + .../metosin/compojure-api/pom.properties | 3 + ...core.classpath.extract-native-dependencies | 1 + .../test/compojure/api/coercion_test.clj | 200 +++ .../test/compojure/api/common_test.clj | 28 + .../compojure/api/compojure_perf_test.clj | 126 ++ .../compojure1/test/compojure/api/dev/gen.clj | 194 +++ .../test/compojure/api/exception_test.clj | 14 + .../test/compojure/api/integration_test.clj | 1511 +++++++++++++++++ .../test/compojure/api/meta_test.clj | 7 + .../test/compojure/api/middleware_test.clj | 88 + .../test/compojure/api/perf_test.clj | 237 +++ .../test/compojure/api/resource_test.clj | 206 +++ .../test/compojure/api/routes_test.clj | 152 ++ .../compojure/api/swagger_ordering_test.clj | 36 + .../test/compojure/api/swagger_test.clj | 187 ++ .../test/compojure/api/sweet_test.clj | 209 +++ .../test/compojure/api/test_domain.clj | 17 + .../test/compojure/api/test_utils.clj | 103 ++ test/compojure/api/integration_test.clj | 105 +- 27 files changed, 3792 insertions(+), 111 deletions(-) create mode 100644 test-suites/compojure1/.lein-repl-history create mode 100644 test-suites/compojure1/project.clj create mode 100644 test-suites/compojure1/target/classes/META-INF/maven/metosin/compojure-api/pom.properties create mode 100644 test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies create mode 100644 test-suites/compojure1/test/compojure/api/coercion_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/common_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/compojure_perf_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/dev/gen.clj create mode 100644 test-suites/compojure1/test/compojure/api/exception_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/integration_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/meta_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/middleware_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/perf_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/resource_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/routes_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/swagger_ordering_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/swagger_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/sweet_test.clj create mode 100644 test-suites/compojure1/test/compojure/api/test_domain.clj create mode 100644 test-suites/compojure1/test/compojure/api/test_utils.clj diff --git a/project.clj b/project.clj index 3a8072e9..2e1f0add 100644 --- a/project.clj +++ b/project.clj @@ -12,6 +12,10 @@ [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"] diff --git a/src/compojure/api/exception.clj b/src/compojure/api/exception.clj index cc281da6..a8512790 100644 --- a/src/compojure/api/exception.clj +++ b/src/compojure/api/exception.clj @@ -3,7 +3,21 @@ [clojure.walk :as walk] [compojure.api.impl.logging :as logging] [compojure.api.coercion.core :as cc] - [compojure.api.coercion.schema])) + [compojure.api.coercion.schema] + [schema.utils :as su]) + (:import [schema.utils ValidationError NamedError])) + +;; 1.1.x +(defn stringify-error + "Stringifies symbols and validation errors in Schema error, keeping the structure intact." + [error] + (walk/postwalk + (fn [x] + (cond + (instance? ValidationError x) (str (su/validation-error-explain x)) + (instance? NamedError x) (str (su/named-error-explain x)) + :else x)) + error)) ;; ;; Default exception handlers diff --git a/src/compojure/api/meta.clj b/src/compojure/api/meta.clj index 19398026..34f9b112 100644 --- a/src/compojure/api/meta.clj +++ b/src/compojure/api/meta.clj @@ -659,6 +659,12 @@ (defn- route-args? [arg] (not= arg [])) +(defn- resolve-var [&env sym] + (when (symbol? sym) + (let [v (resolve &env sym)] + (when (var? v) + v)))) + (def endpoint-vars (into #{} (mapcat (fn [n] (map #(symbol (name %) (name n)) @@ -725,31 +731,11 @@ (defn- static-middleware? [&env body] (and (seq? body) (boolean - (let [sym (first body)] - (when (symbol? sym) - (when-some [v (resolve &env sym)] - (when (var? v) - (when (middleware-vars (symbol v)) - (let [[_ path route-arg & args] body - [options body] (extract-parameters args true) - [path-string lets arg-with-request] (destructure-compojure-api-request path route-arg) - {:keys [lets - letks - responses - middleware - info - swagger - body]} (reduce - (fn [acc [k v]] - (restructure-param k v (update-in acc [:parameters] dissoc k))) - {:lets lets - :letks [] - :responses nil - :middleware [] - :info {} - :body body} - options)] - (static-body? &env body)))))))))) + (when-some [v (resolve-var &env (first body))] + (when (middleware-vars (symbol v)) + (let [[_ mid & body] body] + (and (static-form? &env mid) + (static-body? &env body)))))))) (def route-middleware-vars (into #{} (mapcat (fn [n] @@ -786,12 +772,6 @@ (= sym 'if)) (static-body? &env (next form))))))))) -(defn- resolve-var [&env sym] - (when (symbol? sym) - (let [v (resolve &env sym)] - (when (var? v) - v)))) - (defn- static-resolved-form? [&env form] (boolean (or (and (seq? form) @@ -987,7 +967,7 @@ (let [coach (some-> (System/getProperty "compojure.api.meta.static-context-coach") edn/read-string)] (if-not coach - (when (ffirst (reset-vals! warned-non-static? true)) + (when (first (reset-vals! warned-non-static? true)) (println (str (format "WARNING: Performance issue detected with compojure-api usage in %s.\n" (ns-name *ns*)) "To fix this warning, set: -Dcompojure.api.meta.static-context-coach={:default :print}.\n" diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj index 8cdf83b5..41c1ae5c 100644 --- a/src/compojure/api/middleware.clj +++ b/src/compojure/api/middleware.clj @@ -5,6 +5,13 @@ [compojure.api.coercion :as coercion] [compojure.api.request :as request] [compojure.api.impl.logging :as logging] + + [ring.middleware.format-params :refer [wrap-restful-params]] + [ring.middleware.format-response :refer [wrap-restful-response]] + [ring.swagger.coerce :as coerce] + + ring.middleware.http-response + [ring.swagger.middleware :as rsm] [ring.middleware.keyword-params :refer [wrap-keyword-params]] [ring.middleware.nested-params :refer [wrap-nested-params]] [ring.middleware.params :refer [wrap-params]] @@ -15,6 +22,8 @@ [ring.swagger.common :as rsc] [ring.util.http-response :refer :all]) (:import [clojure.lang ArityException] + [org.yaml.snakeyaml.parser ParserException] + [com.fasterxml.jackson.core JsonParseException] [com.fasterxml.jackson.datatype.joda JodaModule])) ;; @@ -84,6 +93,14 @@ (defn get-components [req] (::components req)) +;; 1.1.x + +(def coercion-request-ks [::options :coercion]) + +(defn wrap-coercion [handler coercion] + (fn [request] + (handler (assoc-in request coercion-request-ks coercion)))) + ;; ;; Options ;; @@ -195,7 +212,15 @@ ;; Api Middleware ;; -(def api-middleware-defaults +(def default-coercion-matchers + {:body coerce/json-schema-coercion-matcher + :string coerce/query-schema-coercion-matcher + :response coerce/json-schema-coercion-matcher}) + +(def no-response-coercion + (constantly (dissoc default-coercion-matchers :response))) + +(def ^:private muuntaja-api-middleware-defaults {:formats ::default :exceptions {:handlers {:ring.util.http-response/response ex/http-response-handler ::ex/request-validation ex/request-validation-handler @@ -206,8 +231,147 @@ :coercion coercion/default-coercion :ring-swagger nil}) +(def api-middleware-defaults + {:format {:formats [:json-kw :yaml-kw :edn :transit-json :transit-msgpack] + :params-opts {} + :response-opts {}} + :exceptions {:handlers {::ex/request-validation ex/request-validation-handler + ::ex/request-parsing ex/request-parsing-handler + ::ex/response-validation ex/response-validation-handler + ::ex/default ex/safe-handler}} + :coercion (constantly default-coercion-matchers) + :ring-swagger nil}) + + (defn api-middleware-options [options] - (rsc/deep-merge api-middleware-defaults options)) + (rsc/deep-merge + (if (contains? options :format) + api-middleware-defaults) + options)) + +(defn check-options! [options] + ; Break at compile time if there are deprecated options + ; These three have been deprecated with 0.23 + (assert (not (:error-handler (:validation-errors options))) + (str "ERROR: Option: [:validation-errors :error-handler] is no longer supported, " + "use {:exceptions {:handlers {:compojure.api.middleware/request-validation your-handler}}} instead." + "Also note that exception-handler arity has been changed.")) + (assert (not (:catch-core-errors? (:validation-errors options))) + (str "ERROR: Option [:validation-errors :catch-core-errors?] is no longer supported, " + "use {:exceptions {:handlers {:schema.core/error compojure.api.exception/schema-error-handler}}} instead." + "Also note that exception-handler arity has been changed.")) + (assert (not (:exception-handler (:exceptions options))) + (str "ERROR: Option [:exceptions :exception-handler] is no longer supported, " + "use {:exceptions {:handlers {:compojure.api.exception/default your-handler}}} instead." + "Also note that exception-handler arity has been changed.")) + (assert (not (map? (:coercion options))) + (str "ERROR: Option [:coercion] should be a funtion of request->type->matcher, got a map instead." + "From 1.0.0 onwards, you should wrap your type->matcher map into a request-> function. If you " + "want to apply the matchers for all request types, wrap your option with 'constantly'")) + ;; 2.0.0+ + (assert (not (and (contains? options :format) + (contains? options :formats))) + (str "ERROR: Option [:format] is for ring-middleware-format\n" + "and [:formats] is for Muuntaja. At most one can be provided.\n" + "See [[api-middleware]] documentation for more details.\n"))) + +;; ring-middleware-format +(def ^:private default-mime-types + {:json "application/json" + :json-kw "application/json" + :edn "application/edn" + :clojure "application/clojure" + :yaml "application/x-yaml" + :yaml-kw "application/x-yaml" + :yaml-in-html "text/html" + :transit-json "application/transit+json" + :transit-msgpack "application/transit+msgpack"}) + +(defn mime-types + [format] + (get default-mime-types format + (some-> format :content-type))) + +(def ^:private response-only-mimes #{:clojure :yaml-in-html}) + +(defn ->mime-types [formats] (keep mime-types formats)) + +(defn handle-req-error [^Throwable e handler request] + ;; Ring-middleware-format catches all exceptions in req handling, + ;; i.e. (handler req) is inside try-catch. If r-m-f was changed to catch only + ;; exceptions from parsing the request, we wouldn't need to check the exception class. + (if (or (instance? JsonParseException e) (instance? ParserException e)) + (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} e)) + (throw e))) + +(defn serializable? + "Predicate which returns true if the response body is serializable. + That is, return type is set by :return compojure-api key or it's + a collection." + [_ {:keys [body] :as response}] + (when response + (or (:compojure.api.meta/serializable? response) + (coll? body)))) + +(defn wrap-options + "Injects compojure-api options into the request." + [handler options] + (fn [request] + (handler (update-in request [::options] merge options)))) + +(defn- ring-middleware-format-api-middleware + [handler options] + (let [{:keys [exceptions format components]} options + {:keys [formats params-opts response-opts]} format] + (cond-> handler + components (wrap-components components) + true ring.middleware.http-response/wrap-http-response + (seq formats) (rsm/wrap-swagger-data {:produces (->mime-types (remove response-only-mimes formats)) + :consumes (->mime-types formats)}) + true (wrap-options (select-keys options [:ring-swagger :coercion])) + (seq formats) (wrap-restful-params {:formats (remove response-only-mimes formats) + :handle-error handle-req-error + :format-options params-opts}) + exceptions (wrap-exceptions exceptions) + (seq formats) (wrap-restful-response {:formats formats + :predicate serializable? + :format-options response-opts}) + true wrap-keyword-params + true wrap-nested-params + true wrap-params))) + +(defn- muuntaja-api-middleware + [handler options] + (let [{:keys [exceptions components formats middleware ring-swagger coercion]} options + muuntaja (create-muuntaja formats)] + (-> handler + (cond-> middleware ((compose-middleware middleware))) + (cond-> components (wrap-components components)) + (cond-> muuntaja (wrap-swagger-data {:consumes (m/decodes muuntaja) + :produces (m/encodes muuntaja)})) + (wrap-inject-data + (cond-> {::request/coercion coercion} + muuntaja (assoc ::request/muuntaja muuntaja) + ring-swagger (assoc ::request/ring-swagger ring-swagger))) + (cond-> muuntaja (muuntaja.middleware/wrap-params)) + ;; all but request-parsing exceptions (to make :body-params visible) + (cond-> exceptions (wrap-exceptions + (update exceptions :handlers dissoc ::ex/request-parsing))) + (cond-> muuntaja (muuntaja.middleware/wrap-format-request muuntaja)) + ;; just request-parsing exceptions + (cond-> exceptions (wrap-exceptions + (update exceptions :handlers select-keys [::ex/request-parsing]))) + (cond-> muuntaja (muuntaja.middleware/wrap-format-response muuntaja)) + (cond-> muuntaja (muuntaja.middleware/wrap-format-negotiate muuntaja)) + + ;; these are really slow middleware, 4.5µs => 9.1µs (+100%) + + ;; 7.8µs => 9.1µs (+27%) + wrap-keyword-params + ;; 7.1µs => 7.8µs (+23%) + wrap-nested-params + ;; 4.5µs => 7.1µs (+50%) + wrap-params))) ;; TODO: test all options! (https://github.com/metosin/compojure-api/issues/137) (defn api-middleware @@ -240,6 +404,14 @@ - **:formats** for Muuntaja middleware. Value can be a valid muuntaja options-map, a Muuntaja instance or nil (to unmount it). See https://github.com/metosin/muuntaja/blob/master/doc/Configuration.md for details. + Incompatible with :format. + + - **:format** for ring-middleware-format middleware (nil to unmount it). Incompatible with :formats. + - **:formats** sequence of supported formats, e.g. `[:json-kw :edn]` + - **:params-opts** for *ring.middleware.format-params/wrap-restful-params*, + e.g. `{:transit-json {:handlers readers}}` + - **:response-opts** for *ring.middleware.format-params/wrap-restful-response*, + e.g. `{:transit-json {:handlers writers}}` - **:middleware** vector of extra middleware to be applied last (just before the handler). @@ -258,45 +430,12 @@ ([handler] (api-middleware handler api-middleware-defaults)) ([handler options] - (let [options (api-middleware-options options) - {:keys [exceptions components formats middleware ring-swagger coercion]} options - muuntaja (create-muuntaja formats)] - - ;; 1.2.0+ - (assert (not (contains? options :format)) - (str "ERROR: Option [:format] is not used with 2.* version.\n" - "Compojure-api uses now Muuntaja insted of ring-middleware-format,\n" - "the new formatting options for it should be under [:formats]. See\n" - "[[api-middleware]] documentation for more details.\n")) - - (-> handler - (cond-> middleware ((compose-middleware middleware))) - (cond-> components (wrap-components components)) - (cond-> muuntaja (wrap-swagger-data {:consumes (m/decodes muuntaja) - :produces (m/encodes muuntaja)})) - (wrap-inject-data - (cond-> {::request/coercion coercion} - muuntaja (assoc ::request/muuntaja muuntaja) - ring-swagger (assoc ::request/ring-swagger ring-swagger))) - (cond-> muuntaja (muuntaja.middleware/wrap-params)) - ;; all but request-parsing exceptions (to make :body-params visible) - (cond-> exceptions (wrap-exceptions - (update exceptions :handlers dissoc ::ex/request-parsing))) - (cond-> muuntaja (muuntaja.middleware/wrap-format-request muuntaja)) - ;; just request-parsing exceptions - (cond-> exceptions (wrap-exceptions - (update exceptions :handlers select-keys [::ex/request-parsing]))) - (cond-> muuntaja (muuntaja.middleware/wrap-format-response muuntaja)) - (cond-> muuntaja (muuntaja.middleware/wrap-format-negotiate muuntaja)) - - ;; these are really slow middleware, 4.5µs => 9.1µs (+100%) - - ;; 7.8µs => 9.1µs (+27%) - wrap-keyword-params - ;; 7.1µs => 7.8µs (+23%) - wrap-nested-params - ;; 4.5µs => 7.1µs (+50%) - wrap-params)))) + (let [options (doto (api-middleware-options options) + check-options!)] + ((if (:format options) + ring-middleware-format-api-middleware + muuntaja-api-middleware) + handler options)))) (defn wrap-format "Muuntaja format middleware. Can be safely mounted on top of multiple api diff --git a/src/compojure/api/resource.clj b/src/compojure/api/resource.clj index ec74a061..66d2c156 100644 --- a/src/compojure/api/resource.clj +++ b/src/compojure/api/resource.clj @@ -120,6 +120,15 @@ ([request respond raise] (handle-async info request respond raise)))) +(defn- create-handler1 [info {:keys [coercion]}] + (fn [{:keys [request-method] :as request}] + (let [request (if coercion (assoc-in request mw/coercion-request-ks coercion) request) + ks (if (contains? info request-method) [request-method] [])] + (if-let [handler (resolve-handler info request-method)] + (-> (coerce-request request info ks) + handler + (coerce-response info request ks)))))) + (defn- merge-parameters-and-responses [info] (let [methods (select-keys info (:methods +mappings+))] (-> info @@ -203,13 +212,21 @@ :post {} :handler (constantly (internal-server-error {:reason \"not implemented\"}))})" - [data] - (let [data (merge-parameters-and-responses data) - public-info (swaggerize (public-root-info data)) - info (merge {:public public-info} (select-keys data [:coercion])) - childs (create-childs data) - handler (create-handler data)] - (routes/map->Route - {:info info - :childs childs - :handler handler}))) + ;1.1.x + ([info options] + (let [info (merge-parameters-and-responses info) + root-info (swaggerize (public-root-info info)) + childs (create-childs info) + handler (create-handler1 info options)] + (routes/create nil nil root-info childs handler))) + ;2.x + ([data] + (let [data (merge-parameters-and-responses data) + public-info (swaggerize (public-root-info data)) + info (merge {:public public-info} (select-keys data [:coercion])) + childs (create-childs data) + handler (create-handler data)] + (routes/map->Route + {:info info + :childs childs + :handler handler})))) diff --git a/src/compojure/api/routes.clj b/src/compojure/api/routes.clj index a468d326..248a4ef3 100644 --- a/src/compojure/api/routes.clj +++ b/src/compojure/api/routes.clj @@ -1,6 +1,7 @@ (ns compojure.api.routes (:require [compojure.core :refer :all] [clojure.string :as string] + [cheshire.core :as cjson] [compojure.api.methods :as methods] [compojure.api.request :as request] [compojure.api.impl.logging :as logging] @@ -145,16 +146,22 @@ {:paths (reduce (fn [acc [path method info]] - (if-not (:no-doc info) - (if-let [public-info (->> (get info :public {}) - (coercion/get-apidocs (:coercion info) "swagger"))] - (update-in - acc [path method] - (fn [old-info] - (let [public-info (or old-info public-info)] - (ensure-path-parameters path public-info)))) - acc) - acc)) + (if (fn? (:coercion info)) ;; 1.1.x + (update-in + acc [path method] + (fn [old-info] + (let [info (or old-info info)] + (ensure-path-parameters path info)))) + (if-not (:no-doc info) + (if-let [public-info (->> (get info :public {}) + (coercion/get-apidocs (:coercion info) "swagger"))] + (update-in + acc [path method] + (fn [old-info] + (let [public-info (or old-info public-info)] + (ensure-path-parameters path public-info)))) + acc) + acc))) (linked/map) routes)}) @@ -209,22 +216,27 @@ (defn- un-quote [s] (str/replace s #"^\"(.+(?=\"$))\"$" "$1")) -(defn- path-string [m s params] - (-> s - (str/replace #":([^/]+)" " :$1 ") - (str/split #" ") - (->> (map - (fn [[head :as token]] - (if (= head \:) - (let [key (keyword (subs token 1)) - value (key params)] - (if value - (un-quote (slurp (m/encode m "application/json" value))) - (throw - (IllegalArgumentException. - (str "Missing path-parameter " key " for path " s))))) - token))) - (apply str)))) +(defn- path-string + ([s params] (path-string nil s params)) + ([m s params] + (-> s + (str/replace #":([^/]+)" " :$1 ") + (str/split #" ") + (->> (map + (fn [[head :as token]] + (if (= head \:) + (let [key (keyword (subs token 1)) + value (key params)] + (if value + (un-quote (if m + (slurp (m/encode m "application/json" value)) + ;;1.1.x + (cjson/generate-string value))) + (throw + (IllegalArgumentException. + (str "Missing path-parameter " key " for path " s))))) + token))) + (apply str))))) (defn path-for* "Extracts the lookup-table from request and finds a route by name." 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/target/classes/META-INF/maven/metosin/compojure-api/pom.properties b/test-suites/compojure1/target/classes/META-INF/maven/metosin/compojure-api/pom.properties new file mode 100644 index 00000000..538845c2 --- /dev/null +++ b/test-suites/compojure1/target/classes/META-INF/maven/metosin/compojure-api/pom.properties @@ -0,0 +1,3 @@ +artifactId=compojure-api +groupId=metosin +version=1.1.14-SNAPSHOT diff --git a/test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies b/test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies new file mode 100644 index 00000000..68f92ce8 --- /dev/null +++ b/test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies @@ -0,0 +1 @@ +[{:dependencies {com.cognitect/transit-java {:vsn "0.8.337", :native-prefix nil}, org.clojure/clojure {:vsn "1.10.1", :native-prefix nil}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil}, metosin/ring-http-response {:vsn "0.9.1", :native-prefix nil}, org.clojure/core.specs.alpha {:vsn "0.2.44", :native-prefix nil}, metosin/muuntaja {:vsn "0.6.6", :native-prefix nil}, marick/suchwow {:vsn "6.0.2", :native-prefix nil}, metosin/ring-swagger-ui {:vsn "3.24.3", :native-prefix nil}, com.fasterxml.jackson.core/jackson-databind {:vsn "2.10.1", :native-prefix nil}, flare {:vsn "0.2.9", :native-prefix nil}, org.clojure/spec.alpha {:vsn "0.2.176", :native-prefix nil}, clj-time {:vsn "0.15.2", :native-prefix nil}, mvxcvi/puget {:vsn "1.1.2", :native-prefix nil}, crypto-random {:vsn "1.2.0", :native-prefix nil}, javax.activation/activation {:vsn "1.1", :native-prefix nil}, metosin/spec-tools {:vsn "0.10.0", :native-prefix nil}, metosin/scjsv {:vsn "0.5.0", :native-prefix nil}, compojure {:vsn "1.6.1", :native-prefix nil}, riddley {:vsn "0.2.0", :native-prefix nil}, commons-fileupload {:vsn "1.4", :native-prefix nil}, org.clojure/tools.macro {:vsn "0.1.5", :native-prefix nil}, com.github.java-json-tools/jackson-coreutils {:vsn "1.9", :native-prefix nil}, com.fasterxml.jackson.dataformat/jackson-dataformat-cbor {:vsn "2.9.6", :native-prefix nil}, org.flatland/useful {:vsn "0.11.6", :native-prefix nil}, com.googlecode.json-simple/json-simple {:vsn "1.1.1", :native-prefix nil}, environ {:vsn "1.1.0", :native-prefix nil}, io.aviso/pretty {:vsn "0.1.37", :native-prefix nil}, com.github.fge/msg-simple {:vsn "1.1", :native-prefix nil}, fipp {:vsn "0.6.17", :native-prefix nil}, com.github.fge/btf {:vsn "1.2", :native-prefix nil}, peridot {:vsn "0.5.1", :native-prefix nil}, com.stuartsierra/component {:vsn "0.4.0", :native-prefix nil}, org.flatland/ordered {:vsn "1.5.7", :native-prefix nil}, medley {:vsn "1.0.0", :native-prefix nil}, org.clojure/tools.namespace {:vsn "0.3.0", :native-prefix nil}, com.fasterxml.jackson.core/jackson-core {:vsn "2.10.1", :native-prefix nil}, http-kit {:vsn "2.3.0", :native-prefix nil}, de.kotka/lazymap {:vsn "3.1.0", :native-prefix nil}, slingshot {:vsn "0.12.2", :native-prefix nil}, org.yaml/snakeyaml {:vsn "1.24", :native-prefix nil}, ring-mock {:vsn "0.1.5", :native-prefix nil}, org.tobereplaced/lettercase {:vsn "1.0.0", :native-prefix nil}, metosin/ring-swagger {:vsn "0.26.2", :native-prefix nil}, javax.mail/mailapi {:vsn "1.4.3", :native-prefix nil}, cheshire {:vsn "5.8.1", :native-prefix nil}, org.apache.httpcomponents/httpcore {:vsn "4.4.5", :native-prefix nil}, mvxcvi/arrangement {:vsn "1.2.0", :native-prefix nil}, potemkin {:vsn "0.4.5", :native-prefix nil}, clojure-msgpack {:vsn "1.2.1", :native-prefix nil}, colorize {:vsn "0.1.1", :native-prefix nil}, org.mozilla/rhino {:vsn "1.7.7.1", :native-prefix nil}, com.github.fge/uri-template {:vsn "0.9", :native-prefix nil}, crypto-equality {:vsn "1.0.0", :native-prefix nil}, com.fasterxml.jackson.core/jackson-annotations {:vsn "2.10.1", :native-prefix nil}, com.github.java-json-tools/json-schema-validator {:vsn "2.2.10", :native-prefix nil}, suspendable {:vsn "0.1.1", :native-prefix nil}, org.clojure/core.unify {:vsn "0.5.7", :native-prefix nil}, org.javassist/javassist {:vsn "3.18.1-GA", :native-prefix nil}, frankiesardo/linked {:vsn "1.3.0", :native-prefix nil}, org.clojure/java.classpath {:vsn "0.2.3", :native-prefix nil}, commons-codec {:vsn "1.11", :native-prefix nil}, com.google.guava/guava {:vsn "16.0.1", :native-prefix nil}, com.googlecode.libphonenumber/libphonenumber {:vsn "8.0.0", :native-prefix nil}, org.msgpack/msgpack {:vsn "0.6.12", :native-prefix nil}, reloaded.repl {:vsn "0.2.4", :native-prefix nil}, com.cognitect/transit-clj {:vsn "0.8.319", :native-prefix nil}, com.fasterxml.jackson.datatype/jackson-datatype-joda {:vsn "2.10.1", :native-prefix nil}, prismatic/plumbing {:vsn "0.5.5", :native-prefix nil}, clj-commons/clj-yaml {:vsn "0.7.0", :native-prefix nil}, ring/ring-codec {:vsn "1.1.2", :native-prefix nil}, org.apache.httpcomponents/httpclient {:vsn "4.5.1", :native-prefix nil}, metosin/schema-tools {:vsn "0.11.0", :native-prefix nil}, org.clojure/core.rrb-vector {:vsn "0.0.14", :native-prefix nil}, prismatic/schema {:vsn "1.1.12", :native-prefix nil}, instaparse {:vsn "1.4.8", :native-prefix nil}, org.clojure/math.combinatorics {:vsn "0.1.5", :native-prefix nil}, clout {:vsn "2.2.1", :native-prefix nil}, com.github.java-json-tools/json-schema-core {:vsn "1.2.10", :native-prefix nil}, org.clojure/tools.reader {:vsn "1.3.2", :native-prefix nil}, ikitommi/linked {:vsn "1.3.1-alpha1", :native-prefix nil}, joda-time {:vsn "2.10.5", :native-prefix nil}, org.tcrawley/dynapath {:vsn "1.0.0", :native-prefix nil}, nrepl {:vsn "0.8.3", :native-prefix nil}, tigris {:vsn "0.1.1", :native-prefix nil}, javax.servlet/servlet-api {:vsn "2.5", :native-prefix nil}, com.rpl/specter {:vsn "1.0.5", :native-prefix nil}, criterium {:vsn "0.4.5", :native-prefix nil}, org.clojure/test.check {:vsn "0.10.0-alpha3", :native-prefix nil}, clj-tuple {:vsn "0.2.2", :native-prefix nil}, metosin/jsonista {:vsn "0.2.5", :native-prefix nil}, org.clojure/core.memoize {:vsn "0.8.2", :native-prefix nil}, com.stuartsierra/dependency {:vsn "0.2.0", :native-prefix nil}, net.sf.jopt-simple/jopt-simple {:vsn "5.0.3", :native-prefix nil}, org.clojure/data.priority-map {:vsn "0.0.7", :native-prefix nil}, commons-io {:vsn "2.6", :native-prefix nil}, org.apache.httpcomponents/httpmime {:vsn "4.5.1", :native-prefix nil}, com.google.code.findbugs/jsr305 {:vsn "3.0.1", :native-prefix nil}, ring/ring-core {:vsn "1.8.0", :native-prefix nil}, org.clojure/core.cache {:vsn "0.8.2", :native-prefix nil}, ring-middleware-format {:vsn "0.7.4", :native-prefix nil}, midje {:vsn "1.9.9", :native-prefix nil}, com.fasterxml.jackson.dataformat/jackson-dataformat-smile {:vsn "2.9.6", :native-prefix nil}, com.fasterxml.jackson.datatype/jackson-datatype-jsr310 {:vsn "2.10.0", :native-prefix nil}, org.clojure/data.codec {:vsn "0.1.0", :native-prefix nil}, javax.xml.bind/jaxb-api {:vsn "2.3.0", :native-prefix nil}, org.clojars.brenton/google-diff-match-patch {:vsn "0.1", :native-prefix nil}}, :native-path "target/native"} {:native-path "target/native", :dependencies {com.cognitect/transit-java {:vsn "0.8.337", :native-prefix nil, :native? false}, org.clojure/clojure {:vsn "1.10.1", :native-prefix nil, :native? false}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil, :native? false}, metosin/ring-http-response {:vsn "0.9.1", :native-prefix nil, :native? false}, org.clojure/core.specs.alpha {:vsn "0.2.44", :native-prefix nil, :native? false}, metosin/muuntaja {:vsn "0.6.6", :native-prefix nil, :native? false}, marick/suchwow {:vsn "6.0.2", :native-prefix nil, :native? false}, metosin/ring-swagger-ui {:vsn "3.24.3", :native-prefix nil, :native? false}, com.fasterxml.jackson.core/jackson-databind {:vsn "2.10.1", :native-prefix nil, :native? false}, flare {:vsn "0.2.9", :native-prefix nil, :native? false}, org.clojure/spec.alpha {:vsn "0.2.176", :native-prefix nil, :native? false}, clj-time {:vsn "0.15.2", :native-prefix nil, :native? false}, mvxcvi/puget {:vsn "1.1.2", :native-prefix nil, :native? false}, crypto-random {:vsn "1.2.0", :native-prefix nil, :native? false}, javax.activation/activation {:vsn "1.1", :native-prefix nil, :native? false}, metosin/spec-tools {:vsn "0.10.0", :native-prefix nil, :native? false}, metosin/scjsv {:vsn "0.5.0", :native-prefix nil, :native? false}, compojure {:vsn "1.6.1", :native-prefix nil, :native? false}, riddley {:vsn "0.2.0", :native-prefix nil, :native? false}, commons-fileupload {:vsn "1.4", :native-prefix nil, :native? false}, org.clojure/tools.macro {:vsn "0.1.5", :native-prefix nil, :native? false}, com.github.java-json-tools/jackson-coreutils {:vsn "1.9", :native-prefix nil, :native? false}, com.fasterxml.jackson.dataformat/jackson-dataformat-cbor {:vsn "2.9.6", :native-prefix nil, :native? false}, org.flatland/useful {:vsn "0.11.6", :native-prefix nil, :native? false}, com.googlecode.json-simple/json-simple {:vsn "1.1.1", :native-prefix nil, :native? false}, environ {:vsn "1.1.0", :native-prefix nil, :native? false}, io.aviso/pretty {:vsn "0.1.37", :native-prefix nil, :native? false}, com.github.fge/msg-simple {:vsn "1.1", :native-prefix nil, :native? false}, fipp {:vsn "0.6.17", :native-prefix nil, :native? false}, com.github.fge/btf {:vsn "1.2", :native-prefix nil, :native? false}, peridot {:vsn "0.5.1", :native-prefix nil, :native? false}, com.stuartsierra/component {:vsn "0.4.0", :native-prefix nil, :native? false}, org.flatland/ordered {:vsn "1.5.7", :native-prefix nil, :native? false}, medley {:vsn "1.0.0", :native-prefix nil, :native? false}, org.clojure/tools.namespace {:vsn "0.3.0", :native-prefix nil, :native? false}, com.fasterxml.jackson.core/jackson-core {:vsn "2.10.1", :native-prefix nil, :native? false}, http-kit {:vsn "2.3.0", :native-prefix nil, :native? false}, de.kotka/lazymap {:vsn "3.1.0", :native-prefix nil, :native? false}, slingshot {:vsn "0.12.2", :native-prefix nil, :native? false}, org.yaml/snakeyaml {:vsn "1.24", :native-prefix nil, :native? false}, ring-mock {:vsn "0.1.5", :native-prefix nil, :native? false}, org.tobereplaced/lettercase {:vsn "1.0.0", :native-prefix nil, :native? false}, metosin/ring-swagger {:vsn "0.26.2", :native-prefix nil, :native? false}, javax.mail/mailapi {:vsn "1.4.3", :native-prefix nil, :native? false}, cheshire {:vsn "5.8.1", :native-prefix nil, :native? false}, org.apache.httpcomponents/httpcore {:vsn "4.4.5", :native-prefix nil, :native? false}, mvxcvi/arrangement {:vsn "1.2.0", :native-prefix nil, :native? false}, potemkin {:vsn "0.4.5", :native-prefix nil, :native? false}, clojure-msgpack {:vsn "1.2.1", :native-prefix nil, :native? false}, colorize {:vsn "0.1.1", :native-prefix nil, :native? false}, org.mozilla/rhino {:vsn "1.7.7.1", :native-prefix nil, :native? false}, com.github.fge/uri-template {:vsn "0.9", :native-prefix nil, :native? false}, crypto-equality {:vsn "1.0.0", :native-prefix nil, :native? false}, com.fasterxml.jackson.core/jackson-annotations {:vsn "2.10.1", :native-prefix nil, :native? false}, com.github.java-json-tools/json-schema-validator {:vsn "2.2.10", :native-prefix nil, :native? false}, suspendable {:vsn "0.1.1", :native-prefix nil, :native? false}, org.clojure/core.unify {:vsn "0.5.7", :native-prefix nil, :native? false}, org.javassist/javassist {:vsn "3.18.1-GA", :native-prefix nil, :native? false}, frankiesardo/linked {:vsn "1.3.0", :native-prefix nil, :native? false}, org.clojure/java.classpath {:vsn "0.2.3", :native-prefix nil, :native? false}, commons-codec {:vsn "1.11", :native-prefix nil, :native? false}, com.google.guava/guava {:vsn "16.0.1", :native-prefix nil, :native? false}, com.googlecode.libphonenumber/libphonenumber {:vsn "8.0.0", :native-prefix nil, :native? false}, org.msgpack/msgpack {:vsn "0.6.12", :native-prefix nil, :native? false}, reloaded.repl {:vsn "0.2.4", :native-prefix nil, :native? false}, com.cognitect/transit-clj {:vsn "0.8.319", :native-prefix nil, :native? false}, com.fasterxml.jackson.datatype/jackson-datatype-joda {:vsn "2.10.1", :native-prefix nil, :native? false}, prismatic/plumbing {:vsn "0.5.5", :native-prefix nil, :native? false}, clj-commons/clj-yaml {:vsn "0.7.0", :native-prefix nil, :native? false}, ring/ring-codec {:vsn "1.1.2", :native-prefix nil, :native? false}, org.apache.httpcomponents/httpclient {:vsn "4.5.1", :native-prefix nil, :native? false}, metosin/schema-tools {:vsn "0.11.0", :native-prefix nil, :native? false}, org.clojure/core.rrb-vector {:vsn "0.0.14", :native-prefix nil, :native? false}, prismatic/schema {:vsn "1.1.12", :native-prefix nil, :native? false}, instaparse {:vsn "1.4.8", :native-prefix nil, :native? false}, org.clojure/math.combinatorics {:vsn "0.1.5", :native-prefix nil, :native? false}, clout {:vsn "2.2.1", :native-prefix nil, :native? false}, com.github.java-json-tools/json-schema-core {:vsn "1.2.10", :native-prefix nil, :native? false}, org.clojure/tools.reader {:vsn "1.3.2", :native-prefix nil, :native? false}, ikitommi/linked {:vsn "1.3.1-alpha1", :native-prefix nil, :native? false}, joda-time {:vsn "2.10.5", :native-prefix nil, :native? false}, org.tcrawley/dynapath {:vsn "1.0.0", :native-prefix nil, :native? false}, nrepl {:vsn "0.8.3", :native-prefix nil, :native? false}, tigris {:vsn "0.1.1", :native-prefix nil, :native? false}, javax.servlet/servlet-api {:vsn "2.5", :native-prefix nil, :native? false}, com.rpl/specter {:vsn "1.0.5", :native-prefix nil, :native? false}, criterium {:vsn "0.4.5", :native-prefix nil, :native? false}, org.clojure/test.check {:vsn "0.10.0-alpha3", :native-prefix nil, :native? false}, clj-tuple {:vsn "0.2.2", :native-prefix nil, :native? false}, metosin/jsonista {:vsn "0.2.5", :native-prefix nil, :native? false}, org.clojure/core.memoize {:vsn "0.8.2", :native-prefix nil, :native? false}, com.stuartsierra/dependency {:vsn "0.2.0", :native-prefix nil, :native? false}, net.sf.jopt-simple/jopt-simple {:vsn "5.0.3", :native-prefix nil, :native? false}, org.clojure/data.priority-map {:vsn "0.0.7", :native-prefix nil, :native? false}, commons-io {:vsn "2.6", :native-prefix nil, :native? false}, org.apache.httpcomponents/httpmime {:vsn "4.5.1", :native-prefix nil, :native? false}, com.google.code.findbugs/jsr305 {:vsn "3.0.1", :native-prefix nil, :native? false}, ring/ring-core {:vsn "1.8.0", :native-prefix nil, :native? false}, org.clojure/core.cache {:vsn "0.8.2", :native-prefix nil, :native? false}, ring-middleware-format {:vsn "0.7.4", :native-prefix nil, :native? false}, midje {:vsn "1.9.9", :native-prefix nil, :native? false}, com.fasterxml.jackson.dataformat/jackson-dataformat-smile {:vsn "2.9.6", :native-prefix nil, :native? false}, com.fasterxml.jackson.datatype/jackson-datatype-jsr310 {:vsn "2.10.0", :native-prefix nil, :native? false}, org.clojure/data.codec {:vsn "0.1.0", :native-prefix nil, :native? false}, javax.xml.bind/jaxb-api {:vsn "2.3.0", :native-prefix nil, :native? false}, org.clojars.brenton/google-diff-match-patch {:vsn "0.1", :native-prefix nil, :native? false}}}] \ No newline at end of file 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/dev/gen.clj b/test-suites/compojure1/test/compojure/api/dev/gen.clj new file mode 100644 index 00000000..b3002079 --- /dev/null +++ b/test-suites/compojure1/test/compojure/api/dev/gen.clj @@ -0,0 +1,194 @@ +(ns compojure.api.dev.gen + (:require [clojure.string :as str] + [clojure.set :as set] + [clojure.walk :as walk])) + +(def impl-local-sym '+impl+) + +(defn normalize-argv [argv] + {:post [(or (empty? %) + (apply distinct? %)) + (not-any? #{impl-local-sym} %)]} + (into [] (map-indexed (fn [i arg] + (if (symbol? arg) + (do (assert (not (namespace arg))) + (if (some #(Character/isDigit (char %)) (name arg)) + (symbol (apply str (concat + (remove #(Character/isDigit (char %)) (name arg)) + [i]))) + arg)) + (symbol (str "arg" i))))) + argv)) + +(defn normalize-arities [arities] + (cond-> arities + (= 1 (count arities)) first)) + +(defn import-fn [sym] + {:pre [(namespace sym)]} + (let [vr (find-var sym) + m (meta vr) + n (:name m) + arglists (:arglists m) + protocol (:protocol m) + when-class (-> sym meta :when-class) + _ (assert (not when-class)) + forward-meta (into (sorted-map) (select-keys m [:tag :arglists :doc :deprecated])) + _ (assert (not= n impl-local-sym)) + _ (when (:macro m) + (throw (IllegalArgumentException. + (str "Calling import-fn on a macro: " sym)))) + form (if protocol + (list* 'defn (with-meta n (dissoc forward-meta :arglists)) + (map (fn [argv] + {:pre [(not-any? #{'&} argv)]} + (list argv (list* sym argv))) + arglists)) + (list 'def (with-meta n forward-meta) sym))] + (cond->> form + #_#_when-class (list 'java-time.util/when-class when-class)))) + +(defn import-macro [sym] + (let [vr (find-var sym) + m (meta vr) + _ (when-not (:macro m) + (throw (IllegalArgumentException. + (str "Calling import-macro on a non-macro: " sym)))) + n (:name m) + arglists (:arglists m)] + (list* 'defmacro n + (concat + (some-> (not-empty (into (sorted-map) (select-keys m [:doc :deprecated]))) + list) + (normalize-arities + (map (fn [argv] + (let [argv (normalize-argv argv)] + (list argv + (if (some #{'&} argv) + (list* 'list* (list 'quote sym) (remove #{'&} argv)) + (list* 'list (list 'quote sym) argv))))) + arglists)))))) + +(defn import-vars + "Imports a list of vars from other namespaces." + [& syms] + (let [unravel (fn unravel [x] + (if (sequential? x) + (->> x + rest + (mapcat unravel) + (map + #(with-meta + (symbol + (str (first x) + (when-let [n (namespace %)] + (str "." n))) + (name %)) + (meta %)))) + [x])) + syms (mapcat unravel syms)] + (map (fn [sym] + (let [vr (if-some [rr (resolve 'clojure.core/requiring-resolve)] + (rr sym) + (do (require (-> sym namespace symbol)) + (resolve sym))) + _ (assert vr (str sym " is unresolvable")) + m (meta vr)] + (if (:macro m) + (import-macro sym) + (import-fn sym)))) + syms))) + +(def compojure-api-sweet-impl-info + {:vars '([compojure.api.core routes defroutes let-routes undocumented middleware route-middleware + context GET ANY HEAD PATCH DELETE OPTIONS POST PUT] + [compojure.api.api api defapi] + [compojure.api.resource resource] + [compojure.api.routes path-for] + [compojure.api.swagger swagger-routes] + [ring.swagger.json-schema describe])}) + +(defn gen-compojure-api-sweet-ns-forms [nsym] + (concat + [";; NOTE: This namespace is generated by compojure.api.dev.gen" + `(~'ns ~nsym + (:require compojure.api.core + compojure.api.api + compojure.api.routes + compojure.api.resource + compojure.api.swagger + ring.swagger.json-schema))] + (apply import-vars (:vars compojure-api-sweet-impl-info)))) + +(def compojure-api-upload-impl-info + {:vars '([ring.middleware.multipart-params wrap-multipart-params] + [ring.swagger.upload TempFileUpload ByteArrayUpload])}) + +(defn gen-compojure-api-upload-ns-forms [nsym] + (concat + [";; NOTE: This namespace is generated by compojure.api.dev.gen" + `(~'ns ~nsym + (:require ring.middleware.multipart-params + ring.swagger.upload))] + (apply import-vars (:vars compojure-api-upload-impl-info)))) + +(defn print-form [form] + (with-bindings + (cond-> {#'*print-meta* true + #'*print-length* nil + #'*print-level* nil} + (resolve '*print-namespace-maps*) + (assoc (resolve '*print-namespace-maps*) false)) + (cond + (string? form) (println form) + :else (println (pr-str (walk/postwalk + (fn [v] + (if (meta v) + (if (symbol? v) + (vary-meta v #(not-empty + (cond-> (sorted-map) + (some? (:tag %)) (assoc :tag (:tag %)) + (some? (:doc %)) (assoc :doc (:doc %)) + ((some-fn true? string?) (:deprecated %)) (assoc :deprecated (:deprecated %)) + (string? (:superseded-by %)) (assoc :superseded-by (:superseded-by %)) + (string? (:supercedes %)) (assoc :supercedes (:supercedes %)) + (some? (:arglists %)) (assoc :arglists (list 'quote (doall (map normalize-argv (:arglists %)))))))) + (with-meta v nil)) + v)) + form))))) + nil) + +(defn print-compojure-api-ns [{:keys [f nsym]}] + (assert f) + (run! print-form (f nsym))) + +(def compojure-api-sweet-nsym + (with-meta + 'compojure.api.sweet + ;;TODO ns meta + nil)) + +(def compojure-api-upload-nsym + (with-meta + 'compojure.api.upload + ;;TODO ns meta + nil)) + +(def compojure-api-sweet-conf {:nsym compojure-api-sweet-nsym + :f #'gen-compojure-api-sweet-ns-forms}) +(def compojure-api-upload-conf {:nsym compojure-api-upload-nsym + :f #'gen-compojure-api-upload-ns-forms}) + +(def gen-source->nsym + {"src/compojure/api/sweet.clj" compojure-api-sweet-conf + "src/compojure/api/upload.clj" compojure-api-upload-conf}) + +(defn spit-compojure-api-ns [] + (doseq [[source conf] gen-source->nsym] + (spit source (with-out-str (print-compojure-api-ns conf))))) + +(comment + (print-compojure-api-ns compojure-api-sweet-conf) + (print-compojure-api-ns compojure-api-upload-conf) + (spit-compojure-api-ns) + ) 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 037bc46e..1fd93963 100644 --- a/test/compojure/api/integration_test.clj +++ b/test/compojure/api/integration_test.clj @@ -523,7 +523,110 @@ (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 ""}}}}}})))) + + (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"})))))) + +(deftest muuntaja-swagger-docs-test (let [app (api {:formats (m/select-formats m/default-options From 0e4f243583a96caa92fb25700950aa9784a6fd4b Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 23 Apr 2024 02:05:07 -0500 Subject: [PATCH 02/18] rm --- test-suites/compojure1/.gitignore | 1 + .../META-INF/maven/metosin/compojure-api/pom.properties | 3 --- .../stale/leiningen.core.classpath.extract-native-dependencies | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 test-suites/compojure1/.gitignore delete mode 100644 test-suites/compojure1/target/classes/META-INF/maven/metosin/compojure-api/pom.properties delete mode 100644 test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies 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/target/classes/META-INF/maven/metosin/compojure-api/pom.properties b/test-suites/compojure1/target/classes/META-INF/maven/metosin/compojure-api/pom.properties deleted file mode 100644 index 538845c2..00000000 --- a/test-suites/compojure1/target/classes/META-INF/maven/metosin/compojure-api/pom.properties +++ /dev/null @@ -1,3 +0,0 @@ -artifactId=compojure-api -groupId=metosin -version=1.1.14-SNAPSHOT diff --git a/test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies b/test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies deleted file mode 100644 index 68f92ce8..00000000 --- a/test-suites/compojure1/target/stale/leiningen.core.classpath.extract-native-dependencies +++ /dev/null @@ -1 +0,0 @@ -[{:dependencies {com.cognitect/transit-java {:vsn "0.8.337", :native-prefix nil}, org.clojure/clojure {:vsn "1.10.1", :native-prefix nil}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil}, metosin/ring-http-response {:vsn "0.9.1", :native-prefix nil}, org.clojure/core.specs.alpha {:vsn "0.2.44", :native-prefix nil}, metosin/muuntaja {:vsn "0.6.6", :native-prefix nil}, marick/suchwow {:vsn "6.0.2", :native-prefix nil}, metosin/ring-swagger-ui {:vsn "3.24.3", :native-prefix nil}, com.fasterxml.jackson.core/jackson-databind {:vsn "2.10.1", :native-prefix nil}, flare {:vsn "0.2.9", :native-prefix nil}, org.clojure/spec.alpha {:vsn "0.2.176", :native-prefix nil}, clj-time {:vsn "0.15.2", :native-prefix nil}, mvxcvi/puget {:vsn "1.1.2", :native-prefix nil}, crypto-random {:vsn "1.2.0", :native-prefix nil}, javax.activation/activation {:vsn "1.1", :native-prefix nil}, metosin/spec-tools {:vsn "0.10.0", :native-prefix nil}, metosin/scjsv {:vsn "0.5.0", :native-prefix nil}, compojure {:vsn "1.6.1", :native-prefix nil}, riddley {:vsn "0.2.0", :native-prefix nil}, commons-fileupload {:vsn "1.4", :native-prefix nil}, org.clojure/tools.macro {:vsn "0.1.5", :native-prefix nil}, com.github.java-json-tools/jackson-coreutils {:vsn "1.9", :native-prefix nil}, com.fasterxml.jackson.dataformat/jackson-dataformat-cbor {:vsn "2.9.6", :native-prefix nil}, org.flatland/useful {:vsn "0.11.6", :native-prefix nil}, com.googlecode.json-simple/json-simple {:vsn "1.1.1", :native-prefix nil}, environ {:vsn "1.1.0", :native-prefix nil}, io.aviso/pretty {:vsn "0.1.37", :native-prefix nil}, com.github.fge/msg-simple {:vsn "1.1", :native-prefix nil}, fipp {:vsn "0.6.17", :native-prefix nil}, com.github.fge/btf {:vsn "1.2", :native-prefix nil}, peridot {:vsn "0.5.1", :native-prefix nil}, com.stuartsierra/component {:vsn "0.4.0", :native-prefix nil}, org.flatland/ordered {:vsn "1.5.7", :native-prefix nil}, medley {:vsn "1.0.0", :native-prefix nil}, org.clojure/tools.namespace {:vsn "0.3.0", :native-prefix nil}, com.fasterxml.jackson.core/jackson-core {:vsn "2.10.1", :native-prefix nil}, http-kit {:vsn "2.3.0", :native-prefix nil}, de.kotka/lazymap {:vsn "3.1.0", :native-prefix nil}, slingshot {:vsn "0.12.2", :native-prefix nil}, org.yaml/snakeyaml {:vsn "1.24", :native-prefix nil}, ring-mock {:vsn "0.1.5", :native-prefix nil}, org.tobereplaced/lettercase {:vsn "1.0.0", :native-prefix nil}, metosin/ring-swagger {:vsn "0.26.2", :native-prefix nil}, javax.mail/mailapi {:vsn "1.4.3", :native-prefix nil}, cheshire {:vsn "5.8.1", :native-prefix nil}, org.apache.httpcomponents/httpcore {:vsn "4.4.5", :native-prefix nil}, mvxcvi/arrangement {:vsn "1.2.0", :native-prefix nil}, potemkin {:vsn "0.4.5", :native-prefix nil}, clojure-msgpack {:vsn "1.2.1", :native-prefix nil}, colorize {:vsn "0.1.1", :native-prefix nil}, org.mozilla/rhino {:vsn "1.7.7.1", :native-prefix nil}, com.github.fge/uri-template {:vsn "0.9", :native-prefix nil}, crypto-equality {:vsn "1.0.0", :native-prefix nil}, com.fasterxml.jackson.core/jackson-annotations {:vsn "2.10.1", :native-prefix nil}, com.github.java-json-tools/json-schema-validator {:vsn "2.2.10", :native-prefix nil}, suspendable {:vsn "0.1.1", :native-prefix nil}, org.clojure/core.unify {:vsn "0.5.7", :native-prefix nil}, org.javassist/javassist {:vsn "3.18.1-GA", :native-prefix nil}, frankiesardo/linked {:vsn "1.3.0", :native-prefix nil}, org.clojure/java.classpath {:vsn "0.2.3", :native-prefix nil}, commons-codec {:vsn "1.11", :native-prefix nil}, com.google.guava/guava {:vsn "16.0.1", :native-prefix nil}, com.googlecode.libphonenumber/libphonenumber {:vsn "8.0.0", :native-prefix nil}, org.msgpack/msgpack {:vsn "0.6.12", :native-prefix nil}, reloaded.repl {:vsn "0.2.4", :native-prefix nil}, com.cognitect/transit-clj {:vsn "0.8.319", :native-prefix nil}, com.fasterxml.jackson.datatype/jackson-datatype-joda {:vsn "2.10.1", :native-prefix nil}, prismatic/plumbing {:vsn "0.5.5", :native-prefix nil}, clj-commons/clj-yaml {:vsn "0.7.0", :native-prefix nil}, ring/ring-codec {:vsn "1.1.2", :native-prefix nil}, org.apache.httpcomponents/httpclient {:vsn "4.5.1", :native-prefix nil}, metosin/schema-tools {:vsn "0.11.0", :native-prefix nil}, org.clojure/core.rrb-vector {:vsn "0.0.14", :native-prefix nil}, prismatic/schema {:vsn "1.1.12", :native-prefix nil}, instaparse {:vsn "1.4.8", :native-prefix nil}, org.clojure/math.combinatorics {:vsn "0.1.5", :native-prefix nil}, clout {:vsn "2.2.1", :native-prefix nil}, com.github.java-json-tools/json-schema-core {:vsn "1.2.10", :native-prefix nil}, org.clojure/tools.reader {:vsn "1.3.2", :native-prefix nil}, ikitommi/linked {:vsn "1.3.1-alpha1", :native-prefix nil}, joda-time {:vsn "2.10.5", :native-prefix nil}, org.tcrawley/dynapath {:vsn "1.0.0", :native-prefix nil}, nrepl {:vsn "0.8.3", :native-prefix nil}, tigris {:vsn "0.1.1", :native-prefix nil}, javax.servlet/servlet-api {:vsn "2.5", :native-prefix nil}, com.rpl/specter {:vsn "1.0.5", :native-prefix nil}, criterium {:vsn "0.4.5", :native-prefix nil}, org.clojure/test.check {:vsn "0.10.0-alpha3", :native-prefix nil}, clj-tuple {:vsn "0.2.2", :native-prefix nil}, metosin/jsonista {:vsn "0.2.5", :native-prefix nil}, org.clojure/core.memoize {:vsn "0.8.2", :native-prefix nil}, com.stuartsierra/dependency {:vsn "0.2.0", :native-prefix nil}, net.sf.jopt-simple/jopt-simple {:vsn "5.0.3", :native-prefix nil}, org.clojure/data.priority-map {:vsn "0.0.7", :native-prefix nil}, commons-io {:vsn "2.6", :native-prefix nil}, org.apache.httpcomponents/httpmime {:vsn "4.5.1", :native-prefix nil}, com.google.code.findbugs/jsr305 {:vsn "3.0.1", :native-prefix nil}, ring/ring-core {:vsn "1.8.0", :native-prefix nil}, org.clojure/core.cache {:vsn "0.8.2", :native-prefix nil}, ring-middleware-format {:vsn "0.7.4", :native-prefix nil}, midje {:vsn "1.9.9", :native-prefix nil}, com.fasterxml.jackson.dataformat/jackson-dataformat-smile {:vsn "2.9.6", :native-prefix nil}, com.fasterxml.jackson.datatype/jackson-datatype-jsr310 {:vsn "2.10.0", :native-prefix nil}, org.clojure/data.codec {:vsn "0.1.0", :native-prefix nil}, javax.xml.bind/jaxb-api {:vsn "2.3.0", :native-prefix nil}, org.clojars.brenton/google-diff-match-patch {:vsn "0.1", :native-prefix nil}}, :native-path "target/native"} {:native-path "target/native", :dependencies {com.cognitect/transit-java {:vsn "0.8.337", :native-prefix nil, :native? false}, org.clojure/clojure {:vsn "1.10.1", :native-prefix nil, :native? false}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil, :native? false}, metosin/ring-http-response {:vsn "0.9.1", :native-prefix nil, :native? false}, org.clojure/core.specs.alpha {:vsn "0.2.44", :native-prefix nil, :native? false}, metosin/muuntaja {:vsn "0.6.6", :native-prefix nil, :native? false}, marick/suchwow {:vsn "6.0.2", :native-prefix nil, :native? false}, metosin/ring-swagger-ui {:vsn "3.24.3", :native-prefix nil, :native? false}, com.fasterxml.jackson.core/jackson-databind {:vsn "2.10.1", :native-prefix nil, :native? false}, flare {:vsn "0.2.9", :native-prefix nil, :native? false}, org.clojure/spec.alpha {:vsn "0.2.176", :native-prefix nil, :native? false}, clj-time {:vsn "0.15.2", :native-prefix nil, :native? false}, mvxcvi/puget {:vsn "1.1.2", :native-prefix nil, :native? false}, crypto-random {:vsn "1.2.0", :native-prefix nil, :native? false}, javax.activation/activation {:vsn "1.1", :native-prefix nil, :native? false}, metosin/spec-tools {:vsn "0.10.0", :native-prefix nil, :native? false}, metosin/scjsv {:vsn "0.5.0", :native-prefix nil, :native? false}, compojure {:vsn "1.6.1", :native-prefix nil, :native? false}, riddley {:vsn "0.2.0", :native-prefix nil, :native? false}, commons-fileupload {:vsn "1.4", :native-prefix nil, :native? false}, org.clojure/tools.macro {:vsn "0.1.5", :native-prefix nil, :native? false}, com.github.java-json-tools/jackson-coreutils {:vsn "1.9", :native-prefix nil, :native? false}, com.fasterxml.jackson.dataformat/jackson-dataformat-cbor {:vsn "2.9.6", :native-prefix nil, :native? false}, org.flatland/useful {:vsn "0.11.6", :native-prefix nil, :native? false}, com.googlecode.json-simple/json-simple {:vsn "1.1.1", :native-prefix nil, :native? false}, environ {:vsn "1.1.0", :native-prefix nil, :native? false}, io.aviso/pretty {:vsn "0.1.37", :native-prefix nil, :native? false}, com.github.fge/msg-simple {:vsn "1.1", :native-prefix nil, :native? false}, fipp {:vsn "0.6.17", :native-prefix nil, :native? false}, com.github.fge/btf {:vsn "1.2", :native-prefix nil, :native? false}, peridot {:vsn "0.5.1", :native-prefix nil, :native? false}, com.stuartsierra/component {:vsn "0.4.0", :native-prefix nil, :native? false}, org.flatland/ordered {:vsn "1.5.7", :native-prefix nil, :native? false}, medley {:vsn "1.0.0", :native-prefix nil, :native? false}, org.clojure/tools.namespace {:vsn "0.3.0", :native-prefix nil, :native? false}, com.fasterxml.jackson.core/jackson-core {:vsn "2.10.1", :native-prefix nil, :native? false}, http-kit {:vsn "2.3.0", :native-prefix nil, :native? false}, de.kotka/lazymap {:vsn "3.1.0", :native-prefix nil, :native? false}, slingshot {:vsn "0.12.2", :native-prefix nil, :native? false}, org.yaml/snakeyaml {:vsn "1.24", :native-prefix nil, :native? false}, ring-mock {:vsn "0.1.5", :native-prefix nil, :native? false}, org.tobereplaced/lettercase {:vsn "1.0.0", :native-prefix nil, :native? false}, metosin/ring-swagger {:vsn "0.26.2", :native-prefix nil, :native? false}, javax.mail/mailapi {:vsn "1.4.3", :native-prefix nil, :native? false}, cheshire {:vsn "5.8.1", :native-prefix nil, :native? false}, org.apache.httpcomponents/httpcore {:vsn "4.4.5", :native-prefix nil, :native? false}, mvxcvi/arrangement {:vsn "1.2.0", :native-prefix nil, :native? false}, potemkin {:vsn "0.4.5", :native-prefix nil, :native? false}, clojure-msgpack {:vsn "1.2.1", :native-prefix nil, :native? false}, colorize {:vsn "0.1.1", :native-prefix nil, :native? false}, org.mozilla/rhino {:vsn "1.7.7.1", :native-prefix nil, :native? false}, com.github.fge/uri-template {:vsn "0.9", :native-prefix nil, :native? false}, crypto-equality {:vsn "1.0.0", :native-prefix nil, :native? false}, com.fasterxml.jackson.core/jackson-annotations {:vsn "2.10.1", :native-prefix nil, :native? false}, com.github.java-json-tools/json-schema-validator {:vsn "2.2.10", :native-prefix nil, :native? false}, suspendable {:vsn "0.1.1", :native-prefix nil, :native? false}, org.clojure/core.unify {:vsn "0.5.7", :native-prefix nil, :native? false}, org.javassist/javassist {:vsn "3.18.1-GA", :native-prefix nil, :native? false}, frankiesardo/linked {:vsn "1.3.0", :native-prefix nil, :native? false}, org.clojure/java.classpath {:vsn "0.2.3", :native-prefix nil, :native? false}, commons-codec {:vsn "1.11", :native-prefix nil, :native? false}, com.google.guava/guava {:vsn "16.0.1", :native-prefix nil, :native? false}, com.googlecode.libphonenumber/libphonenumber {:vsn "8.0.0", :native-prefix nil, :native? false}, org.msgpack/msgpack {:vsn "0.6.12", :native-prefix nil, :native? false}, reloaded.repl {:vsn "0.2.4", :native-prefix nil, :native? false}, com.cognitect/transit-clj {:vsn "0.8.319", :native-prefix nil, :native? false}, com.fasterxml.jackson.datatype/jackson-datatype-joda {:vsn "2.10.1", :native-prefix nil, :native? false}, prismatic/plumbing {:vsn "0.5.5", :native-prefix nil, :native? false}, clj-commons/clj-yaml {:vsn "0.7.0", :native-prefix nil, :native? false}, ring/ring-codec {:vsn "1.1.2", :native-prefix nil, :native? false}, org.apache.httpcomponents/httpclient {:vsn "4.5.1", :native-prefix nil, :native? false}, metosin/schema-tools {:vsn "0.11.0", :native-prefix nil, :native? false}, org.clojure/core.rrb-vector {:vsn "0.0.14", :native-prefix nil, :native? false}, prismatic/schema {:vsn "1.1.12", :native-prefix nil, :native? false}, instaparse {:vsn "1.4.8", :native-prefix nil, :native? false}, org.clojure/math.combinatorics {:vsn "0.1.5", :native-prefix nil, :native? false}, clout {:vsn "2.2.1", :native-prefix nil, :native? false}, com.github.java-json-tools/json-schema-core {:vsn "1.2.10", :native-prefix nil, :native? false}, org.clojure/tools.reader {:vsn "1.3.2", :native-prefix nil, :native? false}, ikitommi/linked {:vsn "1.3.1-alpha1", :native-prefix nil, :native? false}, joda-time {:vsn "2.10.5", :native-prefix nil, :native? false}, org.tcrawley/dynapath {:vsn "1.0.0", :native-prefix nil, :native? false}, nrepl {:vsn "0.8.3", :native-prefix nil, :native? false}, tigris {:vsn "0.1.1", :native-prefix nil, :native? false}, javax.servlet/servlet-api {:vsn "2.5", :native-prefix nil, :native? false}, com.rpl/specter {:vsn "1.0.5", :native-prefix nil, :native? false}, criterium {:vsn "0.4.5", :native-prefix nil, :native? false}, org.clojure/test.check {:vsn "0.10.0-alpha3", :native-prefix nil, :native? false}, clj-tuple {:vsn "0.2.2", :native-prefix nil, :native? false}, metosin/jsonista {:vsn "0.2.5", :native-prefix nil, :native? false}, org.clojure/core.memoize {:vsn "0.8.2", :native-prefix nil, :native? false}, com.stuartsierra/dependency {:vsn "0.2.0", :native-prefix nil, :native? false}, net.sf.jopt-simple/jopt-simple {:vsn "5.0.3", :native-prefix nil, :native? false}, org.clojure/data.priority-map {:vsn "0.0.7", :native-prefix nil, :native? false}, commons-io {:vsn "2.6", :native-prefix nil, :native? false}, org.apache.httpcomponents/httpmime {:vsn "4.5.1", :native-prefix nil, :native? false}, com.google.code.findbugs/jsr305 {:vsn "3.0.1", :native-prefix nil, :native? false}, ring/ring-core {:vsn "1.8.0", :native-prefix nil, :native? false}, org.clojure/core.cache {:vsn "0.8.2", :native-prefix nil, :native? false}, ring-middleware-format {:vsn "0.7.4", :native-prefix nil, :native? false}, midje {:vsn "1.9.9", :native-prefix nil, :native? false}, com.fasterxml.jackson.dataformat/jackson-dataformat-smile {:vsn "2.9.6", :native-prefix nil, :native? false}, com.fasterxml.jackson.datatype/jackson-datatype-jsr310 {:vsn "2.10.0", :native-prefix nil, :native? false}, org.clojure/data.codec {:vsn "0.1.0", :native-prefix nil, :native? false}, javax.xml.bind/jaxb-api {:vsn "2.3.0", :native-prefix nil, :native? false}, org.clojars.brenton/google-diff-match-patch {:vsn "0.1", :native-prefix nil, :native? false}}}] \ No newline at end of file From 2b56baea3fadc4eb7d9103fcb123991c4dde37cf Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Mon, 13 May 2024 14:33:42 -0500 Subject: [PATCH 03/18] rm --- .../compojure1/test/compojure/api/dev/gen.clj | 194 ------------------ 1 file changed, 194 deletions(-) delete mode 100644 test-suites/compojure1/test/compojure/api/dev/gen.clj diff --git a/test-suites/compojure1/test/compojure/api/dev/gen.clj b/test-suites/compojure1/test/compojure/api/dev/gen.clj deleted file mode 100644 index b3002079..00000000 --- a/test-suites/compojure1/test/compojure/api/dev/gen.clj +++ /dev/null @@ -1,194 +0,0 @@ -(ns compojure.api.dev.gen - (:require [clojure.string :as str] - [clojure.set :as set] - [clojure.walk :as walk])) - -(def impl-local-sym '+impl+) - -(defn normalize-argv [argv] - {:post [(or (empty? %) - (apply distinct? %)) - (not-any? #{impl-local-sym} %)]} - (into [] (map-indexed (fn [i arg] - (if (symbol? arg) - (do (assert (not (namespace arg))) - (if (some #(Character/isDigit (char %)) (name arg)) - (symbol (apply str (concat - (remove #(Character/isDigit (char %)) (name arg)) - [i]))) - arg)) - (symbol (str "arg" i))))) - argv)) - -(defn normalize-arities [arities] - (cond-> arities - (= 1 (count arities)) first)) - -(defn import-fn [sym] - {:pre [(namespace sym)]} - (let [vr (find-var sym) - m (meta vr) - n (:name m) - arglists (:arglists m) - protocol (:protocol m) - when-class (-> sym meta :when-class) - _ (assert (not when-class)) - forward-meta (into (sorted-map) (select-keys m [:tag :arglists :doc :deprecated])) - _ (assert (not= n impl-local-sym)) - _ (when (:macro m) - (throw (IllegalArgumentException. - (str "Calling import-fn on a macro: " sym)))) - form (if protocol - (list* 'defn (with-meta n (dissoc forward-meta :arglists)) - (map (fn [argv] - {:pre [(not-any? #{'&} argv)]} - (list argv (list* sym argv))) - arglists)) - (list 'def (with-meta n forward-meta) sym))] - (cond->> form - #_#_when-class (list 'java-time.util/when-class when-class)))) - -(defn import-macro [sym] - (let [vr (find-var sym) - m (meta vr) - _ (when-not (:macro m) - (throw (IllegalArgumentException. - (str "Calling import-macro on a non-macro: " sym)))) - n (:name m) - arglists (:arglists m)] - (list* 'defmacro n - (concat - (some-> (not-empty (into (sorted-map) (select-keys m [:doc :deprecated]))) - list) - (normalize-arities - (map (fn [argv] - (let [argv (normalize-argv argv)] - (list argv - (if (some #{'&} argv) - (list* 'list* (list 'quote sym) (remove #{'&} argv)) - (list* 'list (list 'quote sym) argv))))) - arglists)))))) - -(defn import-vars - "Imports a list of vars from other namespaces." - [& syms] - (let [unravel (fn unravel [x] - (if (sequential? x) - (->> x - rest - (mapcat unravel) - (map - #(with-meta - (symbol - (str (first x) - (when-let [n (namespace %)] - (str "." n))) - (name %)) - (meta %)))) - [x])) - syms (mapcat unravel syms)] - (map (fn [sym] - (let [vr (if-some [rr (resolve 'clojure.core/requiring-resolve)] - (rr sym) - (do (require (-> sym namespace symbol)) - (resolve sym))) - _ (assert vr (str sym " is unresolvable")) - m (meta vr)] - (if (:macro m) - (import-macro sym) - (import-fn sym)))) - syms))) - -(def compojure-api-sweet-impl-info - {:vars '([compojure.api.core routes defroutes let-routes undocumented middleware route-middleware - context GET ANY HEAD PATCH DELETE OPTIONS POST PUT] - [compojure.api.api api defapi] - [compojure.api.resource resource] - [compojure.api.routes path-for] - [compojure.api.swagger swagger-routes] - [ring.swagger.json-schema describe])}) - -(defn gen-compojure-api-sweet-ns-forms [nsym] - (concat - [";; NOTE: This namespace is generated by compojure.api.dev.gen" - `(~'ns ~nsym - (:require compojure.api.core - compojure.api.api - compojure.api.routes - compojure.api.resource - compojure.api.swagger - ring.swagger.json-schema))] - (apply import-vars (:vars compojure-api-sweet-impl-info)))) - -(def compojure-api-upload-impl-info - {:vars '([ring.middleware.multipart-params wrap-multipart-params] - [ring.swagger.upload TempFileUpload ByteArrayUpload])}) - -(defn gen-compojure-api-upload-ns-forms [nsym] - (concat - [";; NOTE: This namespace is generated by compojure.api.dev.gen" - `(~'ns ~nsym - (:require ring.middleware.multipart-params - ring.swagger.upload))] - (apply import-vars (:vars compojure-api-upload-impl-info)))) - -(defn print-form [form] - (with-bindings - (cond-> {#'*print-meta* true - #'*print-length* nil - #'*print-level* nil} - (resolve '*print-namespace-maps*) - (assoc (resolve '*print-namespace-maps*) false)) - (cond - (string? form) (println form) - :else (println (pr-str (walk/postwalk - (fn [v] - (if (meta v) - (if (symbol? v) - (vary-meta v #(not-empty - (cond-> (sorted-map) - (some? (:tag %)) (assoc :tag (:tag %)) - (some? (:doc %)) (assoc :doc (:doc %)) - ((some-fn true? string?) (:deprecated %)) (assoc :deprecated (:deprecated %)) - (string? (:superseded-by %)) (assoc :superseded-by (:superseded-by %)) - (string? (:supercedes %)) (assoc :supercedes (:supercedes %)) - (some? (:arglists %)) (assoc :arglists (list 'quote (doall (map normalize-argv (:arglists %)))))))) - (with-meta v nil)) - v)) - form))))) - nil) - -(defn print-compojure-api-ns [{:keys [f nsym]}] - (assert f) - (run! print-form (f nsym))) - -(def compojure-api-sweet-nsym - (with-meta - 'compojure.api.sweet - ;;TODO ns meta - nil)) - -(def compojure-api-upload-nsym - (with-meta - 'compojure.api.upload - ;;TODO ns meta - nil)) - -(def compojure-api-sweet-conf {:nsym compojure-api-sweet-nsym - :f #'gen-compojure-api-sweet-ns-forms}) -(def compojure-api-upload-conf {:nsym compojure-api-upload-nsym - :f #'gen-compojure-api-upload-ns-forms}) - -(def gen-source->nsym - {"src/compojure/api/sweet.clj" compojure-api-sweet-conf - "src/compojure/api/upload.clj" compojure-api-upload-conf}) - -(defn spit-compojure-api-ns [] - (doseq [[source conf] gen-source->nsym] - (spit source (with-out-str (print-compojure-api-ns conf))))) - -(comment - (print-compojure-api-ns compojure-api-sweet-conf) - (print-compojure-api-ns compojure-api-upload-conf) - (spit-compojure-api-ns) - ) From 1cd7c7a0fa79f5b52437ad0e030bfe9cb084a42e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Mon, 13 May 2024 14:57:47 -0500 Subject: [PATCH 04/18] lazily load rmf --- src/compojure/api/middleware.clj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj index 80c1fd8d..86f637d1 100644 --- a/src/compojure/api/middleware.clj +++ b/src/compojure/api/middleware.clj @@ -6,8 +6,6 @@ [compojure.api.request :as request] [compojure.api.impl.logging :as logging] - [ring.middleware.format-params :refer [wrap-restful-params]] - [ring.middleware.format-response :refer [wrap-restful-response]] [ring.swagger.coerce :as coerce] ring.middleware.http-response @@ -331,13 +329,15 @@ (seq formats) (rsm/wrap-swagger-data {:produces (->mime-types (remove response-only-mimes formats)) :consumes (->mime-types formats)}) true (wrap-options (select-keys options [:ring-swagger :coercion])) - (seq formats) (wrap-restful-params {:formats (remove response-only-mimes formats) - :handle-error handle-req-error - :format-options params-opts}) + (seq formats) ((requiring-resolve 'ring.middleware.format-params/wrap-restful-params) + {:formats (remove response-only-mimes formats) + :handle-error handle-req-error + :format-options params-opts}) exceptions (wrap-exceptions exceptions) - (seq formats) (wrap-restful-response {:formats formats - :predicate serializable? - :format-options response-opts}) + (seq formats) ((requiring-resolve 'ring.middleware.format-response/wrap-restful-response) + {:formats formats + :predicate serializable? + :format-options response-opts}) true wrap-keyword-params true wrap-nested-params true wrap-params))) From feb24b7b5f23f8e796674e773f5adafd8bc5b50f Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Mon, 13 May 2024 15:19:31 -0500 Subject: [PATCH 05/18] wip --- src/compojure/api/meta.clj | 2 +- src/compojure/api/middleware.clj | 6 +- test/compojure/api/integration_test.clj | 87 ++++++++++++------------- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/compojure/api/meta.clj b/src/compojure/api/meta.clj index 004da161..4a2a4e38 100644 --- a/src/compojure/api/meta.clj +++ b/src/compojure/api/meta.clj @@ -766,7 +766,7 @@ (and (seq? body) (boolean (when-some [v (resolve-var &env (first body))] - (when (middleware-vars (symbol v)) + (when (middleware-vars (var->sym v)) (let [[_ mid & body] body] (and (static-form? &env mid) (static-body? &env body)))))))) diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj index 86f637d1..c3edda43 100644 --- a/src/compojure/api/middleware.clj +++ b/src/compojure/api/middleware.clj @@ -321,6 +321,8 @@ (defn- ring-middleware-format-api-middleware [handler options] + (require 'ring.middleware.format-params + 'ring.middleware.format-response) (let [{:keys [exceptions format components]} options {:keys [formats params-opts response-opts]} format] (cond-> handler @@ -329,12 +331,12 @@ (seq formats) (rsm/wrap-swagger-data {:produces (->mime-types (remove response-only-mimes formats)) :consumes (->mime-types formats)}) true (wrap-options (select-keys options [:ring-swagger :coercion])) - (seq formats) ((requiring-resolve 'ring.middleware.format-params/wrap-restful-params) + (seq formats) ((resolve 'ring.middleware.format-params/wrap-restful-params) {:formats (remove response-only-mimes formats) :handle-error handle-req-error :format-options params-opts}) exceptions (wrap-exceptions exceptions) - (seq formats) ((requiring-resolve 'ring.middleware.format-response/wrap-restful-response) + (seq formats) ((resolve 'ring.middleware.format-response/wrap-restful-response) {:formats formats :predicate serializable? :format-options response-opts}) diff --git a/test/compojure/api/integration_test.clj b/test/compojure/api/integration_test.clj index dc430c78..e8a022da 100644 --- a/test/compojure/api/integration_test.clj +++ b/test/compojure/api/integration_test.clj @@ -553,90 +553,87 @@ :definitions {} :paths {"/user" {:get {:responses {:default {:description ""}}}}}})))) - (fact "swagger-routes" + (testing "swagger-routes" - (fact "with defaults" + (testing "with defaults" (let [app (api (swagger-routes))] - (fact "api-docs are mounted to /" + (testing "api-docs are mounted to /" (let [[status body] (raw-get* app "/")] - status => 200 - body => #"Swagger UI")) + (is (= 200 status)) + (is (str/includes? body "Swagger UI")))) - (fact "spec is mounted to /swagger.json" + (testing "spec is mounted to /swagger.json" (let [[status body] (get* app "/swagger.json")] - status => 200 - body => (contains {:swagger "2.0"}))))) + (is (= 200 status)) + (is (= "2.0" (:swagger body))))))) - (fact "with partial overridden values" + (testing "with partial overridden values" (let [app (api (swagger-routes {:ui "/api-docs" :data {:info {:title "Kikka"} :paths {"/ping" {:get {}}}}}))] - (fact "api-docs are mounted" + (testing "api-docs are mounted" (let [[status body] (raw-get* app "/api-docs")] - status => 200 - body => #"Swagger UI")) + (is (= 200 status)) + (is (str/includes? body "Swagger UI")))) - (fact "spec is mounted to /swagger.json" + (testing "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})})))))) + (is (= 200 status)) + (is (= "2.0" (:swagger body))) + (is (= "Kikka" (-> body :info :title))) + (is (some? (-> body :paths (get (keyword "/ping")))))))))) - (fact "swagger via api-options" + (testing "swagger via api-options" - (fact "with defaults" + (testing "with defaults" (let [app (api)] - (fact "api-docs are not mounted" + (testing "api-docs are not mounted" (let [[status body] (raw-get* app "/")] - status => nil)) + (is (nil? status)))) - (fact "spec is not mounted" + (testing "spec is not mounted" (let [[status body] (get* app "/swagger.json")] - status => nil)))) + (is (nil? status)))))) - (fact "with spec" + (testing "with spec" (let [app (api {:swagger {:spec "/swagger.json"}})] - (fact "api-docs are not mounted" + (testing "api-docs are not mounted" (let [[status body] (raw-get* app "/")] - status => nil)) + (is (nil? status)))) - (fact "spec is mounted to /swagger.json" + (testing "spec is mounted to /swagger.json" (let [[status body] (get* app "/swagger.json")] - status => 200 - body => (contains {:swagger "2.0"})))))) + (is (nil? status)) + (is (= "2.0" (:swagger body)))))))) - (fact "with ui" + (testing "with ui" (let [app (api {:swagger {:ui "/api-docs"}})] - (fact "api-docs are mounted" + (testing "api-docs are mounted" (let [[status body] (raw-get* app "/api-docs")] - status => 200 - body => #"Swagger UI")) + (is-200-status status) + (is (str/includes? body "Swagger UI")))) - (fact "spec is not mounted" + (testing "spec is not mounted" (let [[status body] (get* app "/swagger.json")] - status => nil)))) + (is (nil? status)))))) - (fact "with ui and spec" + (testing "with ui and spec" (let [app (api {:swagger {:spec "/swagger.json", :ui "/api-docs"}})] - (fact "api-docs are mounted" + (testing "api-docs are mounted" (let [[status body] (raw-get* app "/api-docs")] - status => 200 - body => #"Swagger UI")) + (is-200-status status) + (is (str/includes? body "Swagger UI")))) - (fact "spec is mounted to /swagger.json" + (testing "spec is mounted to /swagger.json" (let [[status body] (get* app "/swagger.json")] - status => 200 - body => (contains {:swagger "2.0"})))))) + (is-200-status status) + (is (= "2.0" (:swagger body)))))))) (deftest muuntaja-swagger-docs-test (let [app (api From d5cd3d72b73066bd2fd5bc0749197472604487d7 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Mon, 13 May 2024 17:22:16 -0500 Subject: [PATCH 06/18] revert --- src/compojure/api/exception.clj | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/compojure/api/exception.clj b/src/compojure/api/exception.clj index a8512790..cc281da6 100644 --- a/src/compojure/api/exception.clj +++ b/src/compojure/api/exception.clj @@ -3,21 +3,7 @@ [clojure.walk :as walk] [compojure.api.impl.logging :as logging] [compojure.api.coercion.core :as cc] - [compojure.api.coercion.schema] - [schema.utils :as su]) - (:import [schema.utils ValidationError NamedError])) - -;; 1.1.x -(defn stringify-error - "Stringifies symbols and validation errors in Schema error, keeping the structure intact." - [error] - (walk/postwalk - (fn [x] - (cond - (instance? ValidationError x) (str (su/validation-error-explain x)) - (instance? NamedError x) (str (su/named-error-explain x)) - :else x)) - error)) + [compojure.api.coercion.schema])) ;; ;; Default exception handlers From 965aff2df4342905c63950cceaad291ce639be36 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 13:27:59 -0500 Subject: [PATCH 07/18] add back c.api.coerce --- src/compojure/api/coerce.clj | 67 ++++++++++++++++++++++++++++++++ src/compojure/api/middleware.clj | 21 ++++++++++ 2 files changed, 88 insertions(+) create mode 100644 src/compojure/api/coerce.clj 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/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 ;; From fb72503203741bc8757ebb147e3374b6aba05d6e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:06:59 -0500 Subject: [PATCH 08/18] lazily load coercion extensions --- CHANGELOG.md | 3 +++ src/compojure/api/coercion.clj | 5 +++-- src/compojure/api/coercion/register_schema.clj | 8 ++++++++ src/compojure/api/coercion/register_spec.clj | 8 ++++++++ src/compojure/api/coercion/schema.clj | 2 -- src/compojure/api/coercion/spec.clj | 2 -- 6 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 src/compojure/api/coercion/register_schema.clj create mode 100644 src/compojure/api/coercion/register_spec.clj diff --git a/CHANGELOG.md b/CHANGELOG.md index e93d4be7..4e275e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ See also: [compojure-api 1.1.x changelog](./CHANGELOG-1.1.x.md) +## Next +* Lazily load spec and schema coercion support to preserve Clojure 1.8 support for 1.1.x + ## 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/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..b310fc18 100644 --- a/src/compojure/api/coercion/schema.clj +++ b/src/compojure/api/coercion/schema.clj @@ -84,5 +84,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..ea8cf4b6 100644 --- a/src/compojure/api/coercion/spec.clj +++ b/src/compojure/api/coercion/spec.clj @@ -149,5 +149,3 @@ (->SpecCoercion :spec options)) (def default-coercion (create-coercion default-options)) - -(defmethod cc/named-coercion :spec [_] default-coercion) From 27aa29aa4c0cbe08b63aad9930b3d1e700802721 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:12:58 -0500 Subject: [PATCH 09/18] preserve side effect --- src/compojure/api/coercion/schema.clj | 4 +++- src/compojure/api/coercion/spec.clj | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/compojure/api/coercion/schema.clj b/src/compojure/api/coercion/schema.clj index b310fc18..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))) diff --git a/src/compojure/api/coercion/spec.clj b/src/compojure/api/coercion/spec.clj index ea8cf4b6..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) From 8e39651b01a762480169f0ef49278dc5b91ab47f Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:28:09 -0500 Subject: [PATCH 10/18] test future --- project.clj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/project.clj b/project.clj index e77fe64e..61bb8c15 100644 --- a/project.clj +++ b/project.clj @@ -61,6 +61,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 +89,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"] From ef3e4d30807229a9fa2b165ea868580e1d8e46a2 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:37:13 -0500 Subject: [PATCH 11/18] fix pedantic --- project.clj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/project.clj b/project.clj index 61bb8c15..d8ed380e 100644 --- a/project.clj +++ b/project.clj @@ -63,7 +63,8 @@ [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"]]} + :1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha11"] + [org.clojure/spec.alpha "0.3.218"]]} :async {:jvm-opts ["-Dcompojure-api.test.async=true"] :dependencies [[manifold "0.1.8" :exclusions [org.clojure/tools.logging]]]}} :eastwood {:namespaces [:source-paths] From e455e6caa94252021a8f430599f05788f81c5450 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:38:12 -0500 Subject: [PATCH 12/18] +jdk22 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From ec6d42d8cacba25c3b68ecb7b8383c3a04cf828e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:46:18 -0500 Subject: [PATCH 13/18] bump spec-tools --- project.clj | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/project.clj b/project.clj index d8ed380e..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"] @@ -63,8 +62,7 @@ [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"] - [org.clojure/spec.alpha "0.3.218"]]} + :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] From f160d60885b8c5a66b90958d0b97e093ea51e03f Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:46:34 -0500 Subject: [PATCH 14/18] [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e275e5c..a0f4f818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ See also: [compojure-api 1.1.x changelog](./CHANGELOG-1.1.x.md) ## Next * Lazily load spec and schema coercion support to preserve Clojure 1.8 support for 1.1.x +* bump spec-tools to 0.10.6 ## 2.0.0-alpha34-SNAPSHOT * **BREAKING CHANGE**: `:formatter :muuntaja` sometimes required for `api{-middleware}` options From 6b5b7de5aec9a3bb186a56498d545cd84c4cc226 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 14:51:43 -0500 Subject: [PATCH 15/18] ci From 2577f7454608ddca22083bf742d69a960a12c674 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 15:17:55 -0500 Subject: [PATCH 16/18] update for spec-tools 0.10.6 --- CHANGELOG.md | 1 + test19/compojure/api/coercion/spec_coercion_test.clj | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f4f818..9b6c78c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ See also: [compojure-api 1.1.x changelog](./CHANGELOG-1.1.x.md) ## Next * Lazily load spec and schema coercion support to preserve Clojure 1.8 support for 1.1.x * 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 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"} From c1bd124e11b7513fe58b0de7f04cdb081836eb5e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 15:27:24 -0500 Subject: [PATCH 17/18] [skip ci] --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b6c78c3..bfd87589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ See also: [compojure-api 1.1.x changelog](./CHANGELOG-1.1.x.md) ## Next -* Lazily load spec and schema coercion support to preserve Clojure 1.8 support for 1.1.x +* 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)) From ceb17a2386d0b09a3131ae58d9a0d270b0edf7fe Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Mon, 17 Jun 2024 09:50:45 -0500 Subject: [PATCH 18/18] wip --- src/compojure/api/middleware.clj | 229 ++++++------------------------- src/compojure/api/resource.clj | 37 ++--- src/compojure/api/routes.clj | 64 ++++----- 3 files changed, 80 insertions(+), 250 deletions(-) diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj index 3e1d9024..3d6ba644 100644 --- a/src/compojure/api/middleware.clj +++ b/src/compojure/api/middleware.clj @@ -5,11 +5,6 @@ [compojure.api.coercion :as coercion] [compojure.api.request :as request] [compojure.api.impl.logging :as logging] - - [ring.swagger.coerce :as coerce] - - ring.middleware.http-response - [ring.swagger.middleware :as rsm] [ring.middleware.keyword-params :refer [wrap-keyword-params]] [ring.middleware.nested-params :refer [wrap-nested-params]] [ring.middleware.params :refer [wrap-params]] @@ -21,8 +16,6 @@ [ring.swagger.common :as rsc] [ring.util.http-response :refer :all]) (:import [clojure.lang ArityException] - [org.yaml.snakeyaml.parser ParserException] - [com.fasterxml.jackson.core JsonParseException] [com.fasterxml.jackson.datatype.joda JodaModule])) ;; @@ -92,14 +85,6 @@ (defn get-components [req] (::components req)) -;; 1.1.x - -(def coercion-request-ks [::options :coercion]) - -(defn wrap-coercion [handler coercion] - (fn [request] - (handler (assoc-in request coercion-request-ks coercion)))) - ;; ;; Options ;; @@ -231,16 +216,9 @@ ;; Api Middleware ;; -(def default-coercion-matchers - {:body coerce/json-schema-coercion-matcher - :string coerce/query-schema-coercion-matcher - :response coerce/json-schema-coercion-matcher}) - -(def no-response-coercion - (constantly (dissoc default-coercion-matchers :response))) - -(def ^:private muuntaja-api-middleware-defaults - {:formats ::default +(def api-middleware-defaults + {::api-middleware-defaults true + :formats ::default :exceptions {:handlers {:ring.util.http-response/response ex/http-response-handler ::ex/request-validation ex/request-validation-handler ::ex/request-parsing ex/request-parsing-handler @@ -250,153 +228,8 @@ :coercion coercion/default-coercion :ring-swagger nil}) -(def api-middleware-defaults - {::api-middleware-defaults true - :format {:formats [:json-kw :yaml-kw :edn :transit-json :transit-msgpack] - :params-opts {} - :response-opts {}} - :exceptions {:handlers {::ex/request-validation ex/request-validation-handler - ::ex/request-parsing ex/request-parsing-handler - ::ex/response-validation ex/response-validation-handler - ::ex/default ex/safe-handler}} - :coercion (constantly default-coercion-matchers) - :ring-swagger nil}) - - (defn api-middleware-options [options] - (rsc/deep-merge - (if (contains? options :format) - api-middleware-defaults - muuntaja-api-middleware-defaults) - options)) - -(defn check-options! [options] - ; Break at compile time if there are deprecated options - ; These three have been deprecated with 0.23 - (assert (not (:error-handler (:validation-errors options))) - (str "ERROR: Option: [:validation-errors :error-handler] is no longer supported, " - "use {:exceptions {:handlers {:compojure.api.middleware/request-validation your-handler}}} instead." - "Also note that exception-handler arity has been changed.")) - (assert (not (:catch-core-errors? (:validation-errors options))) - (str "ERROR: Option [:validation-errors :catch-core-errors?] is no longer supported, " - "use {:exceptions {:handlers {:schema.core/error compojure.api.exception/schema-error-handler}}} instead." - "Also note that exception-handler arity has been changed.")) - (assert (not (:exception-handler (:exceptions options))) - (str "ERROR: Option [:exceptions :exception-handler] is no longer supported, " - "use {:exceptions {:handlers {:compojure.api.exception/default your-handler}}} instead." - "Also note that exception-handler arity has been changed.")) - (assert (not (map? (:coercion options))) - (str "ERROR: Option [:coercion] should be a funtion of request->type->matcher, got a map instead." - "From 1.0.0 onwards, you should wrap your type->matcher map into a request-> function. If you " - "want to apply the matchers for all request types, wrap your option with 'constantly'")) - ;; 2.0.0+ - (assert (not (and (contains? options :format) - (contains? options :formats))) - (str "ERROR: Option [:format] is for ring-middleware-format\n" - "and [:formats] is for Muuntaja. At most one can be provided.\n" - "See [[api-middleware]] documentation for more details.\n"))) - -;; ring-middleware-format -(def ^:private default-mime-types - {:json "application/json" - :json-kw "application/json" - :edn "application/edn" - :clojure "application/clojure" - :yaml "application/x-yaml" - :yaml-kw "application/x-yaml" - :yaml-in-html "text/html" - :transit-json "application/transit+json" - :transit-msgpack "application/transit+msgpack"}) - -(defn mime-types - [format] - (get default-mime-types format - (some-> format :content-type))) - -(def ^:private response-only-mimes #{:clojure :yaml-in-html}) - -(defn ->mime-types [formats] (keep mime-types formats)) - -(defn handle-req-error [^Throwable e handler request] - ;; Ring-middleware-format catches all exceptions in req handling, - ;; i.e. (handler req) is inside try-catch. If r-m-f was changed to catch only - ;; exceptions from parsing the request, we wouldn't need to check the exception class. - (if (or (instance? JsonParseException e) (instance? ParserException e)) - (throw (ex-info "Error parsing request" {:type ::ex/request-parsing} e)) - (throw e))) - -(defn serializable? - "Predicate which returns true if the response body is serializable. - That is, return type is set by :return compojure-api key or it's - a collection." - [_ {:keys [body] :as response}] - (when response - (or (:compojure.api.meta/serializable? response) - (coll? body)))) - -(defn wrap-options - "Injects compojure-api options into the request." - [handler options] - (fn [request] - (handler (update-in request [::options] merge options)))) - -(defn- ring-middleware-format-api-middleware - [handler options] - (require 'ring.middleware.format-params - 'ring.middleware.format-response) - (let [{:keys [exceptions format components]} options - {:keys [formats params-opts response-opts]} format] - (cond-> handler - components (wrap-components components) - true ring.middleware.http-response/wrap-http-response - (seq formats) (rsm/wrap-swagger-data {:produces (->mime-types (remove response-only-mimes formats)) - :consumes (->mime-types formats)}) - true (wrap-options (select-keys options [:ring-swagger :coercion])) - (seq formats) ((resolve 'ring.middleware.format-params/wrap-restful-params) - {:formats (remove response-only-mimes formats) - :handle-error handle-req-error - :format-options params-opts}) - exceptions (wrap-exceptions exceptions) - (seq formats) ((resolve 'ring.middleware.format-response/wrap-restful-response) - {:formats formats - :predicate serializable? - :format-options response-opts}) - true wrap-keyword-params - true wrap-nested-params - true wrap-params))) - -(defn- muuntaja-api-middleware - [handler options] - (let [{:keys [exceptions components formats middleware ring-swagger coercion]} options - muuntaja (create-muuntaja formats)] - (-> handler - (cond-> middleware ((compose-middleware middleware))) - (cond-> components (wrap-components components)) - (cond-> muuntaja (wrap-swagger-data {:consumes (m/decodes muuntaja) - :produces (m/encodes muuntaja)})) - (wrap-inject-data - (cond-> {::request/coercion coercion} - muuntaja (assoc ::request/muuntaja muuntaja) - ring-swagger (assoc ::request/ring-swagger ring-swagger))) - (cond-> muuntaja (muuntaja.middleware/wrap-params)) - ;; all but request-parsing exceptions (to make :body-params visible) - (cond-> exceptions (wrap-exceptions - (update exceptions :handlers dissoc ::ex/request-parsing))) - (cond-> muuntaja (muuntaja.middleware/wrap-format-request muuntaja)) - ;; just request-parsing exceptions - (cond-> exceptions (wrap-exceptions - (update exceptions :handlers select-keys [::ex/request-parsing]))) - (cond-> muuntaja (muuntaja.middleware/wrap-format-response muuntaja)) - (cond-> muuntaja (muuntaja.middleware/wrap-format-negotiate muuntaja)) - - ;; these are really slow middleware, 4.5µs => 9.1µs (+100%) - - ;; 7.8µs => 9.1µs (+27%) - wrap-keyword-params - ;; 7.1µs => 7.8µs (+23%) - wrap-nested-params - ;; 4.5µs => 7.1µs (+50%) - wrap-params))) + (rsc/deep-merge api-middleware-defaults options)) ;; TODO: test all options! (https://github.com/metosin/compojure-api/issues/137) (defn api-middleware @@ -438,14 +271,6 @@ - **:formats** for Muuntaja middleware. Value can be a valid muuntaja options-map, a Muuntaja instance or nil (to unmount it). See https://github.com/metosin/muuntaja/blob/master/doc/Configuration.md for details. - Incompatible with :format. - - - **:format** for ring-middleware-format middleware (nil to unmount it). Incompatible with :formats. - - **:formats** sequence of supported formats, e.g. `[:json-kw :edn]` - - **:params-opts** for *ring.middleware.format-params/wrap-restful-params*, - e.g. `{:transit-json {:handlers readers}}` - - **:response-opts** for *ring.middleware.format-params/wrap-restful-response*, - e.g. `{:transit-json {:handlers writers}}` - **:middleware** vector of extra middleware to be applied last (just before the handler). @@ -494,13 +319,47 @@ {})) ;; TODO 2.x stable :ring-middleware-format) - ;; TODO remove for 2.x stable _ (assert (= :muuntaja formatter) - (str "Invalid :formatter: " (pr-str formatter) ". Must be :muuntaja."))] - ((case formatter - :ring-middleware-format ring-middleware-format-api-middleware - :muuntaja muuntaja-api-middleware) - handler options)))) + (str "Invalid :formatter: " (pr-str formatter) ". Must be :muuntaja.")) + options (api-middleware-options options) + {:keys [exceptions components formats middleware ring-swagger coercion]} options + muuntaja (create-muuntaja formats)] + + ;; 1.2.0+ + (assert (not (contains? options :format)) + (str "ERROR: Option [:format] is not used with 2.* version.\n" + "Compojure-api uses now Muuntaja insted of ring-middleware-format,\n" + "the new formatting options for it should be under [:formats]. See\n" + "[[api-middleware]] documentation for more details.\n")) + + (-> handler + (cond-> middleware ((compose-middleware middleware))) + (cond-> components (wrap-components components)) + (cond-> muuntaja (wrap-swagger-data {:consumes (m/decodes muuntaja) + :produces (m/encodes muuntaja)})) + (wrap-inject-data + (cond-> {::request/coercion coercion} + muuntaja (assoc ::request/muuntaja muuntaja) + ring-swagger (assoc ::request/ring-swagger ring-swagger))) + (cond-> muuntaja (muuntaja.middleware/wrap-params)) + ;; all but request-parsing exceptions (to make :body-params visible) + (cond-> exceptions (wrap-exceptions + (update exceptions :handlers dissoc ::ex/request-parsing))) + (cond-> muuntaja (muuntaja.middleware/wrap-format-request muuntaja)) + ;; just request-parsing exceptions + (cond-> exceptions (wrap-exceptions + (update exceptions :handlers select-keys [::ex/request-parsing]))) + (cond-> muuntaja (muuntaja.middleware/wrap-format-response muuntaja)) + (cond-> muuntaja (muuntaja.middleware/wrap-format-negotiate muuntaja)) + + ;; these are really slow middleware, 4.5µs => 9.1µs (+100%) + + ;; 7.8µs => 9.1µs (+27%) + wrap-keyword-params + ;; 7.1µs => 7.8µs (+23%) + wrap-nested-params + ;; 4.5µs => 7.1µs (+50%) + wrap-params)))) (defn wrap-format "Muuntaja format middleware. Can be safely mounted on top of multiple api diff --git a/src/compojure/api/resource.clj b/src/compojure/api/resource.clj index 66d2c156..ec74a061 100644 --- a/src/compojure/api/resource.clj +++ b/src/compojure/api/resource.clj @@ -120,15 +120,6 @@ ([request respond raise] (handle-async info request respond raise)))) -(defn- create-handler1 [info {:keys [coercion]}] - (fn [{:keys [request-method] :as request}] - (let [request (if coercion (assoc-in request mw/coercion-request-ks coercion) request) - ks (if (contains? info request-method) [request-method] [])] - (if-let [handler (resolve-handler info request-method)] - (-> (coerce-request request info ks) - handler - (coerce-response info request ks)))))) - (defn- merge-parameters-and-responses [info] (let [methods (select-keys info (:methods +mappings+))] (-> info @@ -212,21 +203,13 @@ :post {} :handler (constantly (internal-server-error {:reason \"not implemented\"}))})" - ;1.1.x - ([info options] - (let [info (merge-parameters-and-responses info) - root-info (swaggerize (public-root-info info)) - childs (create-childs info) - handler (create-handler1 info options)] - (routes/create nil nil root-info childs handler))) - ;2.x - ([data] - (let [data (merge-parameters-and-responses data) - public-info (swaggerize (public-root-info data)) - info (merge {:public public-info} (select-keys data [:coercion])) - childs (create-childs data) - handler (create-handler data)] - (routes/map->Route - {:info info - :childs childs - :handler handler})))) + [data] + (let [data (merge-parameters-and-responses data) + public-info (swaggerize (public-root-info data)) + info (merge {:public public-info} (select-keys data [:coercion])) + childs (create-childs data) + handler (create-handler data)] + (routes/map->Route + {:info info + :childs childs + :handler handler}))) diff --git a/src/compojure/api/routes.clj b/src/compojure/api/routes.clj index 248a4ef3..a468d326 100644 --- a/src/compojure/api/routes.clj +++ b/src/compojure/api/routes.clj @@ -1,7 +1,6 @@ (ns compojure.api.routes (:require [compojure.core :refer :all] [clojure.string :as string] - [cheshire.core :as cjson] [compojure.api.methods :as methods] [compojure.api.request :as request] [compojure.api.impl.logging :as logging] @@ -146,22 +145,16 @@ {:paths (reduce (fn [acc [path method info]] - (if (fn? (:coercion info)) ;; 1.1.x - (update-in - acc [path method] - (fn [old-info] - (let [info (or old-info info)] - (ensure-path-parameters path info)))) - (if-not (:no-doc info) - (if-let [public-info (->> (get info :public {}) - (coercion/get-apidocs (:coercion info) "swagger"))] - (update-in - acc [path method] - (fn [old-info] - (let [public-info (or old-info public-info)] - (ensure-path-parameters path public-info)))) - acc) - acc))) + (if-not (:no-doc info) + (if-let [public-info (->> (get info :public {}) + (coercion/get-apidocs (:coercion info) "swagger"))] + (update-in + acc [path method] + (fn [old-info] + (let [public-info (or old-info public-info)] + (ensure-path-parameters path public-info)))) + acc) + acc)) (linked/map) routes)}) @@ -216,27 +209,22 @@ (defn- un-quote [s] (str/replace s #"^\"(.+(?=\"$))\"$" "$1")) -(defn- path-string - ([s params] (path-string nil s params)) - ([m s params] - (-> s - (str/replace #":([^/]+)" " :$1 ") - (str/split #" ") - (->> (map - (fn [[head :as token]] - (if (= head \:) - (let [key (keyword (subs token 1)) - value (key params)] - (if value - (un-quote (if m - (slurp (m/encode m "application/json" value)) - ;;1.1.x - (cjson/generate-string value))) - (throw - (IllegalArgumentException. - (str "Missing path-parameter " key " for path " s))))) - token))) - (apply str))))) +(defn- path-string [m s params] + (-> s + (str/replace #":([^/]+)" " :$1 ") + (str/split #" ") + (->> (map + (fn [[head :as token]] + (if (= head \:) + (let [key (keyword (subs token 1)) + value (key params)] + (if value + (un-quote (slurp (m/encode m "application/json" value))) + (throw + (IllegalArgumentException. + (str "Missing path-parameter " key " for path " s))))) + token))) + (apply str)))) (defn path-for* "Extracts the lookup-table from request and finds a route by name."