diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 946cce48..4665c66a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: test: strategy: matrix: - jdk: [8, 11, 17, 21] + jdk: [8, 11, 17, 21, 22] name: Java ${{ matrix.jdk }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e93d4be7..bfd87589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ See also: [compojure-api 1.1.x changelog](./CHANGELOG-1.1.x.md) +## Next +* Lazily load spec and schema coercion +* bump spec-tools to 0.10.6 + * notable changes: swagger `:name` defaults to `"body"` instead of `""` ([diff](https://github.com/metosin/spec-tools/compare/0.10.2...0.10.3)) + ## 2.0.0-alpha34-SNAPSHOT * **BREAKING CHANGE**: `:formatter :muuntaja` sometimes required for `api{-middleware}` options * to prepare for 1.x compatibility, :muuntaja must be explicitly configured diff --git a/project.clj b/project.clj index a207642b..e08a7716 100644 --- a/project.clj +++ b/project.clj @@ -12,11 +12,7 @@ [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"] [ring/ring-core "1.8.0"] [compojure "1.6.1" ] - [org.clojure/core.memoize "0.8.2"] - [clj-commons/clj-yaml "0.7.0"] - [org.yaml/snakeyaml "1.24"] - [ring-middleware-format "0.7.4"] - [metosin/spec-tools "0.10.0"] + [metosin/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"] @@ -41,7 +37,6 @@ [org.clojure/core.async "0.6.532"] [javax.servlet/javax.servlet-api "4.0.1"] [peridot "0.5.2"] - [com.rpl/specter "1.1.3"] [com.stuartsierra/component "0.4.0"] [expound "0.8.2"] [metosin/jsonista "0.2.5"] @@ -65,6 +60,9 @@ [org.slf4j/jul-to-slf4j "1.7.30"] [org.slf4j/log4j-over-slf4j "1.7.30"] [ch.qos.logback/logback-classic "1.2.3" ]]} + :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]} + :1.11 {:dependencies [[org.clojure/clojure "1.11.3"]]} + :1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha11"]]} :async {:jvm-opts ["-Dcompojure-api.test.async=true"] :dependencies [[manifold "0.1.8" :exclusions [org.clojure/tools.logging]]]}} :eastwood {:namespaces [:source-paths] @@ -90,7 +88,7 @@ ["change" "version" "leiningen.release/bump-version"] ["vcs" "commit"] ["vcs" "push"]] - :aliases {"all" ["with-profile" "dev:dev,async"] + :aliases {"all" ["with-profile" "dev:dev,async:dev,1.10:dev,1.11:dev,1.12"] "start-thingie" ["run"] "aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"] "test-ancient" ["test"] diff --git a/src/compojure/api/coerce.clj b/src/compojure/api/coerce.clj new file mode 100644 index 00000000..5a147a14 --- /dev/null +++ b/src/compojure/api/coerce.clj @@ -0,0 +1,67 @@ +;; 1.1.x +(ns compojure.api.coerce + (:require [schema.coerce :as sc] + [compojure.api.middleware :as mw] + [compojure.api.exception :as ex] + [clojure.walk :as walk] + [schema.utils :as su] + [linked.core :as linked])) + +(defn memoized-coercer + "Returns a memoized version of a referentially transparent coercer fn. The + memoized version of the function keeps a cache of the mapping from arguments + to results and, when calls with the same arguments are repeated often, has + higher performance at the expense of higher memory use. FIFO with 10000 entries. + Cache will be filled if anonymous coercers are used (does not match the cache)" + [] + (let [cache (atom (linked/map)) + cache-size 10000] + (fn [& args] + (or (@cache args) + (let [coercer (apply sc/coercer args)] + (swap! cache (fn [mem] + (let [mem (assoc mem args coercer)] + (if (>= (count mem) cache-size) + (dissoc mem (-> mem first first)) + mem)))) + coercer))))) + +(defn cached-coercer [request] + (or (-> request mw/get-options :coercer) sc/coercer)) + +(defn coerce-response! [request {:keys [status] :as response} responses] + (-> (when-let [schema (or (:schema (get responses status)) + (:schema (get responses :default)))] + (when-let [matchers (mw/coercion-matchers request)] + (when-let [matcher (matchers :response)] + (let [coercer (cached-coercer request) + coerce (coercer schema matcher) + body (coerce (:body response))] + (if (su/error? body) + (throw (ex-info + (str "Response validation failed: " (su/error-val body)) + (assoc body :type ::ex/response-validation + :response response))) + (assoc response + :compojure.api.meta/serializable? true + :body body)))))) + (or response))) + +(defn body-coercer-middleware [handler responses] + (fn [request] + (coerce-response! request (handler request) responses))) + +(defn coerce! [schema key type request] + (let [value (walk/keywordize-keys (key request))] + (if-let [matchers (mw/coercion-matchers request)] + (if-let [matcher (matchers type)] + (let [coercer (cached-coercer request) + coerce (coercer schema matcher) + result (coerce value)] + (if (su/error? result) + (throw (ex-info + (str "Request validation failed: " (su/error-val result)) + (assoc result :type ::ex/request-validation))) + result)) + value) + value))) diff --git a/src/compojure/api/coercion.clj b/src/compojure/api/coercion.clj index a83a7082..0dd26b04 100644 --- a/src/compojure/api/coercion.clj +++ b/src/compojure/api/coercion.clj @@ -3,8 +3,9 @@ [compojure.api.exception :as ex] [compojure.api.request :as request] [compojure.api.coercion.core :as cc] - [compojure.api.coercion.schema] - [compojure.api.coercion.spec]) + ;; side effects + compojure.api.coercion.register-schema + compojure.api.coercion.register-spec) (:import (compojure.api.coercion.core CoercionError))) (def default-coercion :schema) diff --git a/src/compojure/api/coercion/register_schema.clj b/src/compojure/api/coercion/register_schema.clj new file mode 100644 index 00000000..e1e8f993 --- /dev/null +++ b/src/compojure/api/coercion/register_schema.clj @@ -0,0 +1,8 @@ +(ns compojure.api.coercion.register-schema + (:require [compojure.api.coercion.core :as cc])) + +(defmethod cc/named-coercion :schema [_] + (deref + (or (resolve 'compojure.api.coercion.schema/default-coercion) + (do (require 'compojure.api.coercion.schema) + (resolve 'compojure.api.coercion.schema/default-coercion))))) diff --git a/src/compojure/api/coercion/register_spec.clj b/src/compojure/api/coercion/register_spec.clj new file mode 100644 index 00000000..143320fb --- /dev/null +++ b/src/compojure/api/coercion/register_spec.clj @@ -0,0 +1,8 @@ +(ns compojure.api.coercion.register-spec + (:require [compojure.api.coercion.core :as cc])) + +(defmethod cc/named-coercion :spec [_] + (deref + (or (resolve 'compojure.api.coercion.spec/default-coercion) + (do (require 'compojure.api.coercion.spec) + (resolve 'compojure.api.coercion.spec/default-coercion))))) diff --git a/src/compojure/api/coercion/schema.clj b/src/compojure/api/coercion/schema.clj index b308d0c2..9a7e01b0 100644 --- a/src/compojure/api/coercion/schema.clj +++ b/src/compojure/api/coercion/schema.clj @@ -5,7 +5,9 @@ [compojure.api.coercion.core :as cc] [clojure.walk :as walk] [schema.core :as s] - [compojure.api.common :as common]) + [compojure.api.common :as common] + ;; side effects + compojure.api.coercion.register-schema) (:import (java.io File) (schema.core OptionalKey RequiredKey) (schema.utils ValidationError NamedError))) @@ -84,5 +86,3 @@ (->SchemaCoercion :schema options)) (def default-coercion (create-coercion default-options)) - -(defmethod cc/named-coercion :schema [_] default-coercion) diff --git a/src/compojure/api/coercion/spec.clj b/src/compojure/api/coercion/spec.clj index 9b20481a..b5d6ad31 100644 --- a/src/compojure/api/coercion/spec.clj +++ b/src/compojure/api/coercion/spec.clj @@ -6,7 +6,9 @@ [clojure.walk :as walk] [compojure.api.coercion.core :as cc] [spec-tools.swagger.core :as swagger] - [compojure.api.common :as common]) + [compojure.api.common :as common] + ;; side effects + compojure.api.coercion.register-spec) (:import (clojure.lang IPersistentMap) (schema.core RequiredKey OptionalKey) (spec_tools.core Spec) @@ -149,5 +151,3 @@ (->SpecCoercion :spec options)) (def default-coercion (create-coercion default-options)) - -(defmethod cc/named-coercion :spec [_] default-coercion) diff --git a/src/compojure/api/middleware.clj b/src/compojure/api/middleware.clj index c3edda43..3e1d9024 100644 --- a/src/compojure/api/middleware.clj +++ b/src/compojure/api/middleware.clj @@ -13,6 +13,7 @@ [ring.middleware.keyword-params :refer [wrap-keyword-params]] [ring.middleware.nested-params :refer [wrap-nested-params]] [ring.middleware.params :refer [wrap-params]] + [ring.swagger.coerce :as coerce] [muuntaja.middleware] [muuntaja.core :as m] @@ -103,6 +104,12 @@ ;; Options ;; +;; 1.1.x +(defn get-options + "Extracts compojure-api options from the request." + [request] + (::options request)) + (defn wrap-inject-data "Injects data into the request." [handler data] @@ -123,6 +130,20 @@ ([request respond raise] (handler (coercion/set-request-coercion request coercion) respond raise)))) +;; 1.1.x +(def default-coercion-matchers + {:body coerce/json-schema-coercion-matcher + :string coerce/query-schema-coercion-matcher + :response coerce/json-schema-coercion-matcher}) + +;; 1.1.x +(defn coercion-matchers [request] + (let [options (get-options request)] + (if (contains? options :coercion) + (if-let [provider (:coercion options)] + (provider request)) + default-coercion-matchers))) + ;; ;; Muuntaja ;; diff --git a/test19/compojure/api/coercion/spec_coercion_test.clj b/test19/compojure/api/coercion/spec_coercion_test.clj index 53412eac..b19c0762 100644 --- a/test19/compojure/api/coercion/spec_coercion_test.clj +++ b/test19/compojure/api/coercion/spec_coercion_test.clj @@ -393,7 +393,7 @@ :responses {:default {:description ""}}}} "/body-map" {:post {:parameters [{:description "" :in "body" - :name "" + :name "body" :required true :schema {:properties {:x {:format "int64" :type "integer"} @@ -404,7 +404,7 @@ :responses {:default {:description ""}}}} "/body-params" {:post {:parameters [{:description "" :in "body" - :name "" + :name "body" :required true :schema {:properties {:x {:format "int64" :type "integer"} @@ -415,7 +415,7 @@ :responses {:default {:description ""}}}} "/body-string" {:post {:parameters [{:description "" :in "body" - :name "" + :name "body" :required true :schema {:type "string"}}] :responses {:default {:description ""}}}} @@ -476,7 +476,7 @@ :default {:description ""}}} :post {:parameters [{:description "" :in "body" - :name "" + :name "body" :required true :schema {:properties {:x {:format "int64" :type "integer"}