|
13 | 13 | [schema.core :as s] |
14 | 14 | [schema.coerce :as sc] |
15 | 15 | [schema.utils :as su] |
16 | | - [schema-tools.core :as st])) |
| 16 | + [schema-tools.core :as st] |
| 17 | + [linked.core :as linked] |
| 18 | + [compojure.api.impl.logging :as logging])) |
17 | 19 |
|
18 | 20 | ;; |
19 | 21 | ;; Meta Evil |
|
27 | 29 | "lexically bound meta-data for handlers." |
28 | 30 | '+compojure-api-meta+) |
29 | 31 |
|
| 32 | +(def +compojure-api-coercer+ |
| 33 | + "lexically bound (caching) coercer for handlers." |
| 34 | + '+compojure-api-coercer+) |
| 35 | + |
30 | 36 | (defmacro meta-container [meta & form] |
31 | 37 | `(let [accumulated-meta# (get-local +compojure-api-meta+) |
32 | 38 | ~'+compojure-api-meta+ (deep-merge accumulated-meta# ~meta)] |
|
47 | 53 | ;; Schema |
48 | 54 | ;; |
49 | 55 |
|
50 | | -(def memoized-coercer (memoize sc/coercer)) |
| 56 | +(defn memoized-coercer |
| 57 | + "Returns a memoized version of a referentially transparent coercer fn. The |
| 58 | + memoized version of the function keeps a cache of the mapping from arguments |
| 59 | + to results and, when calls with the same arguments are repeated often, has |
| 60 | + higher performance at the expense of higher memory use. FIFO with 100 entries. |
| 61 | + If the cached if filled, there will be a WARNING logged at least once -> root cause |
| 62 | + is most propably that the route is using anonymous coercer, which doesn't hit |
| 63 | + the cache. It will produce slower performance, but works otherwise as expected." |
| 64 | + [name] |
| 65 | + (let [cache (atom {:mem (linked/map), :overflow false}) |
| 66 | + cache-size 100] |
| 67 | + (fn [& args] |
| 68 | + (or (-> @cache :mem (get args)) |
| 69 | + (let [coercer (apply sc/coercer args)] |
| 70 | + (swap! cache (fn [cache] |
| 71 | + (let [mem (assoc (:mem cache) args coercer)] |
| 72 | + (if (>= (count mem) cache-size) |
| 73 | + (do |
| 74 | + (when-not (:overflow cache) |
| 75 | + ;; side-effecting within a swap! might cause multiple writes. |
| 76 | + ;; it's ok'ish as we are just reporting something that should be |
| 77 | + ;; fixes at development time |
| 78 | + (logging/log! :warning (str "Coercion memoization cache for " name |
| 79 | + " maxing at " cache-size ". " |
| 80 | + "You might recreate the coercer " |
| 81 | + "matcher on each request, causing " |
| 82 | + "coercer re-compilation per request, " |
| 83 | + "effecting coercion performance."))) |
| 84 | + {:mem (dissoc mem (-> mem first first)) |
| 85 | + :overflow true}) |
| 86 | + (assoc cache :mem mem))))) |
| 87 | + coercer))))) |
51 | 88 |
|
52 | 89 | (defn strict [schema] |
53 | 90 | (dissoc schema 'schema.core/Keyword)) |
|
57 | 94 | (fnk-impl/letk-input-schema-and-body-form |
58 | 95 | nil (with-meta bind {:schema s/Any}) [] nil))) |
59 | 96 |
|
60 | | -(defn body-coercer-middleware [handler responses] |
| 97 | +(defn body-coercer-middleware [handler coercer responses] |
61 | 98 | (fn [request] |
62 | 99 | (if-let [{:keys [status] :as response} (handler request)] |
63 | 100 | (if-let [schema (:schema (responses status))] |
64 | 101 | (if-let [matcher (:response (mw/get-coercion-matcher-provider request))] |
65 | | - (let [coerce (memoized-coercer (value-of schema) matcher) |
| 102 | + (let [coerce (coercer (value-of schema) matcher) |
66 | 103 | body (coerce (:body response))] |
67 | 104 | (if (su/error? body) |
68 | 105 | (throw+ (assoc body :type ::ex/response-validation)) |
|
79 | 116 | (assert (not (#{:query :json} type)) (str type " is DEPRECATED since 0.22.0. Use :body or :string instead.")) |
80 | 117 | `(let [value# (keywordize-keys (~key ~+compojure-api-request+))] |
81 | 118 | (if-let [matcher# (~type (mw/get-coercion-matcher-provider ~+compojure-api-request+))] |
82 | | - (let [coerce# (memoized-coercer ~schema matcher#) |
| 119 | + (let [coerce# (~+compojure-api-coercer+ ~schema matcher#) |
83 | 120 | result# (coerce# value#)] |
84 | 121 | (if (su/error? result#) |
85 | 122 | (throw+ (assoc result# :type ::ex/request-validation)) |
|
115 | 152 | ;; |
116 | 153 |
|
117 | 154 | (defmulti restructure-param |
118 | | - "Restructures a key value pair in smart routes. By default the key |
119 | | - is consumed form the :parameters map in acc. k = given key, v = value." |
120 | | - (fn [k v acc] k)) |
| 155 | + "Restructures a key value pair in smart routes. By default the key |
| 156 | + is consumed form the :parameters map in acc. k = given key, v = value." |
| 157 | + (fn [k v acc] k)) |
121 | 158 |
|
122 | 159 | ;; |
123 | 160 | ;; Pass-through swagger metadata |
|
321 | 358 | (defn restructure [method [path arg & args] & [{:keys [body-wrap]}]] |
322 | 359 | (let [body-wrap (or body-wrap 'do) |
323 | 360 | method-symbol (symbol (str (-> method meta :ns) "/" (-> method meta :name))) |
| 361 | + coercer-name (str (keyword (.toLowerCase (name method-symbol))) " " path) |
324 | 362 | [parameters body] (extract-parameters args) |
325 | 363 | [lets letks responses middlewares] [[] [] nil nil] |
326 | 364 | [lets arg-with-request arg] (destructure-compojure-api-request lets arg) |
|
343 | 381 | body (if (seq middlewares) `(route-middlewares ~middlewares ~body ~arg) body) |
344 | 382 | body (if (seq parameters) `(meta-container ~parameters ~body) body) |
345 | 383 | body `(~method-symbol ~path ~arg-with-request ~body) |
346 | | - body (if responses `(body-coercer-middleware ~body ~responses) body)] |
| 384 | + body (if responses `(body-coercer-middleware ~body ~+compojure-api-coercer+ ~responses) body) |
| 385 | + body `(let [~+compojure-api-coercer+ (memoized-coercer ~coercer-name)] ~body)] |
347 | 386 | body)) |
0 commit comments