Skip to content

Commit 0c48047

Browse files
committed
Merge branch '1.1.x' of github.com:metosin/compojure-api into 1.1.x
2 parents 3f92625 + a8cf4d6 commit 0c48047

19 files changed

+630
-20
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
test:
1515
strategy:
1616
matrix:
17-
jdk: [8, 11, 17, 21]
17+
jdk: [8, 11, 17, 21, 22]
1818

1919
name: Java ${{ matrix.jdk }}
2020

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## NEXT
22

3+
## NEXT
4+
* drop support for Clojure 1.8
5+
* upgrade cheshire 5.13.0
6+
* Backport: use muuntaja in compojure.api.validator
7+
38
## 1.1.14 (2024-04-30)
49
* Remove potemkin [#445](https://github.com/metosin/compojure-api/issues/445)
510
* backport `route-middleware`

project.clj

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
:scm {:name "git"
99
:url "https://github.com/metosin/compojure-api"}
1010
:dependencies [[prismatic/plumbing "0.6.0"]
11-
[cheshire "5.9.0"]
11+
[cheshire "5.13.0"]
1212
[compojure "1.6.1"]
1313
[prismatic/schema "1.1.12"]
1414
[org.tobereplaced/lettercase "1.0.0"]
@@ -20,7 +20,7 @@
2020
:profiles {:uberjar {:aot :all
2121
:ring {:handler examples.thingie/app}
2222
:source-paths ["examples/thingie/src"]
23-
:dependencies [[org.clojure/clojure "1.8.0"]
23+
:dependencies [[org.clojure/clojure "1.9.0"]
2424
[http-kit "2.3.0"]
2525
[reloaded.repl "0.2.4"]
2626
[com.stuartsierra/component "0.4.0"]]}
@@ -30,7 +30,13 @@
3030
[lein-midje "3.2.1"]
3131
[lein-ring "0.12.0"]
3232
[funcool/codeina "0.5.0"]]
33-
:dependencies [[org.clojure/clojure "1.8.0"]
33+
:dependencies [[org.clojure/clojure "1.9.0"]
34+
;; bump
35+
[fipp "0.6.26"]
36+
[metosin/spec-tools "0.10.6"]
37+
[metosin/muuntaja "0.6.6"]
38+
[metosin/jsonista "0.2.5"]
39+
[com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.1"]
3440
[slingshot "0.12.2"]
3541
[peridot "0.5.1"]
3642
[javax.servlet/servlet-api "2.5"]
@@ -47,9 +53,9 @@
4753
"-Xmx4096m"
4854
"-Dclojure.compiler.direct-linking=true"]}
4955
:logging {:dependencies [[org.clojure/tools.logging "0.5.0"]]}
50-
:1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
51-
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
52-
:1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}}
56+
:1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}
57+
:1.11 {:dependencies [[org.clojure/clojure "1.11.3"]]}
58+
:1.12 {:dependencies [[org.clojure/clojure "1.12.0-alpha11"]]}}
5359
:eastwood {:namespaces [:source-paths]
5460
:add-linters [:unused-namespaces]}
5561
:codeina {:sources ["src"]
@@ -73,10 +79,10 @@
7379
["change" "version" "leiningen.release/bump-version"]
7480
["vcs" "commit"]
7581
["vcs" "push"]]
76-
:aliases {"all" ["with-profile" "dev:dev,logging:dev,1.10"]
82+
:aliases {"all" ["with-profile" "dev:dev,logging:dev,1.10:dev,1.11:dev,1.12"]
7783
"start-thingie" ["run"]
7884
"aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"]
7985
"test-ancient" ["midje"]
8086
"perf" ["with-profile" "default,dev,perf"]
8187
"deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."}
82-
["do" ["clean"] ["midje"] ["deploy" "clojars"]]})
88+
["do" ["clean"] ["midje"] ["deploy" "clojars"]]})

