From ce5455318441f46ef4c426a2e2029b06d0d175dc Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 14 May 2024 17:16:09 -0500 Subject: [PATCH] [skip ci] --- CONTRIBUTING.md => .github/CONTRIBUTING.md | 0 project.clj | 71 +- src/compojure/api/api.clj | 59 +- src/compojure/api/coerce.clj | 1 + src/compojure/api/core.clj | 24 +- src/compojure/api/exception.clj | 79 +- src/compojure/api/impl/logging.clj | 21 +- src/compojure/api/meta.clj | 1057 +++++++++++++++++--- src/compojure/api/middleware.clj | 416 +++++--- src/compojure/api/resource.clj | 161 ++- src/compojure/api/routes.clj | 108 +- src/compojure/api/swagger.clj | 26 +- src/compojure/api/sweet.clj | 12 +- src/compojure/api/upload.clj | 2 +- 14 files changed, 1551 insertions(+), 486 deletions(-) rename CONTRIBUTING.md => .github/CONTRIBUTING.md (100%) diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/project.clj b/project.clj index ac12927b..e08a7716 100644 --- a/project.clj +++ b/project.clj @@ -1,22 +1,27 @@ -(defproject metosin/compojure-api "1.1.15-SNAPSHOT" +(defproject metosin/compojure-api "2.0.0-alpha34-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"} - :dependencies [[prismatic/plumbing "0.6.0"] - [cheshire "5.13.0"] - [compojure "1.6.1"] - [prismatic/schema "1.1.12"] - [org.tobereplaced/lettercase "1.0.0"] - [frankiesardo/linked "1.3.0"] - [ring-middleware-format "0.7.4"] + :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" ] + [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"] - [metosin/ring-swagger-ui "2.2.10"]] + + ;; Fix dependency conflicts + [clj-time "0.15.2"] + [joda-time "2.10.5"] + [riddley "0.2.0"]] + :pedantic? :abort :profiles {:uberjar {:aot :all :ring {:handler examples.thingie/app} :source-paths ["examples/thingie/src"] @@ -24,27 +29,25 @@ [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"] + :dev {:plugins [[lein-clojars "0.9.1"] + [lein-ring "0.12.5"] [funcool/codeina "0.5.0"]] :dependencies [[org.clojure/clojure "1.9.0"] - ;; bump - [fipp "0.6.26"] - [metosin/spec-tools "0.10.6"] - [metosin/muuntaja "0.6.6"] - [metosin/jsonista "0.2.5"] - [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"] - [slingshot "0.12.2"] - [peridot "0.5.1"] - [javax.servlet/servlet-api "2.5"] - [midje "1.9.9"] + [org.clojure/core.unify "0.6.0"] + [org.clojure/core.async "0.6.532"] + [javax.servlet/javax.servlet-api "4.0.1"] + [peridot "0.5.2"] [com.stuartsierra/component "0.4.0"] + [expound "0.8.2"] + [metosin/jsonista "0.2.5"] [reloaded.repl "0.2.4"] + [metosin/muuntaja-msgpack "0.6.6"] + [metosin/muuntaja-yaml "0.6.6"] + [org.immutant/immutant "2.1.10"] [http-kit "2.3.0"] [criterium "0.4.5"]] + :jvm-opts ["-Dcompojure.api.meta.static-context-coach={:default :assert :verbose true}"] + :test-paths ["test19"] :ring {:handler examples.thingie/app :reload-paths ["src" "examples/thingie/src"]} :source-paths ["examples/thingie/src" "examples/thingie/dev-src"] @@ -52,10 +55,16 @@ :perf {:jvm-opts ^:replace ["-server" "-Xmx4096m" "-Dclojure.compiler.direct-linking=true"]} - :logging {:dependencies [[org.clojure/tools.logging "0.5.0"]]} + :logging {:dependencies [[org.clojure/tools.logging "0.5.0"] + [org.slf4j/jcl-over-slf4j "1.7.30"] + [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"]]}} + :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] :add-linters [:unused-namespaces]} :codeina {:sources ["src"] @@ -79,10 +88,10 @@ ["change" "version" "leiningen.release/bump-version"] ["vcs" "commit"] ["vcs" "push"]] - :aliases {"all" ["with-profile" "dev:dev,logging:dev,1.10:dev,1.11:dev,1.12"] + :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" ["midje"] + "test-ancient" ["test"] "perf" ["with-profile" "default,dev,perf"] "deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."} - ["do" ["clean"] ["midje"] ["deploy" "clojars"]]}) + ["do" ["clean"] ["test"] ["deploy" "clojars"]]}) diff --git a/src/compojure/api/api.clj b/src/compojure/api/api.clj index 763a0dc2..89835e78 100644 --- a/src/compojure/api/api.clj +++ b/src/compojure/api/api.clj @@ -1,16 +1,17 @@ (ns compojure.api.api (:require [compojure.api.core :as c] [compojure.api.swagger :as swagger] - [compojure.api.middleware :as middleware] + [compojure.api.middleware :as mw] + [compojure.api.request :as request] [compojure.api.routes :as routes] [compojure.api.common :as common] - [compojure.api.coerce :as coerce] + [compojure.api.request :as request] [ring.swagger.common :as rsc] [ring.swagger.middleware :as rsm])) (def api-defaults (merge - middleware/api-middleware-defaults + mw/api-middleware-defaults {:api {:invalid-routes-fn routes/log-invalid-child-routes :disable-api-middleware? false} :swagger {:ui nil, :spec nil}})) @@ -23,8 +24,7 @@ options map as the first parameter: (api - {:formats [:json-kw :edn :transit-msgpack :transit-json] - :exceptions {:handlers {:compojure.api.exception/default my-logging-handler}} + {:exceptions {:handlers {:compojure.api.exception/default my-logging-handler}} :api {:invalid-routes-fn (constantly nil)} :swagger {:spec \"/swagger.json\" :ui \"/api-docs\" @@ -47,30 +47,55 @@ ### api-middleware options + See `compojure.api.middleware/api-middleware` for more available options. + " (:doc (meta #'compojure.api.middleware/api-middleware)))} api [& body] (let [[options handlers] (common/extract-parameters body false) + _ (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")) + _ (when (and (not (:formatter options)) + (not (contains? options :formats)) + (not (System/getProperty "compojure.api.middleware.global-default-formatter"))) + (throw (ex-info (str "ERROR: Please set `:formatter :muuntaja` in the options map of `api`.\n" + "e.g., (api {:formatter :muuntaja} routes...)\n" + "To prepare for backwards compatibility with compojure-api 1.x, the formatting library must be\n" + "explicitly chosen if not configured by `:format` (ring-middleware-format) or \n" + "`:formats` (muuntaja). Once 2.x is stable, the default will be `:formatter :ring-middleware-format`.\n" + "To globally override the formatter, use -Dcompojure.api.middleware.global-default-formatter=:muuntaja") + {}))) options (rsc/deep-merge api-defaults options) handler (apply c/routes (concat [(swagger/swagger-routes (:swagger options))] handlers)) - routes (routes/get-routes handler (:api options)) + partial-api-route (routes/map->Route + {:childs [handler] + :info {:coercion (:coercion options)}}) + routes (routes/get-routes partial-api-route (:api options)) paths (-> routes routes/ring-swagger-paths swagger/transform-operations) lookup (routes/route-lookup-table routes) swagger-data (get-in options [:swagger :data]) enable-api-middleware? (not (get-in options [:api :disable-api-middleware?])) - api-handler (cond-> handler - swagger-data (rsm/wrap-swagger-data swagger-data) - enable-api-middleware? (middleware/api-middleware - (dissoc options :api :swagger)) - true (middleware/wrap-options - {:paths paths - :coercer (coerce/memoized-coercer) - :lookup lookup}))] - (routes/create nil nil {} [handler] api-handler))) + api-middleware-options (dissoc (mw/api-middleware-options (assoc (dissoc options :api :swagger) ::via-api true)) + ::mw/api-middleware-defaults) + api-handler (-> handler + (cond-> swagger-data (rsm/wrap-swagger-data swagger-data)) + (cond-> enable-api-middleware? (mw/api-middleware + api-middleware-options)) + (mw/wrap-inject-data + {::request/paths paths + ::request/lookup lookup}))] + (assoc partial-api-route :handler api-handler))) (defmacro - ^{:doc (str - "Defines an api. + ^{:superseded-by "api" + :deprecated "2.0.0" + :doc (str + "Deprecated: please use (def name (api ...body..)) + + Defines an api. API middleware options: diff --git a/src/compojure/api/coerce.clj b/src/compojure/api/coerce.clj index c63a8b6d..5a147a14 100644 --- a/src/compojure/api/coerce.clj +++ b/src/compojure/api/coerce.clj @@ -1,3 +1,4 @@ +;; 1.1.x (ns compojure.api.coerce (:require [schema.coerce :as sc] [compojure.api.middleware :as mw] diff --git a/src/compojure/api/core.clj b/src/compojure/api/core.clj index 07b22a14..0c9cb97e 100644 --- a/src/compojure/api/core.clj +++ b/src/compojure/api/core.clj @@ -54,16 +54,16 @@ :deprecated "1.1.14" :superseded-by "route-middleware"} [middleware & body] - (when (not= "true" (System/getProperty "compojure.api.core.suppress-middleware-warning")) - (println (str "compojure.api.core.middleware is deprecated because of security issues. " - "Please use route-middleware instead. middleware will be disabled in a future release." - "Set -dcompojure.api.core.suppress-middleware-warning=true to suppress this warning."))) + (assert (= "true" (System/getProperty "compojure.api.core.allow-dangerous-middleware")) + (str "compojure.api.core.middleware is deprecated because of security issues. " + "Please use route-middleware instead. " + "Set compojure.api.core.allow-dangerous-middleware=true to keep using middleware.")) `(let [body# (routes ~@body) wrap-mw# (mw/compose-middleware ~middleware)] (routes/create nil nil {} [body#] (wrap-mw# body#)))) (defn route-middleware - "Wraps routes with given middleware using thread-first macro." + "Wraps routes with given middlewares using thread-first macro." {:style/indent 1 :supercedes "middleware"} [middleware & body] @@ -76,11 +76,11 @@ (defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true :&form &form :&env &env})) -(defmacro GET {:style/indent 2} [& args] (meta/restructure :get args nil)) -(defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args nil)) -(defmacro HEAD {:style/indent 2} [& args] (meta/restructure :head args nil)) -(defmacro PATCH {:style/indent 2} [& args] (meta/restructure :patch args nil)) -(defmacro DELETE {:style/indent 2} [& args] (meta/restructure :delete args nil)) +(defmacro GET {:style/indent 2} [& args] (meta/restructure :get args nil)) +(defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args nil)) +(defmacro HEAD {:style/indent 2} [& args] (meta/restructure :head args nil)) +(defmacro PATCH {:style/indent 2} [& args] (meta/restructure :patch args nil)) +(defmacro DELETE {:style/indent 2} [& args] (meta/restructure :delete args nil)) (defmacro OPTIONS {:style/indent 2} [& args] (meta/restructure :options args nil)) -(defmacro POST {:style/indent 2} [& args] (meta/restructure :post args nil)) -(defmacro PUT {:style/indent 2} [& args] (meta/restructure :put args nil)) +(defmacro POST {:style/indent 2} [& args] (meta/restructure :post args nil)) +(defmacro PUT {:style/indent 2} [& args] (meta/restructure :put args nil)) diff --git a/src/compojure/api/exception.clj b/src/compojure/api/exception.clj index 0844bc5d..cc281da6 100644 --- a/src/compojure/api/exception.clj +++ b/src/compojure/api/exception.clj @@ -2,10 +2,8 @@ (:require [ring.util.http-response :as response] [clojure.walk :as walk] [compojure.api.impl.logging :as logging] - [schema.utils :as su]) - (:import [schema.utils ValidationError NamedError] - [com.fasterxml.jackson.core JsonParseException] - [org.yaml.snakeyaml.parser ParserException])) + [compojure.api.coercion.core :as cc] + [compojure.api.coercion.schema])) ;; ;; Default exception handlers @@ -21,42 +19,62 @@ (response/internal-server-error {:type "unknown-exception" :class (.getName (.getClass e))})) -(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)) - +;; TODO: coercion should handle how to publish data (defn response-validation-handler - "Creates error response based on Schema error." + "Creates error response based on a response error. The following keys are available: + + :type type of the exception (::response-validation) + :coercion coercion instance used + :in location of the value ([:response :body]) + :schema schema to be validated against + :error schema error + :request raw request + :response raw response" [e data req] - (response/internal-server-error {:errors (stringify-error (su/error-val data))})) + (response/internal-server-error + (-> data + (dissoc :request :response) + (update :coercion cc/get-name) + (assoc :value (-> data :response :body)) + (->> (cc/encode-error (:coercion data)))))) +;; TODO: coercion should handle how to publish data (defn request-validation-handler - "Creates error response based on Schema error." + "Creates error response based on Schema error. The following keys are available: + + :type type of the exception (::request-validation) + :coercion coercion instance used + :value value that was validated + :in location of the value (e.g. [:request :query-params]) + :schema schema to be validated against + :error schema error + :request raw request" [e data req] - (response/bad-request {:errors (stringify-error (su/error-val data))})) + (response/bad-request + (-> data + (dissoc :request) + (update :coercion cc/get-name) + (->> (cc/encode-error (:coercion data)))))) + +(defn http-response-handler + "reads response from ex-data :response" + [_ {:keys [response]} _] + response) (defn schema-error-handler "Creates error response based on Schema error." [e data req] - ; FIXME: Why error is not wrapped to ErrorContainer here? - (response/bad-request {:errors (stringify-error (:error data))})) + (response/bad-request + {:errors (compojure.api.coercion.schema/stringify (:error data))})) (defn request-parsing-handler [^Exception ex data req] - (let [cause (.getCause ex)] - (response/bad-request {:type (cond - (instance? JsonParseException cause) "json-parse-exception" - (instance? ParserException cause) "yaml-parse-exception" - :else "parse-exception") - :message (.getMessage cause)}))) - + (let [cause (.getCause ex) + original (.getCause cause)] + (response/bad-request + (merge (select-keys data [:type :format :charset]) + (if original {:original (.getMessage original)}) + {:message (.getMessage cause)})))) ;; ;; Logging ;; @@ -75,5 +93,6 @@ ;; Mappings from other Exception types to our base types ;; -(def legacy-exception-types - {:ring.swagger.schema/validation ::request-validation}) +(def mapped-exception-types + {:ring.swagger.schema/validation ::request-validation + :muuntaja/decode ::request-parsing}) diff --git a/src/compojure/api/impl/logging.clj b/src/compojure/api/impl/logging.clj index 1244fb30..f1ad70cb 100644 --- a/src/compojure/api/impl/logging.clj +++ b/src/compojure/api/impl/logging.clj @@ -6,17 +6,18 @@ (declare log!) ;; use c.t.l logging if available, default to console logging -(if (find-ns 'clojure.tools.logging) +(try (eval `(do (require 'clojure.tools.logging) (defmacro ~'log! [& ~'args] - `(do - (clojure.tools.logging/log ~@~'args))))) - (let [log (fn [level more] (println (.toUpperCase (name level)) (str/join " " more)))] - (defn log! [level x & more] - (if (instance? Throwable x) - (do - (log level more) - (.printStackTrace ^Throwable x)) - (log level (into [x] more)))))) + `(clojure.tools.logging/log ~@~'args)))) + (catch Exception _ + (let [log (fn [level more] (println (.toUpperCase (name level)) (str/join " " more)))] + (defn log! [level x & more] + (if (instance? Throwable x) + (do + (log level more) + (.printStackTrace ^Throwable x)) + (log level (into [x] more)))) + (log! :warn "clojure.tools.logging not found on classpath, compojure.api logging to console.")))) diff --git a/src/compojure/api/meta.clj b/src/compojure/api/meta.clj index 1b093871..4a2a4e38 100644 --- a/src/compojure/api/meta.clj +++ b/src/compojure/api/meta.clj @@ -1,5 +1,7 @@ (ns compojure.api.meta - (:require [compojure.api.common :as common :refer [extract-parameters]] + (:require [clojure.edn :as edn] ;; TODO load lazily + [clojure.pprint :as pp] ;;TODO load lazily + [compojure.api.common :refer [extract-parameters]] [compojure.api.middleware :as mw] [compojure.api.routes :as routes] [plumbing.core :as p] @@ -8,13 +10,19 @@ [ring.swagger.json-schema :as js] [schema.core :as s] [schema-tools.core :as st] - [compojure.api.coerce :as coerce] - compojure.core)) + [compojure.api.coercion :as coercion] + [compojure.api.help :as help] + compojure.core + compojure.api.compojure-compat + [compojure.api.common :as common])) (def +compojure-api-request+ "lexically bound ring-request for handlers." '+compojure-api-request+) +(defn- var->sym [^clojure.lang.Var v] + (symbol (-> v .ns ns-name name) (-> v .sym name))) + ;; ;; Schema ;; @@ -23,19 +31,34 @@ (dissoc schema 'schema.core/Keyword)) (defn fnk-schema [bind] - (->> - (:input-schema - (fnk-impl/letk-input-schema-and-body-form - nil (with-meta bind {:schema s/Any}) [] nil)) - reverse - (into {}))) + (try + (->> + (:input-schema + (fnk-impl/letk-input-schema-and-body-form + nil (with-meta bind {:schema s/Any}) [] nil)) + reverse + (into {})) + (catch Exception _ + (let [hint (cond + ;; [a ::a] + (and (= 2 (count bind)) (keyword? (second bind))) + (str "[" (first bind) " :- " (second bind) "]") + + :else nil)] + (throw (IllegalArgumentException. + (str "Binding is not valid, please refer to " + "https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax\n" + " for more information.\n\n" + " binding: " bind "\n\n" + (if hint (str " did you mean: " hint "\n\n"))))))))) (s/defn src-coerce! "Return source code for coerce! for a schema with coercion type, extracted from a key in a ring request." - [schema, key, type :- mw/CoercionType] - (assert (not (#{:query :json} type)) (str type " is DEPRECATED since 0.22.0. Use :body or :string instead.")) - `(coerce/coerce! ~schema ~key ~type ~+compojure-api-request+)) + ([schema, key, type] + (src-coerce! schema, key, type, true)) + ([schema, key, type, keywordize?] + `(coercion/coerce-request! ~schema ~key ~type ~keywordize? false ~+compojure-api-request+))) (defn- convert-return [schema] {200 {:schema schema @@ -51,179 +74,565 @@ (fn [k v acc] k)) ;; -;; Pass-through swagger metadata +;; dynamic +;; + +(defmethod help/help-for [:meta :dynamic] [_ _] + (help/text + "If set to to `true`, makes a `context` dynamic," + "which wraps the body in a closure that is evaluated on each request." + "This is the default behavior in vanilla compojure. In compojure-api," + "this is also the usual behavior, except:" + "If the context does not bind any variables and its body contains" + "just top-level calls to compojure.api endpoint macros like GET," + "then the body will be cached for each request." + + (help/code + "(context \"/static\" []" + " :static true" + " (when (= 0 (random-int 2))" + " (println 'ping!) ;; never printed during request" + " ;; mounting decided once" + " (GET \"/ping\" [] (ok \"pong\"))))" + "" + "(context \"/dynamic\" []" + " :dynamic true" + " (when (= 0 (random-int 2))" + " (println 'ping!) ;; printed 50% of requests" + " ;; mounted for 50% of requests" + " (GET \"/ping\" [] (ok \"pong\"))))"))) + +(defmethod restructure-param :dynamic [k v acc] + (update-in acc [:info :public] assoc k v)) + +(defmethod help/help-for [:meta :static] [_ _] + (help/text + "If set to to `true`, makes a `context` static," + "which resolves the body before processing requests." + "This is much faster than :dynamic contexts at the" + "cost of expressivity: routes under a static context" + "cannot change based on the request." + + (help/code + "(context \"/static\" []" + " :static true" + " (when (= 0 (random-int 2))" + " (println 'ping!) ;; never printed during request" + " ;; mounting decided once" + " (GET \"/ping\" [] (ok \"pong\"))))" + "" + "(context \"/dynamic\" []" + " :dynamic true" + " (when (= 0 (random-int 2))" + " (println 'ping!) ;; printed 50% of requests" + " ;; mounted for 50% of requests" + " (GET \"/ping\" [] (ok \"pong\")))"))) + +(defmethod restructure-param :static [k v acc] + (update-in acc [:info :public] assoc k v)) + +;; +;; summary ;; +(defmethod help/help-for [:meta :summary] [_ _] + (help/text + "A short summary of what the operation does. For maximum" + "readability in the swagger-ui, this field SHOULD be less" + "than 120 characters.\n" + (help/code + "(GET \"/ok\" []" + " :summary \"this endpoint alreays returns 200\"" + " (ok))"))) + (defmethod restructure-param :summary [k v acc] - (update-in acc [:swagger] assoc k v)) + (update-in acc [:info :public] assoc k v)) + +;; +;; description +;; + +(defmethod help/help-for [:meta :description] [_ _] + (help/text + "A verbose explanation of the operation behavior." + "GFM syntax can be used for rich text representation." + (help/code + "(GET \"/ok\" []" + " :description \"this is a `GET`.\"" + " (ok))"))) (defmethod restructure-param :description [k v acc] - (update-in acc [:swagger] assoc k v)) + (update-in acc [:info :public] assoc k v)) + +;; +;; OperationId +;; + +(defmethod help/help-for [:meta :operationId] [_ _] + (help/text + "Unique string used to identify the operation. The id MUST be" + "unique among all operations described in the API. Tools and" + "libraries MAY use the operationId to uniquely identify an operation," + "therefore, it is recommended to follow common programming naming conventions.\n" + (help/code + "(GET \"/pets\" []" + " :operationId \"get-pets\"" + " (ok))"))) (defmethod restructure-param :operationId [k v acc] - (update-in acc [:swagger] assoc k v)) + (update-in acc [:info :public] assoc k v)) + +;; +;; Consumes +;; + +(defmethod help/help-for [:meta :consumes] [_ _] + (help/text + "Swagger-ui hint about mime-types the endpoints can consume." + "Takes a vector or set. Just for docs.\n" + (help/code + "(GET \"/edn-endpoint\" []" + " :consumes #{\"application/edn\"}" + " :produces #(\"application/edn\"}" + " (ok))"))) (defmethod restructure-param :consumes [k v acc] - (update-in acc [:swagger] assoc k v)) + (update-in acc [:info :public] assoc k v)) + +;; +;; Provides +;; + +(defmethod help/help-for [:meta :produces] [_ _] + (help/text + "Swagger-ui hint about mime-types the endpoints produces." + "Takes a vector or set. Just for docs.\n" + (help/code + "(GET \"/edn-endpoint\" []" + " :consumes #{\"application/edn\"}" + " :produces #{\"application/edn\"}" + " (ok))"))) (defmethod restructure-param :produces [k v acc] - (update-in acc [:swagger] assoc k v)) + (update-in acc [:info :public] assoc k v)) ;; -;; Smart restructurings +;; no-doc ;; -; Boolean to discard the route out from api documentation -; Example: -; :no-doc true +(defmethod help/help-for [:meta :no-doc] [_ _] + (help/text + "Boolean to discard the route out from api documentation\n" + (help/code + "(GET \"/secret\" []" + " :no-doc true" + " (ok))"))) + (defmethod restructure-param :no-doc [_ v acc] - (update-in acc [:swagger] assoc :x-no-doc v)) - -; publishes the data as swagger-parameters without any side-effects / coercion. -; Examples: -; :swagger {:responses {200 {:schema User} -; 404 {:schema Error -; :description "Not Found"} } -; :paramerers {:query {:q s/Str} -; :body NewUser}}} + (update-in acc [:info] assoc :no-doc v)) + +;; +;; swagger +;; + +(defmethod help/help-for [:meta :swagger] [_ _] + (help/text + "Raw swagger-data, just for docs.\n" + (help/code + "(GET \"/ok\" []" + " :swagger {:responses {200 {:schema User}" + " 404 {:schema Error" + " :description \"Not Found\"}}" + " :parameters {:query {:q s/Str}" + " :body NewUser}}" + " (ok))"))) + (defmethod restructure-param :swagger [_ swagger acc] - (assoc-in acc [:swagger :swagger] swagger)) + (assoc-in acc [:info :public :swagger] swagger)) + +;; +;; name +;; + +(defmethod help/help-for [:meta :name] [_ _] + (help/text + "Name of a route. Used in bi-directional routing.\n" + (help/code + "(context \"/user\" []" + " (GET \"/:id\" []" + " :path-params [id :- s/Int]" + " :name ::user" + " (ok))" + " (POST \"/\" []" + " (created (path-for ::user {:id (random-int)}))))"))) -; Route name, used with path-for -; Example: -; :name :user-route (defmethod restructure-param :name [_ v acc] - (update-in acc [:swagger] assoc :x-name v)) + (-> acc + (assoc-in [:info :name] v) + (assoc-in [:info :public :x-name] v))) + +;; +;; tags +;; + +(defmethod help/help-for [:meta :tags] [_ _] + (help/text + "Takes a sequence or a set of tags for the swagger-doc.\n" + (help/code + "(GET \"/ok\" []" + " :tags #{\"admin\", \"common\"}" + " (ok))"))) -; Tags for api categorization. Ignores duplicates. -; Examples: -; :tags [:admin] (defmethod restructure-param :tags [_ tags acc] - (update-in acc [:swagger :tags] (comp set into) tags)) + (update-in acc [:info :public :tags] (comp set into) tags)) + +;; +;; return +;; + +(defmethod help/help-for [:meta :return] [_ _] + (help/text + "Request (status 200) body schema for coercion and api-docs.\n" + (help/code + "(GET \"/user\" []" + " :return {:name (s/maybe s/Str)" + " (ok {:name \"Kirsi\"))"))) -; Defines a return type and coerces the return value of a body against it. -; Examples: -; :return MySchema -; :return {:value String} -; :return #{{:key (s/maybe Long)}} (defmethod restructure-param :return [_ schema acc] - (let [response (convert-return schema)] + (let [response (convert-return schema) + g (gensym 'response)] (-> acc - (update-in [:swagger :responses] (fnil conj []) response) - (update-in [:responses] (fnil conj []) response)))) - -; value is a map of http-response-code -> Schema. Translates to both swagger -; parameters and return schema coercion. Schemas can be decorated with meta-data. -; Examples: -; :responses {403 nil} -; :responses {403 {:schema ErrorEnvelope}} -; :responses {403 {:schema ErrorEnvelope, :description \"Underflow\"}} + (update-in [:outer-lets] into [g response]) + (update-in [:info :public :responses] (fnil conj []) g) + (update-in [:responses] (fnil conj []) g)))) + +;; +;; responses +;; + +(defmethod help/help-for [:meta :responses] [_ _] + (help/text + "Map of response status code (or :default) -> response schema." + "Response can have keys :schema, :description and :headers.\n" + (help/code + "(GET \"/user\" []" + " :responses {200 {:schema {:name (s/maybe s/Str)}}" + " 404 {:schema {:error s/Int}" + " :description \"not found\"}" + " (ok {:name nil}))" + "" + "(GET \"/user\" []" + " :responses {200 {:schema s/Any, :description \"ok\"}" + " :default {:schema s/Any, :description \"default\"" + " (bad-request \"kosh\"))"))) + (defmethod restructure-param :responses [_ responses acc] - (-> acc - (update-in [:swagger :responses] (fnil conj []) responses) - (update-in [:responses] (fnil conj []) responses))) - -; reads body-params into a enhanced let. First parameter is the let symbol, -; second is the Schema to be coerced! against. -; Examples: -; :body [user User] -(defmethod restructure-param :body [_ [value schema] acc] - (-> acc - (update-in [:lets] into [value (src-coerce! schema :body-params :body)]) - (assoc-in [:swagger :parameters :body] schema))) - -; reads query-params into a enhanced let. First parameter is the let symbol, -; second is the Schema to be coerced! against. -; Examples: -; :query [user User] -(defmethod restructure-param :query [_ [value schema] acc] - (-> acc - (update-in [:lets] into [value (src-coerce! schema :query-params :string)]) - (assoc-in [:swagger :parameters :query] schema))) - -; reads header-params into a enhanced let. First parameter is the let symbol, -; second is the Schema to be coerced! against. -; Examples: -; :headers [headers Headers] -(defmethod restructure-param :headers [_ [value schema] acc] - (-> acc - (update-in [:lets] into [value (src-coerce! schema :headers :string)]) - (assoc-in [:swagger :parameters :header] schema))) + (let [g (gensym 'responses)] + (-> acc + (update :outer-lets into [g responses]) + (update-in [:info :public :responses] (fnil conj []) g) + (update-in [:responses] (fnil conj []) g)))) + +;; +;; body +;; + +(defmethod help/help-for [:meta :body] [_ _] + (help/text + "body-params with let-syntax. First parameter is the symbol," + "second is the Schema used in coercion (and api-docs).\n" + (help/code + "(POST \"/echo\" []" + " :body [body User]" + " (ok body))"))) + +(defmethod restructure-param :body [_ [value schema :as bv] acc] + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-body")) + (assert (= 2 (count bv)) + (str ":body should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-body=true"))) + (let [g (gensym 'body-schema)] + (-> acc + (update :outer-lets into [g schema]) + (update-in [:lets] into [value (src-coerce! g :body-params :body false)]) + (assoc-in [:info :public :parameters :body] g)))) + +;; +;; query +;; + +(defmethod help/help-for [:meta :query] [_ _] + (help/text + "query-params with let-syntax. First parameter is the symbol," + "second is the Schema used in coercion (and api-docs).\n" + (help/code + "(GET \"/search\" []" + " :query [params {:q s/Str, :max s/Int}]" + " (ok params))"))) + +(defmethod restructure-param :query [_ [value schema :as bv] acc] + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-query")) + (assert (= 2 (count bv)) + (str ":query should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-query=true"))) + (let [g (gensym 'query-schema)] + (-> acc + (update :outer-lets into [g schema]) + (update-in [:lets] into [value (src-coerce! g :query-params :string)]) + (assoc-in [:info :public :parameters :query] g)))) + +;; +;; headers +;; + +(defmethod help/help-for [:meta :headers] [_ _] + (help/text + "header-params with let-syntax. First parameter is the symbol," + "second is the Schema used in coercion (and api-docs).\n" + (help/code + "(GET \"/headers\" []" + " :headers [headers HeaderSchema]" + " (ok headers))"))) + +(defmethod restructure-param :headers [_ [value schema :as bv] acc] + (when-not (= "true" (System/getProperty "compojure.api.meta.allow-bad-headers")) + (assert (= 2 (count bv)) + (str ":headers should be [sym schema], provided: " bv + "\nDisable this check with -Dcompojure.api.meta.allow-bad-headers=true"))) + (let [g (gensym 'headers-schema)] + (-> acc + (update :outer-lets into [g schema]) + (update-in [:lets] into [value (src-coerce! g :headers :string)]) + (assoc-in [:info :public :parameters :header] g)))) + +;; +;; body-params +;; + +(defmethod help/help-for [:meta :body-params] [_ _] + (help/text + "body-params with letk. Schema is used for both coercion and api-docs." + "https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax\n" + (help/code + "(POST \"/math\" []" + " :body-params [x :- s/Int, {y :- s/Int 1}]" + " (ok {:total (+ x y)}))"))) -; restructures body-params with plumbing letk notation. Example: -; :body-params [id :- Long name :- String] (defmethod restructure-param :body-params [_ body-params acc] - (let [schema (strict (fnk-schema body-params))] + (let [schema (strict (fnk-schema body-params)) + g (gensym 'body-params-schema)] (-> acc - (update-in [:letks] into [body-params (src-coerce! schema :body-params :body)]) - (assoc-in [:swagger :parameters :body] schema)))) + (update :outer-lets into [g schema]) + (update-in [:letks] into [body-params (src-coerce! g :body-params :body)]) + (assoc-in [:info :public :parameters :body] g)))) + +;; +;; form-params +;; + +(defmethod help/help-for [:meta :form-params] [_ _] + (help/text + "form-params with letk. Schema is used for both coercion and api-docs." + "Also sets the :swagger :consumes to #{\"application/x-www-form-urlencoded\"}." + "https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax\n" + (help/code + "(POST \"/math\" []" + " :form-params [x :- s/Int, {y :- s/Int 1}]" + " (ok {:total (+ x y)}))"))) -; restructures form-params with plumbing letk notation. Example: -; :form-params [id :- Long name :- String] (defmethod restructure-param :form-params [_ form-params acc] - (let [schema (strict (fnk-schema form-params))] + (let [schema (strict (fnk-schema form-params)) + g (gensym 'form-params-schema)] (-> acc - (update-in [:letks] into [form-params (src-coerce! schema :form-params :string)]) - (update-in [:swagger :parameters :formData] st/merge schema) - (assoc-in [:swagger :consumes] ["application/x-www-form-urlencoded"])))) + (update :outer-lets into [g schema]) + (update-in [:letks] into [form-params (src-coerce! g :form-params :string)]) + (update-in [:info :public :parameters :formData] #(if % (list `st/merge % g) g)) + (assoc-in [:info :public :consumes] ["application/x-www-form-urlencoded"])))) + +;; +;; multipart-params +;; + +(defmethod help/help-for [:meta :multipart-params] [_ _] + (help/text + "multipart-params with letk. Schema is used for both coercion and api-docs." + "Should be used with a middleware to do the actual file-upload." + "Sets also the :swagger :consumes to #{\"multipart/form-data\"}." + "https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax\n" + (help/code + "(require '[compojure.api.upload :as upload]" + "" + "(POST \"/file\" []" + " :multipart-params [foo :- upload/TempFileUpload]" + " :middleware [upload/wrap-multipart-params]" + " (ok (dissoc foo :tempfile)))"))) -; restructures multipart-params with plumbing letk notation and consumes "multipart/form-data" -; :multipart-params [file :- compojure.api.upload/TempFileUpload] (defmethod restructure-param :multipart-params [_ params acc] - (let [schema (strict (fnk-schema params))] + (let [schema (strict (fnk-schema params)) + g (gensym 'multipart-params-schema)] (-> acc - (update-in [:letks] into [params (src-coerce! schema :multipart-params :string)]) - (update-in [:swagger :parameters :formData] st/merge schema) - (assoc-in [:swagger :consumes] ["multipart/form-data"])))) + (update :outer-lets into [g schema]) + (update-in [:letks] into [params (src-coerce! g :multipart-params :string)]) + (update-in [:info :public :parameters :formData] #(if % (list `st/merge % g) g)) + (assoc-in [:info :public :consumes] ["multipart/form-data"])))) + +;; +;; header-params +;; + +(defmethod help/help-for [:meta :header-params] [_ _] + (help/text + "header-params with letk. Schema is used for both coercion and api-docs." + "https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax\n" + (help/code + "(POST \"/math\" []" + " :header-params [x :- s/Int, {y :- s/Int 1}]" + " (ok {:total (+ x y)}))"))) -; restructures header-params with plumbing letk notation. Example: -; :header-params [id :- Long name :- String] (defmethod restructure-param :header-params [_ header-params acc] - (let [schema (fnk-schema header-params)] + (let [schema (fnk-schema header-params) + g (gensym 'multipart-params-schema)] (-> acc - (update-in [:letks] into [header-params (src-coerce! schema :headers :string)]) - (assoc-in [:swagger :parameters :header] schema)))) + (update :outer-lets into [g schema]) + (update-in [:letks] into [header-params (src-coerce! g :headers :string)]) + (assoc-in [:info :public :parameters :header] g)))) + +;; +;; :query-params +;; + +(defmethod help/help-for [:meta :query-params] [_ _] + (help/text + "query-params with letk. Schema is used for both coercion and api-docs." + "https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax\n" + (help/code + "(POST \"/math\" []" + " :query-params [x :- s/Int, {y :- s/Int 1}]" + " (ok {:total (+ x y)}))"))) -; restructures query-params with plumbing letk notation. Example: -; :query-params [id :- Long name :- String] (defmethod restructure-param :query-params [_ query-params acc] - (let [schema (fnk-schema query-params)] + (let [schema (fnk-schema query-params) + g (gensym 'multipart-params-schema)] (-> acc - (update-in [:letks] into [query-params (src-coerce! schema :query-params :string)]) - (assoc-in [:swagger :parameters :query] schema)))) + (update :outer-lets into [g schema]) + (update-in [:letks] into [query-params (src-coerce! g :query-params :string)]) + (assoc-in [:info :public :parameters :query] g)))) + +;; +;; path-params +;; + +(defmethod help/help-for [:meta :path-params] [_ _] + (help/text + "path-params with letk. Schema is used for both coercion and api-docs." + "https://github.com/plumatic/plumbing/tree/master/src/plumbing/fnk#fnk-syntax\n" + (help/code + "(POST \"/math/:x/:y\" []" + " :path-params [x :- s/Int, {y :- s/Int 1}]" + " (ok {:total (+ x y)}))"))) -; restructures path-params by plumbing letk notation. Example: -; :path-params [id :- Long name :- String] (defmethod restructure-param :path-params [_ path-params acc] - (let [schema (fnk-schema path-params)] + (let [schema (fnk-schema path-params) + g (gensym 'form-params-schema)] (-> acc - (update-in [:letks] into [path-params (src-coerce! schema :route-params :string)]) - (assoc-in [:swagger :parameters :path] schema)))) + (update :outer-lets into [g schema]) + (update-in [:letks] into [path-params (src-coerce! g :route-params :string)]) + (assoc-in [:info :public :parameters :path] g)))) + +;; +;; middleware +;; + +(defmethod help/help-for [:meta :middleware] [_ _] + (help/text + "Applies the given vector of middleware to the route." + "Middleware is presented as data in a Duct-style form:" + "" + "1) ring mw-function (handler->request->response)" + "" + "2) mw-function and it's arguments separately - mw is" + " created by applying function with handler and args\n" + (help/code + "(defn require-role [handler role]" + " (fn [request]" + " (if (has-role? request role)" + " (handler request)" + " (unauthorized))))" + "" + "(def require-admin #(require-role % :admin))" + "" + "(GET \"/admin\" []" + " :middleware [require-admin]" + " (ok))" + "" + "(GET \"/admin\" []" + " :middleware [[require-role :admin]]" + " (ok))" + "" + "(GET \"/admin\" []" + " :middleware [#(require-role % :admin)]" + " (ok))" + ))) -; Applies the given vector of middlewares to the route (defmethod restructure-param :middleware [_ middleware acc] (update-in acc [:middleware] into middleware)) -; Bind to stuff in request components using letk syntax +;; +;; components +;; + +(defmethod help/help-for [:meta :components] [_ _] + (help/text + "binds components into request via letk. Schema is not used here." + "to enable component injection into request, one should use either:" + "" + "1) `api`-options :components" + "2) `compojure.api.middleware/wrap-components" + "" + (help/code + "(defn app [{:keys [db] :as system}]" + " (api" + " {:components system}" + " (GET \"/ok\" []" + " :components [db]" + " (ok (do-something-with db)))))"))) + (defmethod restructure-param :components [_ components acc] (update-in acc [:letks] into [components `(mw/get-components ~+compojure-api-request+)])) -; route-specific override for coercers +;; +;; coercion +;; + +(defmethod help/help-for [:meta :coercion] [_ _] + (help/text + "Route-specific overrides for coercion. See more on wiki:" + "https://github.com/metosin/compojure-api/wiki/Validation-and-coercion\n" + (help/code + "(POST \"/user\" []" + " :coercion my-custom-coercion" + " :body [user User]" + " (ok user))"))) + (defmethod restructure-param :coercion [_ coercion acc] - (update-in acc [:middleware] conj [mw/wrap-coercion coercion])) + (let [g (gensym 'coercion)] + (-> acc + (update :outer-lets into [g coercion]) + (assoc-in [:info :coercion] g) + (update-in [:middleware] conj [`mw/wrap-coercion g])))) ;; ;; Impl ;; (defmacro dummy-let - "Dummy let-macro used in resolving route-docs. not part of normal invokation chain." + "Dummy let-macro used in resolving route-docs. not part of normal invocation chain." [bindings & body] (let [bind-form (vec (apply concat (for [n (take-nth 2 bindings)] [n nil])))] `(let ~bind-form ~@body))) (defmacro dummy-letk - "Dummy letk-macro used in resolving route-docs. not part of normal invokation chain." + "Dummy letk-macro used in resolving route-docs. not part of normal invocation chain." [bindings & body] (reduce (fn [cur-body-form [bind-form]] @@ -269,50 +678,401 @@ (RuntimeException. (str "unknown compojure destruction syntax: " arg)))))) -(defn merge-parameters - "Merge parameters at runtime to allow usage of runtime-paramers with route-macros." +(defn- merge-public-parameters [{:keys [responses swagger] :as parameters}] (cond-> parameters (seq responses) (assoc :responses (common/merge-vector responses)) swagger (-> (dissoc :swagger) (rsc/deep-merge swagger)))) -(defn restructure [method [path arg & args] {:keys [context?]}] +(defn merge-parameters + "Merge parameters at runtime to allow usage of runtime-paramers with route-macros." + [info] + (cond-> info + (contains? info :public) (update :public merge-public-parameters))) + +(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)) + '[compojure.api.core + compojure.api.sweet]))) + '[GET ANY HEAD PATCH DELETE OPTIONS POST PUT])) + +(def routes-vars #{'compojure.api.sweet/routes + 'compojure.api.core/routes}) + +(declare static-body? static-form?) + +(defn- static-endpoint? [&env form] + (and (seq? form) + (boolean + (let [sym (first form)] + (when (symbol? sym) + (when-some [v (resolve &env sym)] + (when (var? v) + (let [sym (var->sym v)] + (or (endpoint-vars sym) + (and (routes-vars sym) + (static-body? &env (next form)))))))))))) + +(def resource-vars '#{compojure.api.sweet/resource + compojure.api.resource/resource}) + +(defn- static-resource? [&env form] + (and (seq? form) + (boolean + (let [sym (first form)] + (when (symbol? sym) + (when-some [v (resolve &env sym)] + (when (var? v) + (let [sym (var->sym v)] + (when (and (resource-vars sym) + (= 2 (count form))) + (let [[_ data] form] + (static-form? &env data))))))))))) + +(def context-vars (into #{} + (mapcat (fn [n] + (map #(symbol (name %) (name n)) + '[compojure.api.core + compojure.api.sweet]))) + '[context])) + +(defn- static-context? [&env body] + (and (seq? body) + (boolean + (let [sym (first body)] + (when (symbol? sym) + (when-some [v (resolve &env sym)] + (when (var? v) + (context-vars (var->sym v))))))))) + +(def middleware-vars (into #{} + (mapcat (fn [n] + (map #(symbol (name %) (name n)) + '[compojure.api.core + compojure.api.sweet]))) + '[middleware])) + +(defn- static-middleware? [&env body] + (and (seq? body) + (boolean + (when-some [v (resolve-var &env (first body))] + (when (middleware-vars (var->sym v)) + (let [[_ mid & body] body] + (and (static-form? &env mid) + (static-body? &env body)))))))) + +(def route-middleware-vars (into #{} + (mapcat (fn [n] + (map #(symbol (name %) (name n)) + '[compojure.api.core + compojure.api.sweet]))) + '[route-middleware])) + +(def ^:private ^:dynamic *not-safely-static* nil) + +(defn- static-route-middleware? [&env body] + (and (seq? body) + (boolean + (let [sym (first body)] + (when (symbol? sym) + (when-some [v (resolve &env sym)] + (when (var? v) + (when (route-middleware-vars (var->sym v)) + (let [[_ mids & body] body] + (and (some? mids) + (static-body? &env body))))))))))) + +(defn- static-cond? [&env form] + (and (seq? form) + (boolean + (let [sym (first form)] + (when (symbol? sym) + (let [v (resolve &env sym)] + (when (or (= #'when v) + (= #'cond v) + (= #'= v) + (= #'not= v) + (= #'boolean v) + (= sym 'if)) + (static-body? &env (next form))))))))) + +(defn- static-resolved-form? [&env form] + (boolean + (or (and (seq? form) + (= 2 (count form)) + (= 'var (first form)) + (when-some [v (resolve-var nil (second form))] + (not (:dynamic (meta v))))) + (when (symbol? form) + (let [r (resolve &env form)] + (or (class? r) + (and (var? r) + (not (:dynamic (meta r)))))))))) + +(defn- static-expansion? [&env form] + (boolean + (when (and (seq? form) + (symbol? (first form)) + (not (contains? &env (first form)))) + (let [form' (macroexpand-1 form)] + (when-not (identical? form' form) + (static-form? &env form')))))) + +(defn- constant-form? [&env form] + (or ((some-fn nil? keyword? number? boolean? string?) form) + (and (seq? form) + (= 2 (count form)) + (= 'quote (first form))) + (and (vector? form) + (every? #(static-form? &env %) form)) + (and (map? form) + (every? #(static-form? &env %) form)) + (and (seq? form) + (next form) + (= 'fn* (first form))) + (and (seq? form) + ('#{clojure.spec.alpha/keys} + (some-> (resolve-var &env (first form)) + var->sym))) + (and (seq? form) + (symbol? (first form)) + (when-some [v (resolve-var &env (first form))] + (when (or (#{"spec-tools.data-spec" + "spec-tools.core" + "schema.core" + "ring.util.http-response"} + (namespace (var->sym v))) + ('#{compojure.api.sweet/describe + ring.swagger.json-schema/describe + clojure.core/constantly} + (var->sym v))) + (when-not (some #{:dynamic :macro} (meta v)) + (every? #(static-form? &env %) (next form)))))))) + +(defn- static-binder-env [&env bv] + (when (and (vector? bv) + (even? (count bv))) + (let [flat (eduction + (partition-all 2) + (mapcat (fn [[l init]] + (if (and (= :let l) + (even? count init)) + (partition-all 2 init) + [[l init]]))) + bv)] + (reduce (fn [&env [l init]] + (if-not (or (simple-symbol? l) + (simple-keyword? l) ;;for + (static-form? init)) + (reduced nil) + (cond-> &env + (simple-symbol? l) + (assoc l true)))) + (or &env {}) + flat)))) + +(defn- static-let? [&env form] + (and (seq? form) + (symbol? (first form)) + (when-some [op (or (when (= 'let* (first form)) + 'let*) + (when-some [v (resolve-var &env (first form))] + (let [sym (var->sym v)] + (when (contains? + '#{clojure.core/let clojure.core/for + compojure.api.sweet/let-routes compojure.api.core/let-routes} + sym) + sym))))] + (let [;; expand destructuring + [_ bv & body] (macroexpand + (if ('#{compojure.api.sweet/let-routes compojure.api.core/let-routes} op) + form + (list* `let (next form))))] + (when-some [&env (static-binder-env &env bv)] + (static-body? &env body)))))) + +(defn- static-vector? [&env body] + (and (vector? body) + (every? #(static-body? &env %) body))) + +(defn- static-form? [&env form] + (let [res (boolean + (or (contains? &env form) ;;local + (static-resolved-form? &env form) + (constant-form? &env form) + (static-endpoint? &env form) + (static-resource? &env form) + (static-let? &env form) + (static-cond? &env form) + (static-context? &env form) + (static-middleware? &env form) + (static-route-middleware? &env form) + (static-expansion? &env form)))] + (when-not res + (some-> *not-safely-static* (swap! conj {:form form :&env (into {} (map (fn [[k v]] + [k (if (boolean? v) v (class v))])) + &env)}))) + res)) + +(defn- static-body? [&env body] + (every? #(static-form? &env %) body)) + +(def ^:private warned-non-static? (atom false)) + +(defn restructure [method [path route-arg & args] {:keys [context? &form &env]}] (let [[options body] (extract-parameters args true) - [path-string lets arg-with-request arg] (destructure-compojure-api-request path arg) + [path-string lets arg-with-request] (destructure-compojure-api-request path route-arg) {:keys [lets letks + outer-lets responses middleware - middlewares + info swagger - parameters body]} (reduce (fn [acc [k v]] (restructure-param k v (update-in acc [:parameters] dissoc k))) {:lets lets :letks [] + :outer-lets [] ;; lets around the call to map->Route :responses nil :middleware [] - :swagger {} + :info {} :body body} options) - ;; migration helpers - _ (assert (not middlewares) ":middlewares is deprecated with 1.0.0, use :middleware instead.") - _ (assert (not parameters) ":parameters is deprecated with 1.0.0, use :swagger instead.") + coercion (:coercion info) + + _ (assert (not (and (-> info :public :dynamic) + (-> info :public :static))) + "Cannot be both a :dynamic and :static context.") + + ;; I think it's ok if we have :outer-lets + bindings? (boolean (or (route-args? route-arg) (seq lets) (seq letks))) + + _ (assert (not (and (-> info :public :static) + bindings?)) + "A context cannot be :static and also provide bindings. Either push bindings into endpoints or remove :static.") + + configured-dynamic? (or (-> info :public :dynamic) + (true? (get-in (meta *ns*) [:metosin/compojure-api :dynamic-contexts])) + (contains? + (some-> (System/getProperty "compojure.api.meta.dynamic-context-namespaces") + edn/read-string + set) + (ns-name *ns*))) + + configured-static? (or (-> info :public :static) + (when-not configured-dynamic? + (or (true? (get-in (meta *ns*) [:metosin/compojure-api :static-contexts])) + (contains? + (some-> (System/getProperty "compojure.api.meta.static-context-namespaces") + edn/read-string + set) + (ns-name *ns*))))) + + static? (or configured-static? + (and (not configured-dynamic?) + (not bindings?))) + + a (atom []) + safely-static? (boolean + (when context? + (when static? + (try (binding [*not-safely-static* a] + (static-body? &env body)) + (catch Exception e + (println `restructure-param "Internal error, please report the following trace to https://github.com/metosin/compojure-api") + (prn {:form &form :env &env}) + (prn e) + false))))) + + _ (when (and context? static?) + (when-not safely-static? + (when (and static? (not configured-static?)) + (let [coach (some-> (System/getProperty "compojure.api.meta.static-context-coach") + edn/read-string)] + (if-not coach + (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" + "To suppress this warning, set: -Dcompojure.api.meta.static-context-coach={:default :off}.\n" + "This warning will only print once, other namespaces may be affected."))) + (let [_ (assert (map? coach) + (str "-Dcompojure.api.meta.static-context-coach should be a map, given: " + (pr-str coach))) + nsym (ns-name *ns*) + mode (or (get coach nsym) + (get coach :default) + :print) + _ (when (:verbose coach) + (println "The following forms were not inferred static:") + (pp/pprint @a)) + msg (str "This looks like it could be a static context: " (pr-str {:form &form :meta (meta &form)}) + "\n\n" + "If you intend for the body of this context to be evaluated on every request, please " + "use (context ... :dynamic true ...)." + "\n\n" + "If you intend for the body of this context to be fixed for every request, please " + "use (context ... :static true ...)." + "\n\n" + "If you feel this case could be automatically inferred as :static, please suggest a " + "new inference rule at https://github.com/metosin/compojure-api. Use " + "-Dcompojure.api.meta.static-context-coach={:verbose true} to print additional information " + "and include it in the issue." + "\n\n" + "To suppress this message for this namespace use -Dcompojure.api.meta.static-context-coach=" + "{" nsym " " :off "}" + "\n\nCurrent coach config: " (pr-str coach))] + (case mode + :off nil + :print (println msg) + :assert (throw (ex-info msg + {:form &form + :meta (meta &form)})) + (throw (ex-info "compojure.api.meta.static-context-coach mode must be either :off, :print, or :assert" {:coach coach + :provided mode}))))))))) + + ;; :dynamic by default + static-context? (and static? context? safely-static?) + + info (cond-> info + static-context? (assoc :static-context? static-context?)) + + _ (assert (nil? swagger) ":swagger is deprecated with 2.0.0, use [:info :public] instead") ;; response coercion middleware, why not just code? - middleware (if (seq responses) (conj middleware `[coerce/body-coercer-middleware (common/merge-vector ~responses)]) middleware)] + middleware (if (seq responses) (conj middleware `[coercion/wrap-coerce-response (common/merge-vector ~responses)]) middleware)] (if context? ;; context - (let [form `(compojure.core/routes ~@body) + (let [form `(routing [~@body]) form (if (seq letks) `(p/letk ~letks ~form) form) form (if (seq lets) `(let ~lets ~form) form) + ;; coercion is set via middleware. for contexts, middleware is applied after let & letk -bindings + ;; to make coercion visible to the lets & letks, we apply it before any let & letk -bindings + form (if (and coercion (not static-context?)) + `(let [~+compojure-api-request+ (coercion/set-request-coercion ~+compojure-api-request+ ~coercion)] + ~form) + form) form (if (seq middleware) `((mw/compose-middleware ~middleware) ~form) form) - form `(compojure.core/context ~path ~arg-with-request ~form) + form (if static-context? + `(let [form# ~form] + (compojure.core/context ~path ~arg-with-request form#)) + `(compojure.core/context ~path ~arg-with-request ~form)) ;; create and apply a separate lookup-function to find the inner routes childs (let [form (vec body) @@ -320,16 +1080,27 @@ form (if (seq lets) `(dummy-let ~lets ~form) form) form `(compojure.core/let-request [~arg-with-request ~'+compojure-api-request+] ~form) form `(fn [~'+compojure-api-request+] ~form) - form `(~form {})] - form)] - - `(routes/create ~path-string ~method (merge-parameters ~swagger) ~childs ~form)) + form `(delay (flatten (~form {})))] + form) + form `(routes/map->Route + {:path ~path-string + :method ~method + :info (merge-parameters ~info) + :childs ~childs + :handler ~form}) + form (if (seq outer-lets) `(let ~outer-lets ~form) form)] + form) ;; endpoints (let [form `(do ~@body) form (if (seq letks) `(p/letk ~letks ~form) form) form (if (seq lets) `(let ~lets ~form) form) form (compojure.core/compile-route method path arg-with-request (list form)) - form (if (seq middleware) `(compojure.core/wrap-routes ~form (mw/compose-middleware ~middleware)) form)] - - `(routes/create ~path-string ~method (merge-parameters ~swagger) nil ~form))))) + form (if (seq middleware) `(compojure.core/wrap-routes ~form (mw/compose-middleware ~middleware)) form) + form `(routes/map->Route + {:path ~path-string + :method ~method + :info (merge-parameters ~info) + :handler ~form}) + form (if (seq outer-lets) `(let ~outer-lets ~form) form)] + form)))) diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj index 9d6c7705..3d6ba644 100644 --- a/src/compojure/api/middleware.clj +++ b/src/compojure/api/middleware.clj @@ -1,34 +1,48 @@ (ns compojure.api.middleware (:require [compojure.core :refer :all] [compojure.api.exception :as ex] + [compojure.api.common :as common] + [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.middleware.http-response [ring.middleware.keyword-params :refer [wrap-keyword-params]] [ring.middleware.nested-params :refer [wrap-nested-params]] [ring.middleware.params :refer [wrap-params]] - [ring.swagger.common :as rsc] - [ring.swagger.middleware :as rsm] [ring.swagger.coerce :as coerce] - [ring.util.http-response :refer :all] - [schema.core :as s]) - (:import [com.fasterxml.jackson.core JsonParseException] - [org.yaml.snakeyaml.parser ParserException] - [clojure.lang ArityException])) + + [muuntaja.middleware] + [muuntaja.core :as m] + + [ring.swagger.common :as rsc] + [ring.util.http-response :refer :all]) + (:import [clojure.lang ArityException] + [com.fasterxml.jackson.datatype.joda JodaModule])) ;; ;; Catch exceptions ;; -(def rethrow-exceptions? ::rethrow-exceptions?) - -(defn- call-error-handler [error-handler error data request] - (try - (error-handler error data request) - (catch ArityException _ - (logging/log! :warn "Error-handler arity has been changed.") - (error-handler error)))) +(defn- super-classes [^Class k] + (loop [sk (.getSuperclass k), ks []] + (if-not (= sk Object) + (recur (.getSuperclass sk) (conj ks sk)) + ks))) + +(defn- call-error-handler [default-handler handlers error request] + (let [{:keys [type] :as data} (ex-data error) + type (or (get ex/mapped-exception-types type) type) + ex-class (class error) + error-handler (or (get handlers type) + (get handlers ex-class) + (some + (partial get handlers) + (super-classes ex-class)) + default-handler)] + (try + (error-handler error (assoc data :type type) request) + (catch ArityException _ + (logging/log! :warn "Error-handler arity has been changed.") + (error-handler error))))) (defn wrap-exceptions "Catches all exceptions and delegates to correct error handler according to :type of Exceptions @@ -36,19 +50,24 @@ - **:compojure.api.exception/default** - Handler used when exception type doesn't match other handler, by default prints stack trace." [handler {:keys [handlers]}] - (let [default-handler (get handlers ::ex/default ex/safe-handler)] - (assert (fn? default-handler) "Default exception handler must be a function.") - (fn [request] - (try - (handler request) - (catch Throwable e - (let [{:keys [type] :as data} (ex-data e) - type (or (get ex/legacy-exception-types type) type) - handler (or (get handlers type) default-handler)] - ; FIXME: Used for validate - (if (rethrow-exceptions? request) - (throw e) - (call-error-handler handler e data request)))))))) + (let [default-handler (get handlers ::ex/default ex/safe-handler) + rethrow-or-respond (fn [e request respond raise] + ;; FIXME: Used for validate + (if (::rethrow-exceptions? request) + (raise e) + (respond (call-error-handler default-handler handlers e request))))] + (assert (ifn? default-handler) "Default exception handler must be a function.") + (fn + ([request] + (try + (handler request) + (catch Throwable e + (rethrow-or-respond e request identity #(throw %))))) + ([request respond raise] + (try + (handler request respond (fn [e] (rethrow-or-respond e request respond raise))) + (catch Throwable e + (rethrow-or-respond e request respond raise))))))) ;; ;; Component integration @@ -57,41 +76,52 @@ (defn wrap-components "Assoc given components to the request." [handler components] - (fn [req] - (handler (assoc req ::components components)))) + (fn + ([req] + (handler (assoc req ::components components))) + ([req respond raise] + (handler (assoc req ::components components) respond raise)))) (defn get-components [req] (::components req)) ;; -;; Ring-swagger options +;; Options ;; -(defn wrap-options - "Injects compojure-api options into the request." - [handler options] - (fn [request] - (handler (update-in request [::options] merge 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] + (fn + ([request] + (handler (common/fast-map-merge request data))) + ([request respond raise] + (handler (common/fast-map-merge request data) respond raise)))) + ;; ;; coercion ;; -(s/defschema CoercionType (s/enum :body :string :response)) +(defn wrap-coercion [handler coercion] + (fn + ([request] + (handler (coercion/set-request-coercion request coercion))) + ([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}) -(def no-response-coercion - (constantly (dissoc default-coercion-matchers :response))) - +;; 1.1.x (defn coercion-matchers [request] (let [options (get-options request)] (if (contains? options :coercion) @@ -99,68 +129,108 @@ (provider request)) default-coercion-matchers))) -(def coercion-request-ks [::options :coercion]) +;; +;; Muuntaja +;; -(defn wrap-coercion [handler coercion] - (fn [request] - (handler (assoc-in request coercion-request-ks coercion)))) +(defn encode? + "Returns true if the response body is serializable: body is a + collection or response has key :compojure.api.meta/serializable?" + [_ response] + (or (:compojure.api.meta/serializable? response) + (coll? (:body response)))) + +(def default-muuntaja-options + (assoc-in + m/default-options + [:formats "application/json" :opts :modules] + [(JodaModule.)])) + +(defn create-muuntaja + ([] + (create-muuntaja default-muuntaja-options)) + ([muuntaja-or-options] + (let [opts #(assoc-in % [:http :encode-response-body?] encode?)] + (cond + + (nil? muuntaja-or-options) + nil + + (= ::default muuntaja-or-options) + (m/create (opts default-muuntaja-options)) + + (m/muuntaja? muuntaja-or-options) + (-> muuntaja-or-options (m/options) (opts) (m/create)) + + (map? muuntaja-or-options) + (m/create (opts muuntaja-or-options)) + + :else + (throw + (ex-info + (str "Invalid :formats - " muuntaja-or-options) + {:options muuntaja-or-options})))))) + +;; +;; middleware +;; + +(defn middleware-fn [middleware] + (if (vector? middleware) + (let [[f & arguments] middleware] + #(apply f % arguments)) + middleware)) + +(defn compose-middleware [middleware] + (->> middleware + (keep identity) + (map middleware-fn) + (apply comp identity))) ;; -;; ring-middleware-format stuff +;; swagger-data ;; -(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 set-swagger-data + "Add extra top-level swagger-data into a request. + Data can be read with get-swagger-data." + ([request data] + (update request ::request/swagger (fnil conj []) data))) + +(defn get-swagger-data + "Reads and deep-merges top-level swagger-data from request, + pushed in by set-swagger-data." + [request] + (apply rsc/deep-merge (::request/swagger request))) + +(defn wrap-swagger-data + "Middleware that adds top level swagger-data into request." + [handler data] + (fn + ([request] + (handler (set-swagger-data request data))) + ([request respond raise] + (handler (set-swagger-data request data) respond raise)))) ;; ;; Api Middleware ;; (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 + {::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 ::ex/response-validation ex/response-validation-handler ::ex/default ex/safe-handler}} - :coercion (constantly default-coercion-matchers) + :middleware nil + :coercion coercion/default-coercion :ring-swagger nil}) +(defn api-middleware-options [options] + (rsc/deep-merge api-middleware-defaults options)) + ;; TODO: test all options! (https://github.com/metosin/compojure-api/issues/137) (defn api-middleware "Opinionated chain of middlewares for web apis. Takes optional options-map. @@ -186,76 +256,126 @@ ### Options + - **:formatter** either :ring-middleware-format or :muuntaja. + During 2.x pre-releases, this will be a required key, unless + :formats is provided, which is equivalent to setting to :muuntaja. + Stable 2.x releases will default to :ring-middleware-format if + not provided or :format is set, unless :formats is provided, + which is equivalent to setting to :muuntaja. + Stable 2.x will print a deprecation warning if implicitly + or explicitly set to :ring-middleware-format. + - **:exceptions** for *compojure.api.middleware/wrap-exceptions* (nil to unmount it) - **:handlers** Map of error handlers for different exception types, type refers to `:type` key in ExceptionInfo data. - - **:format** for ring-middleware-format middlewares (nil to unmount it) - - **: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}}` + - **: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. + + - **:middleware** vector of extra middleware to be applied last (just before the handler). - **:ring-swagger** options for ring-swagger's swagger-json method. e.g. `{:ignore-missing-mappings? true}` - **:coercion** A function from request->type->coercion-matcher, used - in endpoint coercion for :body, :string and :response. - Defaults to `(constantly compojure.api.middleware/default-coercion-matchers)` - Setting value to nil disables all coercion + in endpoint coercion for types :body, :string and :response. + Defaults to `compojure.api.middleware/default-coercion` + Setting value to nil disables all coercion. - **:components** Components which should be accessible to handlers using :components restructuring. (If you are using api, you might want to take look at using wrap-components middleware manually.). Defaults to nil (middleware not mounted)." - ([handler] (api-middleware handler nil)) + ([handler] + (throw (ex-info (str "ERROR: Please set `:formatter :muuntaja` in the options map of `api-middleware.\n" + "e.g., (api-middleware {:formatter :muuntaja})\n" + "To prepare for backwards compatibility with compojure-api 1.x, the formatting library must be \n" + "explicitly chosen if not configured by `:format` (ring-middleware-format) or\n" + "`:formats` (muuntaja). Once 2.x is stable, the default will be `:formatter :ring-middleware-format`.") + {})) + (api-middleware handler api-middleware-defaults)) ([handler options] - (let [options (rsc/deep-merge api-middleware-defaults options) - {:keys [exceptions format components]} options - {:keys [formats params-opts response-opts]} format] - ; 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'")) - (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 middleware-fn [middleware] - (if (vector? middleware) - (let [[f & arguments] middleware] - #(apply f % arguments)) - middleware)) + (when (and (::api-middleware-defaults options) + (not (:formatter options)) + (not (System/getProperty "compojure.api.middleware.global-default-formatter"))) + (throw (ex-info (str "ERROR: Please set `:formatter :muuntaja` in the options map of `api-middleware.\n" + "e.g., (api-middleware {:formatter :muuntaja})\n" + "To prepare for backwards compatibility with compojure-api 1.x, the formatting library must be\n" + "explicitly chosen if not configured by `:format` (ring-middleware-format) or\n" + ":formats (muuntaja). Once 2.x is stable, the default will be `:formatter :ring-middleware-format`.\n" + "To globally override the default formatter, use -Dcompojure.api.middleware.global-default-formatter=:muuntaja") + {}))) + (let [formatter (or (:formatter options) + (when (or (contains? options :formats) + (= (System/getProperty "compojure.api.middleware.global-default-formatter") + ":muuntaja")) + :muuntaja) + (throw (ex-info (str "ERROR: Please set `:formatter :muuntaja` in the options map of `api-middleware.\n" + "e.g., (api-middleware {:formatter :muuntaja})\n" + "To prepare for backwards compatibility with compojure-api 1.x, the formatting library must be\n" + "explicitly chosen if not configured by `:format` (ring-middleware-format) or\n" + ":formats (muuntaja). Once 2.x is stable, the default will be `:formatter :ring-middleware-format`.\n" + "To globally override the default formatter, use -Dcompojure.api.middleware.global-default-formatter=:muuntaja") + {})) + ;; TODO 2.x stable + :ring-middleware-format) + _ (assert (= :muuntaja formatter) + (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 + + - **: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." + ([handler] + (wrap-format handler {:formats ::default})) + ([handler options] + (let [options (rsc/deep-merge {:formats ::default} options) + muuntaja (create-muuntaja (:formats options))] -(defn compose-middleware [middleware] - (->> middleware - (keep identity) - (map middleware-fn) - (apply comp identity))) + (cond-> handler + muuntaja (-> (wrap-swagger-data {:consumes (m/decodes muuntaja) + :produces (m/encodes muuntaja)}) + (muuntaja.middleware/wrap-format-request muuntaja) + (muuntaja.middleware/wrap-format-response muuntaja) + (muuntaja.middleware/wrap-format-negotiate muuntaja)))))) diff --git a/src/compojure/api/resource.clj b/src/compojure/api/resource.clj index 76d5c67b..ec74a061 100644 --- a/src/compojure/api/resource.clj +++ b/src/compojure/api/resource.clj @@ -1,13 +1,16 @@ (ns compojure.api.resource (:require [compojure.api.routes :as routes] - [compojure.api.coerce :as coerce] + [compojure.api.coercion :as coercion] + [compojure.api.methods :as methods] [ring.swagger.common :as rsc] [schema.core :as s] [plumbing.core :as p] - [compojure.api.middleware :as mw])) + [compojure.api.async] + [compojure.api.middleware :as mw] + [compojure.api.coercion.core :as cc])) (def ^:private +mappings+ - {:methods #{:get :head :patch :delete :options :post :put} + {:methods methods/all-methods :parameters {:query-params [:query-params :query :string true] :body-params [:body-params :body :body false] :form-params [:form-params :formData :string true] @@ -25,51 +28,112 @@ (:parameters +mappings+)) (dissoc info :handler))) +(defn- inject-coercion [request info] + (if (contains? info :coercion) + (coercion/set-request-coercion request (:coercion info)) + request)) + (defn- coerce-request [request info ks] (reduce-kv (fn [request ring-key [compojure-key _ type open?]] - (if-let [schema (get-in info (concat ks [:parameters ring-key]))] - (let [schema (if open? (assoc schema s/Keyword s/Any) schema)] - (update request ring-key merge (coerce/coerce! schema compojure-key type request))) + (if-let [model (get-in info (concat ks [:parameters ring-key]))] + (let [coerced (coercion/coerce-request! + model compojure-key type (not= :body type) open? request)] + (if open? + (update request ring-key merge coerced) + (assoc request ring-key coerced))) request)) - request + (inject-coercion request info) (:parameters +mappings+))) (defn- coerce-response [response info request ks] - (coerce/coerce-response! request response (get-in info (concat ks [:responses])))) - -(defn- resolve-handler [info request-method] - (or - (get-in info [request-method :handler]) - (get-in info [:handler]))) + (coercion/coerce-response! request response (get-in info (concat ks [:responses])))) + +(defn- maybe-async [async? x] + (if (and async? x) [x true])) + +(defn- maybe-sync [x] + (if x [x false])) + +(defn- resolve-handler [info path-info route request-method async?] + (and + (or + ;; directly under a context + (= path-info "/") + ;; under an compojure endpoint + route + ;; vanilla ring + (nil? path-info)) + (let [[handler async] (or + (maybe-async async? (get-in info [request-method :async-handler])) + (maybe-sync (get-in info [request-method :handler])) + (maybe-async async? (get-in info [:async-handler])) + (maybe-sync (get-in info [:handler])))] + (if handler + [handler async])))) + +(defn- middleware-chain [info request-method handler] + (let [direct-mw (:middleware info) + method-mw (:middleware (get info request-method)) + middleware (mw/compose-middleware (concat direct-mw method-mw))] + (middleware handler))) (defn- create-childs [info] (map (fn [[method info]] - (routes/create "/" method (swaggerize info) nil nil)) + (routes/map->Route + {:path "/" + :method method + :info {:public (swaggerize info)}})) (select-keys info (:methods +mappings+)))) -(defn- create-handler [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- handle-sync [info {:keys [request-method path-info :compojure/route] :as request}] + (when-let [[raw-handler] (resolve-handler info path-info route request-method false)] + (let [ks (if (contains? info request-method) [request-method] []) + handler (middleware-chain info request-method raw-handler)] + (-> (coerce-request request info ks) + (handler) + (compojure.response/render request) + (coerce-response info request ks))))) + +(defn- handle-async [info {:keys [request-method path-info :compojure/route] :as request} respond raise] + (if-let [[raw-handler async?] (resolve-handler info path-info route request-method true)] + (let [ks (if (contains? info request-method) [request-method] []) + respond-coerced (fn [response] + (respond + (try (coerce-response response info request ks) + (catch Throwable e (raise e))))) + handler (middleware-chain info request-method raw-handler)] + (try + (as-> (coerce-request request info ks) $ + (if async? + (handler $ #(compojure.response/send % $ respond-coerced raise) raise) + (compojure.response/send (handler $) $ respond-coerced raise))) + (catch Throwable e + (raise e)))) + (respond nil))) + +(defn- create-handler [info] + (fn + ([request] + (handle-sync info request)) + ([request respond raise] + (handle-async info request respond raise)))) (defn- merge-parameters-and-responses [info] (let [methods (select-keys info (:methods +mappings+))] (-> info (merge - (p/for-map [[method method-info] methods] - method (-> method-info - (->> (rsc/deep-merge (select-keys info [:parameters]))) - (update :responses (fn [responses] (merge (:responses info) responses))))))))) - -(defn- root-info [info] + (p/for-map [[method method-info] methods + :let [responses (merge + (:responses info) + (:responses method-info))]] + method (cond-> (->> method-info (rsc/deep-merge (select-keys info [:parameters]))) + (seq responses) (assoc :responses responses))))))) + +(defn- public-root-info [info] (-> (reduce dissoc info (:methods +mappings+)) - (dissoc :parameters :responses))) + (dissoc :parameters :responses :coercion))) ;; ;; Public api @@ -81,17 +145,21 @@ ; TODO: validate input against ring-swagger schema, fail for missing handlers ; TODO: extract parameter schemas from handler fnks? (defn resource - "Creates a nested compojure-api Route from enchanced ring-swagger operations map and options. + "Creates a nested compojure-api Route from enchanced ring-swagger operations map. By default, applies both request- and response-coercion based on those definitions. - Options: + Extra keys: + + - **:middleware** Middleware in duct-format either at top-level or under methods. + Top-level mw are applied first if route matches, method-level + mw are applied next if method matches - **:coercion** A function from request->type->coercion-matcher, used in resource coercion for :body, :string and :response. Setting value to `(constantly nil)` disables both request- & response coercion. See tests and wiki for details. - Enchancements to ring-swagger operations map: + Enhancements to ring-swagger operations map: 1) :parameters use ring request keys (query-params, path-params, ...) instead of swagger-params (query, path, ...). This keeps things simple as ring keys are used in @@ -104,9 +172,14 @@ 2.2) :responses are merged into operation :responses (operation can fully override them) 2.3) all others (:produces, :consumes, :summary,...) are deep-merged by compojure-api - 3) special key `:handler` either under operations or at top-level. Value should be a - ring-handler function, responsible for the actual request processing. Handler lookup - order is the following: operations-level, top-level. + 3) special keys `:handler` and/or `:async-handler` either under operations or at top-level. + They should be 1-ary and 3-ary Ring handler functions, respectively, that are responsible + for the actual request processing. Handler lookup order is the following: + + 3.1) If called asynchronously, operations-level :async-handler + 3.2) Operations-level :handler + 3.3) If called asynchronously, top-level :async-handler + 3.4) Top-level :handler 4) request-coercion is applied once, using deep-merged parameters for a given operation or resource-level if only resource-level handler is defined. @@ -130,11 +203,13 @@ :post {} :handler (constantly (internal-server-error {:reason \"not implemented\"}))})" - ([info] - (resource info {})) - ([info options] - (let [info (merge-parameters-and-responses info) - root-info (swaggerize (root-info info)) - childs (create-childs info) - handler (create-handler info options)] - (routes/create nil nil root-info childs 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 e4312523..a468d326 100644 --- a/src/compojure/api/routes.clj +++ b/src/compojure/api/routes.clj @@ -1,16 +1,20 @@ (ns compojure.api.routes (:require [compojure.core :refer :all] [clojure.string :as string] - [cheshire.core :as json] - [compojure.api.middleware :as mw] + [compojure.api.methods :as methods] + [compojure.api.request :as request] [compojure.api.impl.logging :as logging] + [compojure.api.impl.json :as json] [compojure.api.common :as common] + [muuntaja.core :as m] [ring.swagger.common :as rsc] [clojure.string :as str] [linked.core :as linked] [compojure.response] - [schema.core :as s]) - (:import [clojure.lang AFn IFn Var])) + [schema.core :as s] + [compojure.api.coercion :as coercion]) + (:import (clojure.lang AFn IFn Var IDeref) + (java.io Writer))) ;; ;; Route records @@ -47,37 +51,61 @@ (update-in route [0] (fn [uri] (if (str/blank? uri) "/" uri)))) (-get-routes handler options)))) +(defn get-static-context-routes + ([handler] + (get-static-context-routes handler nil)) + ([handler options] + (filter (fn [[_ _ info]] (get info :static-context?)) + (get-routes handler options)))) + +(defn- realize-childs [route] + (update route :childs #(if (instance? IDeref %) @% %))) + +(defn- filter-childs [route] + (update route :childs (partial filter (partial satisfies? Routing)))) + (defrecord Route [path method info childs handler] Routing (-get-routes [this options] - (let [valid-childs (filter-routes this options)] - (if (seq childs) + (let [this (-> this realize-childs) + valid-childs (filter-routes this options) + make-method-path-fn (fn [m] [path m info])] + (if (-> this filter-childs :childs seq) (vec (for [[p m i] (mapcat #(-get-routes % options) valid-childs)] [(->paths path p) m (rsc/deep-merge info i)])) - (into [] (if path [[path method info]]))))) + (into [] (cond + (and path method) [(make-method-path-fn method)] + path (mapv make-method-path-fn methods/all-methods)))))) compojure.response/Renderable - (render [_ {:keys [uri request-method]}] - (throw - (ex-info - (str "\ncompojure.api.routes/Route can't be returned from endpoint " - (-> request-method name str/upper-case) " \"" uri "\". " - "For nested routes, use `context` instead: (context \"path\" [] ...)\n") - {:request-method request-method - :path path - :method method - :uri uri}))) + (render [_ request] + (handler request)) + + ;; Sendable implementation in compojure.api.async IFn (invoke [_ request] (handler request)) + (invoke [_ request respond raise] + (handler request respond raise)) + (applyTo [this args] (AFn/applyToHelper this args))) (defn create [path method info childs handler] (->Route path method info childs handler)) +(defmethod print-method Route + [this ^Writer w] + (let [childs (some-> this realize-childs filter-childs :childs seq vec)] + (.write w (str "#Route" + (cond-> (dissoc this :handler :childs) + (not (:path this)) (dissoc :path) + (not (seq (:info this))) (dissoc :info) + (not (:method this)) (dissoc :method) + childs (assoc :childs childs)))))) + ;; ;; Invalid route handlers ;; @@ -117,11 +145,16 @@ {:paths (reduce (fn [acc [path method info]] - (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)}) @@ -133,8 +166,19 @@ (for [[id freq] (frequencies seq) :when (> freq 1)] id)) +(defn all-paths [routes] + (reduce + (fn [acc [path method info]] + (let [public-info (get info :public {})] + (update-in acc [path method] + (fn [old-info] + (let [public-info (or old-info public-info)] + (ensure-path-parameters path public-info)))))) + (linked/map) + routes)) + (defn route-lookup-table [routes] - (let [entries (for [[path endpoints] (-> routes ring-swagger-paths :paths) + (let [entries (for [[path endpoints] (all-paths routes) [method {:keys [x-name parameters]}] endpoints :let [params (:path parameters)] :when x-name] @@ -155,12 +199,6 @@ ;; Endpoint Trasformers ;; -(defn strip-no-doc-endpoints - "Endpoint transformer, strips all endpoints that have :x-no-doc true." - [endpoint] - (if-not (some-> endpoint :x-no-doc true?) - endpoint)) - (defn non-nil-routes [endpoint] (or endpoint {})) @@ -171,7 +209,7 @@ (defn- un-quote [s] (str/replace s #"^\"(.+(?=\"$))\"$" "$1")) -(defn- path-string [s params] +(defn- path-string [m s params] (-> s (str/replace #":([^/]+)" " :$1 ") (str/split #" ") @@ -181,7 +219,7 @@ (let [key (keyword (subs token 1)) value (key params)] (if value - (un-quote (json/generate-string value)) + (un-quote (slurp (m/encode m "application/json" value))) (throw (IllegalArgumentException. (str "Missing path-parameter " key " for path " s))))) @@ -191,14 +229,14 @@ (defn path-for* "Extracts the lookup-table from request and finds a route by name." [route-name request & [params]] - (let [[path details] (some-> request - mw/get-options - :lookup + (let [m (or (::request/muuntaja request) json/muuntaja) + [path details] (some-> request + ::request/lookup route-name first) path-params (:params details)] (if (seq path-params) - (path-string path params) + (path-string m path params) path))) (defmacro path-for diff --git a/src/compojure/api/swagger.clj b/src/compojure/api/swagger.clj index 18055863..a2f160da 100644 --- a/src/compojure/api/swagger.clj +++ b/src/compojure/api/swagger.clj @@ -1,14 +1,15 @@ (ns compojure.api.swagger (:require [compojure.api.core :as c] - [compojure.api.common :as common] [compojure.api.middleware :as mw] + [compojure.api.request :as request] [ring.util.http-response :refer [ok]] [ring.swagger.common :as rsc] [ring.swagger.middleware :as rsm] [ring.swagger.core :as swagger] [ring.swagger.swagger-ui :as swagger-ui] [ring.swagger.swagger2 :as swagger2] - [compojure.api.routes :as routes])) + [compojure.api.routes :as routes] + [spec-tools.swagger.core])) (defn base-path [request] (let [context (swagger/context request)] @@ -24,9 +25,7 @@ first)) (defn transform-operations [swagger] - (->> swagger - (swagger2/transform-operations routes/non-nil-routes) - (swagger2/transform-operations routes/strip-no-doc-endpoints))) + (swagger2/transform-operations routes/non-nil-routes swagger)) (defn swagger-ui [options] (assert (map? options) "Since 1.1.11, compojure.api.swagger/swagger-ui takes just one map as argument, with `:path` for the path.") @@ -39,12 +38,13 @@ (c/GET path request :no-doc true :name ::swagger - (let [runtime-info (rsm/get-swagger-data request) + (let [runtime-info1 (mw/get-swagger-data request) + runtime-info2 (rsm/get-swagger-data request) base-path {:basePath (base-path request)} - options (:ring-swagger (mw/get-options request)) - paths (:paths (mw/get-options request)) - swagger (apply rsc/deep-merge (keep identity [base-path paths extra-info runtime-info])) - spec (swagger2/swagger-json swagger options)] + options (::request/ring-swagger request) + paths (::request/paths request) + swagger (apply rsc/deep-merge (keep identity [base-path paths extra-info runtime-info1 runtime-info2])) + spec (spec-tools.swagger.core/swagger-spec (swagger2/swagger-json swagger options))] (ok spec))))) ;; @@ -56,15 +56,21 @@ (defn swagger-routes "Returns routes for swagger-articats (ui & spec). Accepts an options map, with the following options: + **:ui** Path for the swagger-ui (defaults to \"/\"). Setting the value to nil will cause the swagger-ui not to be mounted + **:spec** Path for the swagger-spec (defaults to \"/swagger.json\") Setting the value to nil will cause the swagger-ui not to be mounted + **:data** Swagger data in the Ring-Swagger format. + **:options** **:ui** Options to configure the ui **:spec** Options to configure the spec. Nada at the moment. + Example options: + {:ui \"/api-docs\" :spec \"/swagger.json\" :options {:ui {:jsonEditor true} diff --git a/src/compojure/api/sweet.clj b/src/compojure/api/sweet.clj index 1a84d637..737a45c3 100644 --- a/src/compojure/api/sweet.clj +++ b/src/compojure/api/sweet.clj @@ -4,8 +4,8 @@ (defmacro defroutes {:doc "Define a Ring handler function from a sequence of routes.\n The name may optionally be followed by a doc-string and metadata map."} [name & routes] (list* (quote compojure.api.core/defroutes) name routes)) (defmacro let-routes {:doc "Takes a vector of bindings and a body of routes.\n\n Equivalent to: `(let [...] (routes ...))`"} [bindings & body] (list* (quote compojure.api.core/let-routes) bindings body)) (def ^{:arglists (quote ([& handlers])), :doc "Routes without route-documentation. Can be used to wrap routes,\n not satisfying compojure.api.routes/Routing -protocol."} undocumented compojure.api.core/undocumented) -(defmacro middleware {:deprecated "1.1.14", :doc "Wraps routes with given middleware using thread-first macro.\n\n Note that middlewares will be executed even if routes in body\n do not match the request uri. Be careful with middleware that\n has side-effects."} [middleware & body] (list* (quote compojure.api.core/middleware) middleware body)) -(def ^{:arglists (quote ([middleware & body])), :doc "Wraps routes with given middleware using thread-first macro."} route-middleware compojure.api.core/route-middleware) +(defmacro middleware {:deprecated "1.1.14", :doc "Wraps routes with given middlewares using thread-first macro.\n\n Note that middlewares will be executed even if routes in body\n do not match the request uri. Be careful with middleware that\n has side-effects."} [middleware & body] (list* (quote compojure.api.core/middleware) middleware body)) +(def ^{:arglists (quote ([middleware & body])), :doc "Wraps routes with given middlewares using thread-first macro."} route-middleware compojure.api.core/route-middleware) (defmacro context [& args] (list* (quote compojure.api.core/context) args)) (defmacro GET [& args] (list* (quote compojure.api.core/GET) args)) (defmacro ANY [& args] (list* (quote compojure.api.core/ANY) args)) @@ -15,9 +15,9 @@ (defmacro OPTIONS [& args] (list* (quote compojure.api.core/OPTIONS) args)) (defmacro POST [& args] (list* (quote compojure.api.core/POST) args)) (defmacro PUT [& args] (list* (quote compojure.api.core/PUT) args)) -(def ^{:arglists (quote ([& body])), :doc "Returns a ring handler wrapped in compojure.api.middleware/api-middlware.\n Creates the route-table at api creation time and injects that into the request via\n middlewares. Api and the mounted api-middleware can be configured by optional\n options map as the first parameter:\n\n (api\n {:formats [:json-kw :edn :transit-msgpack :transit-json]\n :exceptions {:handlers {:compojure.api.exception/default my-logging-handler}}\n :api {:invalid-routes-fn (constantly nil)}\n :swagger {:spec \"/swagger.json\"\n :ui \"/api-docs\"\n :data {:info {:version \"1.0.0\"\n :title \"My API\"\n :description \"the description\"}}}}\n (context \"/api\" []\n ...))\n\n ### direct api options:\n\n - **:api** All api options are under `:api`.\n - **:invalid-routes-fn** A 2-arity function taking handler and a sequence of\n invalid routes (not satisfying compojure.api.route.Routing)\n setting value to nil ignores invalid routes completely.\n defaults to `compojure.api.routes/log-invalid-child-routes`\n - **:disable-api-middleware?** boolean to disable the `api-middleware` from api.\n - **:swagger** Options to configure the Swagger-routes. Defaults to nil.\n See `compojure.api.swagger/swagger-routes` for details.\n\n ### api-middleware options\n\n Opinionated chain of middlewares for web apis. Takes optional options-map.\n\n ### Exception handlers\n\n An error handler is a function of exception, ex-data and request to response.\n\n When defining these options, it is suggested to use alias for the exceptions namespace,\n e.g. `[compojure.api.exception :as ex]`.\n\n Default:\n\n {::ex/request-validation ex/request-validation-handler\n ::ex/request-parsing ex/request-parsing-handler\n ::ex/response-validation ex/response-validation-handler\n ::ex/default ex/safe-handler}\n\n Note: Because the handlers are merged into default handlers map, to disable default handler you\n need to provide `nil` value as handler.\n\n Note: To catch Schema errors use `{:schema.core/error ex/schema-error-handler}`.\n\n ### Options\n\n - **:exceptions** for *compojure.api.middleware/wrap-exceptions* (nil to unmount it)\n - **:handlers** Map of error handlers for different exception types, type refers to `:type` key in ExceptionInfo data.\n\n - **:format** for ring-middleware-format middlewares (nil to unmount it)\n - **:formats** sequence of supported formats, e.g. `[:json-kw :edn]`\n - **:params-opts** for *ring.middleware.format-params/wrap-restful-params*,\n e.g. `{:transit-json {:handlers readers}}`\n - **:response-opts** for *ring.middleware.format-params/wrap-restful-response*,\n e.g. `{:transit-json {:handlers writers}}`\n\n - **:ring-swagger** options for ring-swagger's swagger-json method.\n e.g. `{:ignore-missing-mappings? true}`\n\n - **:coercion** A function from request->type->coercion-matcher, used\n in endpoint coercion for :body, :string and :response.\n Defaults to `(constantly compojure.api.middleware/default-coercion-matchers)`\n Setting value to nil disables all coercion\n\n - **:components** Components which should be accessible to handlers using\n :components restructuring. (If you are using api,\n you might want to take look at using wrap-components\n middleware manually.). Defaults to nil (middleware not mounted)."} api compojure.api.api/api) -(defmacro defapi {:doc "Defines an api.\n\n API middleware options:\n\n Opinionated chain of middlewares for web apis. Takes optional options-map.\n\n ### Exception handlers\n\n An error handler is a function of exception, ex-data and request to response.\n\n When defining these options, it is suggested to use alias for the exceptions namespace,\n e.g. `[compojure.api.exception :as ex]`.\n\n Default:\n\n {::ex/request-validation ex/request-validation-handler\n ::ex/request-parsing ex/request-parsing-handler\n ::ex/response-validation ex/response-validation-handler\n ::ex/default ex/safe-handler}\n\n Note: Because the handlers are merged into default handlers map, to disable default handler you\n need to provide `nil` value as handler.\n\n Note: To catch Schema errors use `{:schema.core/error ex/schema-error-handler}`.\n\n ### Options\n\n - **:exceptions** for *compojure.api.middleware/wrap-exceptions* (nil to unmount it)\n - **:handlers** Map of error handlers for different exception types, type refers to `:type` key in ExceptionInfo data.\n\n - **:format** for ring-middleware-format middlewares (nil to unmount it)\n - **:formats** sequence of supported formats, e.g. `[:json-kw :edn]`\n - **:params-opts** for *ring.middleware.format-params/wrap-restful-params*,\n e.g. `{:transit-json {:handlers readers}}`\n - **:response-opts** for *ring.middleware.format-params/wrap-restful-response*,\n e.g. `{:transit-json {:handlers writers}}`\n\n - **:ring-swagger** options for ring-swagger's swagger-json method.\n e.g. `{:ignore-missing-mappings? true}`\n\n - **:coercion** A function from request->type->coercion-matcher, used\n in endpoint coercion for :body, :string and :response.\n Defaults to `(constantly compojure.api.middleware/default-coercion-matchers)`\n Setting value to nil disables all coercion\n\n - **:components** Components which should be accessible to handlers using\n :components restructuring. (If you are using api,\n you might want to take look at using wrap-components\n middleware manually.). Defaults to nil (middleware not mounted)."} [name & body] (list* (quote compojure.api.api/defapi) name body)) -(def ^{:arglists (quote ([info] [info options])), :doc "Creates a nested compojure-api Route from enchanced ring-swagger operations map and options.\n By default, applies both request- and response-coercion based on those definitions.\n\n Options:\n\n - **:coercion** A function from request->type->coercion-matcher, used\n in resource coercion for :body, :string and :response.\n Setting value to `(constantly nil)` disables both request- &\n response coercion. See tests and wiki for details.\n\n Enchancements to ring-swagger operations map:\n\n 1) :parameters use ring request keys (query-params, path-params, ...) instead of\n swagger-params (query, path, ...). This keeps things simple as ring keys are used in\n the handler when destructuring the request.\n\n 2) at resource root, one can add any ring-swagger operation definitions, which will be\n available for all operations, using the following rules:\n\n 2.1) :parameters are deep-merged into operation :parameters\n 2.2) :responses are merged into operation :responses (operation can fully override them)\n 2.3) all others (:produces, :consumes, :summary,...) are deep-merged by compojure-api\n\n 3) special key `:handler` either under operations or at top-level. Value should be a\n ring-handler function, responsible for the actual request processing. Handler lookup\n order is the following: operations-level, top-level.\n\n 4) request-coercion is applied once, using deep-merged parameters for a given\n operation or resource-level if only resource-level handler is defined.\n\n 5) response-coercion is applied once, using merged responses for a given\n operation or resource-level if only resource-level handler is defined.\n\n Note: Swagger operations are generated only from declared operations (:get, :post, ..),\n despite the top-level handler could process more operations.\n\n Example:\n\n (resource\n {:parameters {:query-params {:x Long}}\n :responses {500 {:schema {:reason s/Str}}}\n :get {:parameters {:query-params {:y Long}}\n :responses {200 {:schema {:total Long}}}\n :handler (fn [request]\n (ok {:total (+ (-> request :query-params :x)\n (-> request :query-params :y))}))}\n :post {}\n :handler (constantly\n (internal-server-error {:reason \"not implemented\"}))})"} resource compojure.api.resource/resource) +(def ^{:arglists (quote ([& body])), :doc "Returns a ring handler wrapped in compojure.api.middleware/api-middlware.\n Creates the route-table at api creation time and injects that into the request via\n middlewares. Api and the mounted api-middleware can be configured by optional\n options map as the first parameter:\n\n (api\n {:exceptions {:handlers {:compojure.api.exception/default my-logging-handler}}\n :api {:invalid-routes-fn (constantly nil)}\n :swagger {:spec \"/swagger.json\"\n :ui \"/api-docs\"\n :data {:info {:version \"1.0.0\"\n :title \"My API\"\n :description \"the description\"}}}}\n (context \"/api\" []\n ...))\n\n ### direct api options:\n\n - **:api** All api options are under `:api`.\n - **:invalid-routes-fn** A 2-arity function taking handler and a sequence of\n invalid routes (not satisfying compojure.api.route.Routing)\n setting value to nil ignores invalid routes completely.\n defaults to `compojure.api.routes/log-invalid-child-routes`\n - **:disable-api-middleware?** boolean to disable the `api-middleware` from api.\n - **:swagger** Options to configure the Swagger-routes. Defaults to nil.\n See `compojure.api.swagger/swagger-routes` for details.\n\n ### api-middleware options\n\n See `compojure.api.middleware/api-middleware` for more available options.\n\n Opinionated chain of middlewares for web apis. Takes optional options-map.\n\n ### Exception handlers\n\n An error handler is a function of exception, ex-data and request to response.\n\n When defining these options, it is suggested to use alias for the exceptions namespace,\n e.g. `[compojure.api.exception :as ex]`.\n\n Default:\n\n {::ex/request-validation ex/request-validation-handler\n ::ex/request-parsing ex/request-parsing-handler\n ::ex/response-validation ex/response-validation-handler\n ::ex/default ex/safe-handler}\n\n Note: Because the handlers are merged into default handlers map, to disable default handler you\n need to provide `nil` value as handler.\n\n Note: To catch Schema errors use `{:schema.core/error ex/schema-error-handler}`.\n\n ### Options\n\n - **:exceptions** for *compojure.api.middleware/wrap-exceptions* (nil to unmount it)\n - **:handlers** Map of error handlers for different exception types, type refers to `:type` key in ExceptionInfo data.\n\n - **:formats** for Muuntaja middleware. Value can be a valid muuntaja options-map,\n a Muuntaja instance or nil (to unmount it). See\n https://github.com/metosin/muuntaja/blob/master/doc/Configuration.md for details.\n\n - **:middleware** vector of extra middleware to be applied last (just before the handler).\n\n - **:ring-swagger** options for ring-swagger's swagger-json method.\n e.g. `{:ignore-missing-mappings? true}`\n\n - **:coercion** A function from request->type->coercion-matcher, used\n in endpoint coercion for types :body, :string and :response.\n Defaults to `compojure.api.middleware/default-coercion`\n Setting value to nil disables all coercion.\n\n - **:components** Components which should be accessible to handlers using\n :components restructuring. (If you are using api,\n you might want to take look at using wrap-components\n middleware manually.). Defaults to nil (middleware not mounted)."} api compojure.api.api/api) +(defmacro defapi {:deprecated "2.0.0", :doc "Deprecated: please use (def name (api ...body..))\n \n Defines an api.\n\n API middleware options:\n\n Opinionated chain of middlewares for web apis. Takes optional options-map.\n\n ### Exception handlers\n\n An error handler is a function of exception, ex-data and request to response.\n\n When defining these options, it is suggested to use alias for the exceptions namespace,\n e.g. `[compojure.api.exception :as ex]`.\n\n Default:\n\n {::ex/request-validation ex/request-validation-handler\n ::ex/request-parsing ex/request-parsing-handler\n ::ex/response-validation ex/response-validation-handler\n ::ex/default ex/safe-handler}\n\n Note: Because the handlers are merged into default handlers map, to disable default handler you\n need to provide `nil` value as handler.\n\n Note: To catch Schema errors use `{:schema.core/error ex/schema-error-handler}`.\n\n ### Options\n\n - **:exceptions** for *compojure.api.middleware/wrap-exceptions* (nil to unmount it)\n - **:handlers** Map of error handlers for different exception types, type refers to `:type` key in ExceptionInfo data.\n\n - **:formats** for Muuntaja middleware. Value can be a valid muuntaja options-map,\n a Muuntaja instance or nil (to unmount it). See\n https://github.com/metosin/muuntaja/blob/master/doc/Configuration.md for details.\n\n - **:middleware** vector of extra middleware to be applied last (just before the handler).\n\n - **:ring-swagger** options for ring-swagger's swagger-json method.\n e.g. `{:ignore-missing-mappings? true}`\n\n - **:coercion** A function from request->type->coercion-matcher, used\n in endpoint coercion for types :body, :string and :response.\n Defaults to `compojure.api.middleware/default-coercion`\n Setting value to nil disables all coercion.\n\n - **:components** Components which should be accessible to handlers using\n :components restructuring. (If you are using api,\n you might want to take look at using wrap-components\n middleware manually.). Defaults to nil (middleware not mounted)."} [name & body] (list* (quote compojure.api.api/defapi) name body)) +(def ^{:arglists (quote ([data])), :doc "Creates a nested compojure-api Route from enchanced ring-swagger operations map.\n By default, applies both request- and response-coercion based on those definitions.\n\n Extra keys:\n\n - **:middleware** Middleware in duct-format either at top-level or under methods.\n Top-level mw are applied first if route matches, method-level\n mw are applied next if method matches\n\n - **:coercion** A function from request->type->coercion-matcher, used\n in resource coercion for :body, :string and :response.\n Setting value to `(constantly nil)` disables both request- &\n response coercion. See tests and wiki for details.\n\n Enchancements to ring-swagger operations map:\n\n 1) :parameters use ring request keys (query-params, path-params, ...) instead of\n swagger-params (query, path, ...). This keeps things simple as ring keys are used in\n the handler when destructuring the request.\n\n 2) at resource root, one can add any ring-swagger operation definitions, which will be\n available for all operations, using the following rules:\n\n 2.1) :parameters are deep-merged into operation :parameters\n 2.2) :responses are merged into operation :responses (operation can fully override them)\n 2.3) all others (:produces, :consumes, :summary,...) are deep-merged by compojure-api\n\n 3) special keys `:handler` and/or `:async-handler` either under operations or at top-level.\n They should be 1-ary and 3-ary Ring handler functions, respectively, that are responsible\n for the actual request processing. Handler lookup order is the following:\n\n 3.1) If called asynchronously, operations-level :async-handler\n 3.2) Operations-level :handler\n 3.3) If called asynchronously, top-level :async-handler\n 3.4) Top-level :handler\n\n 4) request-coercion is applied once, using deep-merged parameters for a given\n operation or resource-level if only resource-level handler is defined.\n\n 5) response-coercion is applied once, using merged responses for a given\n operation or resource-level if only resource-level handler is defined.\n\n Note: Swagger operations are generated only from declared operations (:get, :post, ..),\n despite the top-level handler could process more operations.\n\n Example:\n\n (resource\n {:parameters {:query-params {:x Long}}\n :responses {500 {:schema {:reason s/Str}}}\n :get {:parameters {:query-params {:y Long}}\n :responses {200 {:schema {:total Long}}}\n :handler (fn [request]\n (ok {:total (+ (-> request :query-params :x)\n (-> request :query-params :y))}))}\n :post {}\n :handler (constantly\n (internal-server-error {:reason \"not implemented\"}))})"} resource compojure.api.resource/resource) (defmacro path-for {:doc "Extracts the lookup-table from request and finds a route by name."} [route-name & arg2] (list* (quote compojure.api.routes/path-for) route-name arg2)) -(def ^{:arglists (quote ([] [options])), :doc "Returns routes for swagger-articats (ui & spec). Accepts an options map, with the\n following options:\n **:ui** Path for the swagger-ui (defaults to \"/\").\n Setting the value to nil will cause the swagger-ui not to be mounted\n **:spec** Path for the swagger-spec (defaults to \"/swagger.json\")\n Setting the value to nil will cause the swagger-ui not to be mounted\n **:data** Swagger data in the Ring-Swagger format.\n **:options**\n **:ui** Options to configure the ui\n **:spec** Options to configure the spec. Nada at the moment.\n Example options:\n {:ui \"/api-docs\"\n :spec \"/swagger.json\"\n :options {:ui {:jsonEditor true}\n :spec {}}\n :data {:basePath \"/app\"\n :info {:version \"1.0.0\"\n :title \"Sausages\"\n :description \"Sausage description\"\n :termsOfService \"http://helloreverb.com/terms/\"\n :contact {:name \"My API Team\"\n :email \"foo@example.com\"\n :url \"http://www.metosin.fi\"}\n :license {:name: \"Eclipse Public License\"\n :url: \"http://www.eclipse.org/legal/epl-v10.html\"}}\n :tags [{:name \"sausages\", :description \"Sausage api-set\"}]}}"} swagger-routes compojure.api.swagger/swagger-routes) +(def ^{:arglists (quote ([] [options])), :doc "Returns routes for swagger-articats (ui & spec). Accepts an options map, with the\n following options:\n\n **:ui** Path for the swagger-ui (defaults to \"/\").\n Setting the value to nil will cause the swagger-ui not to be mounted\n\n **:spec** Path for the swagger-spec (defaults to \"/swagger.json\")\n Setting the value to nil will cause the swagger-ui not to be mounted\n\n **:data** Swagger data in the Ring-Swagger format.\n\n **:options**\n **:ui** Options to configure the ui\n **:spec** Options to configure the spec. Nada at the moment.\n\n Example options:\n\n {:ui \"/api-docs\"\n :spec \"/swagger.json\"\n :options {:ui {:jsonEditor true}\n :spec {}}\n :data {:basePath \"/app\"\n :info {:version \"1.0.0\"\n :title \"Sausages\"\n :description \"Sausage description\"\n :termsOfService \"http://helloreverb.com/terms/\"\n :contact {:name \"My API Team\"\n :email \"foo@example.com\"\n :url \"http://www.metosin.fi\"}\n :license {:name: \"Eclipse Public License\"\n :url: \"http://www.eclipse.org/legal/epl-v10.html\"}}\n :tags [{:name \"sausages\", :description \"Sausage api-set\"}]}}"} swagger-routes compojure.api.swagger/swagger-routes) (def ^{:arglists (quote ([schema desc & kvs])), :doc "Attach description and possibly other meta-data to a schema."} describe ring.swagger.json-schema/describe) diff --git a/src/compojure/api/upload.clj b/src/compojure/api/upload.clj index 0a59e5e1..61ca42a0 100644 --- a/src/compojure/api/upload.clj +++ b/src/compojure/api/upload.clj @@ -1,5 +1,5 @@ ;; NOTE: This namespace is generated by compojure.api.dev.gen (ns compojure.api.upload (:require ring.middleware.multipart-params ring.swagger.upload)) -(def ^{:arglists (quote ([handler] [handler options])), :doc "Middleware to parse multipart parameters from a request. Adds the\n following keys to the request map:\n\n :multipart-params - a map of multipart parameters\n :params - a merged map of all types of parameter\n\n The following options are accepted\n\n :encoding - character encoding to use for multipart parsing.\n Overrides the encoding specified in the request. If not\n specified, uses the encoding specified in a part named\n \"_charset_\", or the content type for each part, or\n request character encoding if the part has no encoding,\n or \"UTF-8\" if no request character encoding is set.\n\n :fallback-encoding - specifies the character encoding used in parsing if a\n part of the request does not specify encoding in its\n content type or no part named \"_charset_\" is present.\n Has no effect if :encoding is also set.\n\n :store - a function that stores a file upload. The function\n should expect a map with :filename, content-type and\n :stream keys, and its return value will be used as the\n value for the parameter in the multipart parameter map.\n The default storage function is the temp-file-store.\n\n :progress-fn - a function that gets called during uploads. The\n function should expect four parameters: request,\n bytes-read, content-length, and item-count."} wrap-multipart-params ring.middleware.multipart-params/wrap-multipart-params) +(def ^{:arglists (quote ([handler] [handler options])), :doc "Middleware to parse multipart parameters from a request. Adds the\n following keys to the request map:\n\n :multipart-params - a map of multipart parameters\n :params - a merged map of all types of parameter\n\n The following options are accepted\n\n :encoding - character encoding to use for multipart parsing.\n Overrides the encoding specified in the request. If not\n specified, uses the encoding specified in a part named\n \"_charset_\", or the content type for each part, or\n request character encoding if the part has no encoding,\n or \"UTF-8\" if no request character encoding is set.\n\n :fallback-encoding - specifies the character encoding used in parsing if a\n part of the request does not specify encoding in its\n content type or no part named \"_charset_\" is present.\n Has no effect if :encoding is also set.\n\n :store - a function that stores a file upload. The function\n should expect a map with :filename, :content-type and\n :stream keys, and its return value will be used as the\n value for the parameter in the multipart parameter map.\n The default storage function is the temp-file-store.\n\n :progress-fn - a function that gets called during uploads. The\n function should expect four parameters: request,\n bytes-read, content-length, and item-count."} wrap-multipart-params ring.middleware.multipart-params/wrap-multipart-params) (def ^{:doc "Schema for file param created by ring.middleware.multipart-params.temp-file store."} TempFileUpload ring.swagger.upload/TempFileUpload) (def ^{:doc "Schema for file param created by ring.middleware.multipart-params.byte-array store."} ByteArrayUpload ring.swagger.upload/ByteArrayUpload)