src/compojure/api/async.clj

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
(ns compojure.api.async
2+
(:require [compojure.response :as response]
3+
[compojure.api.common :as common]
4+
compojure.api.routes))
5+
6+
(common/when-ns 'manifold.deferred
7+
;; Compojure is smart enough to get the success value out of deferred by
8+
;; itself, but we want to catch the exceptions as well.
9+
(extend-protocol compojure.response/Sendable
10+
manifold.deferred.IDeferred
11+
(send* [deferred request respond raise]
12+
(manifold.deferred/on-realized deferred #(response/send % request respond raise) raise))))
13+
14+
(common/when-ns 'clojure.core.async
15+
(extend-protocol compojure.response/Sendable
16+
clojure.core.async.impl.channels.ManyToManyChannel
17+
(send* [channel request respond raise]
18+
(clojure.core.async/go
19+
(let [message (clojure.core.async/<! channel)]
20+
(if (instance? Throwable message)
21+
(raise message)
22+
(response/send message request respond raise)))))))
23+
24+
(extend-protocol compojure.response/Sendable
25+
compojure.api.routes.Route
26+
(send* [this request respond raise]
27+
((.handler this) request #(response/send % request respond raise) raise)))

src/compojure/api/coercion.clj

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
(ns compojure.api.coercion
2+
(:require [clojure.walk :as walk]
3+
[compojure.api.exception :as ex]
4+
[compojure.api.request :as request]
5+
[compojure.api.coercion.core :as cc]
6+
;; side effects
7+
compojure.api.coercion.register-schema
8+
compojure.api.coercion.register-spec)
9+
(:import (compojure.api.coercion.core CoercionError)))
10+
11+
(def default-coercion :schema)
12+
13+
(defn set-request-coercion [request coercion]
14+
(assoc request ::request/coercion coercion))
15+
16+
(defn get-request-coercion [request]
17+
(if-let [entry (find request ::request/coercion)]
18+
(val entry)
19+
default-coercion))
20+
21+
(defn resolve-coercion [coercion]
22+
(cond
23+
(nil? coercion) nil
24+
(keyword? coercion) (cc/named-coercion coercion)
25+
(satisfies? cc/Coercion coercion) coercion
26+
:else (throw (ex-info (str "invalid coercion " coercion) {:coercion coercion}))))
27+
28+
(defn get-apidocs [maybe-coercion spec info]
29+
(if-let [coercion (resolve-coercion maybe-coercion)]
30+
(cc/get-apidocs coercion spec info)))
31+
32+
(defn coerce-request! [model in type keywordize? open? request]
33+
(let [transform (if keywordize? walk/keywordize-keys identity)
34+
value (transform (in request))]
35+
(if-let [coercion (-> request
36+
(get-request-coercion)
37+
(resolve-coercion))]
38+
(let [model (if open? (cc/make-open coercion model) model)
39+
format (some-> request :muuntaja/request :format)
40+
result (cc/coerce-request coercion model value type format request)]
41+
(if (instance? CoercionError result)
42+
(throw (ex-info
43+
(str "Request validation failed: " (pr-str result))
44+
(merge
45+
(into {} result)
46+
{:type ::ex/request-validation
47+
:coercion coercion
48+
:value value
49+
:in [:request in]
50+
:request request})))
51+
result))
52+
value)))
53+
54+
(defn coerce-response! [request {:keys [status body] :as response} responses]
55+
(if-let [model (or (:schema (get responses status))
56+
(:schema (get responses :default)))]
57+
(if-let [coercion (-> request
58+
(get-request-coercion)
59+
(resolve-coercion))]
60+
(let [format (or (-> response :muuntaja/content-type)
61+
(some-> request :muuntaja/response :format))
62+
accept? (cc/accept-response? coercion model)]
63+
(if accept?
64+
(let [result (cc/coerce-response coercion model body :response format response)]
65+
(if (instance? CoercionError result)
66+
(throw (ex-info
67+
(str "Response validation failed: " (pr-str result))
68+
(merge
69+
(into {} result)
70+
{:type ::ex/response-validation
71+
:coercion coercion
72+
:value body
73+
:in [:response :body]
74+
:request request
75+
:response response})))
76+
(assoc response
77+
:compojure.api.meta/serializable? true
78+
:body result)))
79+
response))
80+
response)
81+
response))
82+
83+
;;
84+
;; middleware
85+
;;
86+
87+
(defn wrap-coerce-response [handler responses]
88+
(fn
89+
([request]
90+
(coerce-response! request (handler request) responses))
91+
([request respond raise]
92+
(handler
93+
request
94+
(fn [response]
95+
(try
96+
(respond (coerce-response! request response responses))
97+
(catch Exception e
98+
(raise e))))
99+
raise))))

src/compojure/api/coercion/core.clj

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(ns compojure.api.coercion.core)
2+
3+
(defprotocol Coercion
4+
(get-name [this])
5+
(get-apidocs [this model data])
6+
(make-open [this model])
7+
(encode-error [this error])
8+
(coerce-request [this model value type format request])
9+
(accept-response? [this model])
10+
(coerce-response [this model value type format request]))
11+
12+
(defrecord CoercionError [])
13+
14+
(defmulti named-coercion identity :default ::default)
15+
16+
(defmethod named-coercion ::default [x]
17+
(let [message (if (= :spec x)
18+
(str "spec-coercion is not enabled. "
19+
"you most likely are missing the "
20+
"required deps: org.clojure/clojure 1.9+ "
21+
"and metosin/spec-tools.")
22+
(str "cant find named-coercion for " x))]
23+
(throw (ex-info message {:name x}))))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
(ns compojure.api.coercion.register-schema
2+
(:require [compojure.api.coercion.core :as cc]))
3+
4+
(defmethod cc/named-coercion :schema [_]
5+
(deref
6+
(or (resolve 'compojure.api.coercion.schema/default-coercion)
7+
(do (require 'compojure.api.coercion.schema)
8+
(resolve 'compojure.api.coercion.schema/default-coercion)))))
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
(ns compojure.api.coercion.register-spec
2+
(:require [compojure.api.coercion.core :as cc]))
3+
4+
(defmethod cc/named-coercion :spec [_]
5+
(deref
6+
(or (resolve 'compojure.api.coercion.spec/default-coercion)
7+
(do (require 'compojure.api.coercion.spec)
8+
(resolve 'compojure.api.coercion.spec/default-coercion)))))

src/compojure/api/coercion/schema.clj

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
(ns compojure.api.coercion.schema
2+
(:require [schema.coerce :as sc]
3+
[schema.utils :as su]
4+
[ring.swagger.coerce :as coerce]
5+
[compojure.api.coercion.core :as cc]
6+
[clojure.walk :as walk]
7+
[schema.core :as s]
8+
[compojure.api.common :as common]
9+
;; side effects
10+
compojure.api.coercion.register-schema)
11+
(:import (java.io File)
12+
(schema.core OptionalKey RequiredKey)
13+
(schema.utils ValidationError NamedError)))
14+
15+
(def string-coercion-matcher coerce/query-schema-coercion-matcher)
16+
(def json-coercion-matcher coerce/json-schema-coercion-matcher)
17+
18+
(defn stringify
19+
"Stringifies Schema records recursively."
20+
[error]
21+
(walk/prewalk
22+
(fn [x]
23+
(cond
24+
(class? x) (.getName ^Class x)
25+
(instance? OptionalKey x) (pr-str (list 'opt (:k x)))
26+
(instance? RequiredKey x) (pr-str (list 'req (:k x)))
27+
(and (satisfies? s/Schema x) (record? x)) (try (pr-str (s/explain x)) (catch Exception _ x))
28+
(instance? ValidationError x) (str (su/validation-error-explain x))
29+
(instance? NamedError x) (str (su/named-error-explain x))
30+
:else x))
31+
error))
32+
33+
(def memoized-coercer
34+
(common/fifo-memoize sc/coercer 1000))
35+
36+
;; don't use coercion for certain types
37+
(defmulti coerce-response? identity :default ::default)
38+
(defmethod coerce-response? ::default [_] true)
39+
(defmethod coerce-response? File [_] false)
40+
41+
(defrecord SchemaCoercion [name options]
42+
cc/Coercion
43+
(get-name [_] name)
44+
45+
(get-apidocs [_ _ data] data)
46+
47+
(make-open [_ schema]
48+
(if (map? schema)
49+
(assoc schema s/Keyword s/Any)
50+
schema))
51+
52+
(encode-error [_ error]
53+
(-> error
54+
(update :schema pr-str)
55+
(update :errors stringify)))
56+
57+
(coerce-request [_ schema value type format request]
58+
(let [type-options (options type)]
59+
(if-let [matcher (or (get (get type-options :formats) format)
60+
(get type-options :default))]
61+
(let [coerce (memoized-coercer schema matcher)
62+
coerced (coerce value)]
63+
(if (su/error? coerced)
64+
(let [errors (su/error-val coerced)]
65+
(cc/map->CoercionError
66+
{:schema schema
67+
:errors errors}))
68+
coerced))
69+
value)))
70+
71+
(accept-response? [_ model]
72+
(coerce-response? model))
73+
74+
(coerce-response [this schema value type format request]
75+
(cc/coerce-request this schema value type format request)))
76+
77+
(def default-options
78+
{:body {:default (constantly nil)
79+
:formats {"application/json" json-coercion-matcher
80+
"application/msgpack" json-coercion-matcher
81+
"application/x-yaml" json-coercion-matcher}}
82+
:string {:default string-coercion-matcher}
83+
:response {:default (constantly nil)}})
84+
85+
(defn create-coercion [options]
86+
(->SchemaCoercion :schema options))
87+
88+
(def default-coercion (create-coercion default-options))

0 commit comments

Comments
 (0)