Skip to content

Commit

Permalink
Merge branch '1.1.x' of github.com:metosin/compojure-api into 1.1.x
Browse files Browse the repository at this point in the history
  • Loading branch information
frenchy64 committed May 27, 2024
2 parents 3f92625 + a8cf4d6 commit 0c48047
Show file tree
Hide file tree
Showing 19 changed files with 630 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
test:
strategy:
matrix:
jdk: [8, 11, 17, 21]
jdk: [8, 11, 17, 21, 22]

name: Java ${{ matrix.jdk }}

Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
## NEXT

## NEXT
* drop support for Clojure 1.8
* upgrade cheshire 5.13.0
* Backport: use muuntaja in compojure.api.validator

## 1.1.14 (2024-04-30)
* Remove potemkin [#445](https://github.com/metosin/compojure-api/issues/445)
* backport `route-middleware`
Expand Down
22 changes: 14 additions & 8 deletions project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
:scm {:name "git"
:url "https://github.com/metosin/compojure-api"}
:dependencies [[prismatic/plumbing "0.6.0"]
[cheshire "5.9.0"]
[cheshire "5.13.0"]
[compojure "1.6.1"]
[prismatic/schema "1.1.12"]
[org.tobereplaced/lettercase "1.0.0"]
Expand All @@ -20,7 +20,7 @@
:profiles {:uberjar {:aot :all
:ring {:handler examples.thingie/app}
:source-paths ["examples/thingie/src"]
:dependencies [[org.clojure/clojure "1.8.0"]
:dependencies [[org.clojure/clojure "1.9.0"]
[http-kit "2.3.0"]
[reloaded.repl "0.2.4"]
[com.stuartsierra/component "0.4.0"]]}
Expand All @@ -30,7 +30,13 @@
[lein-midje "3.2.1"]
[lein-ring "0.12.0"]
[funcool/codeina "0.5.0"]]
:dependencies [[org.clojure/clojure "1.8.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"]
Expand All @@ -47,9 +53,9 @@
"-Xmx4096m"
"-Dclojure.compiler.direct-linking=true"]}
:logging {:dependencies [[org.clojure/tools.logging "0.5.0"]]}
:1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
:1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]}
:1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]}}
: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"]]}}
:eastwood {:namespaces [:source-paths]
:add-linters [:unused-namespaces]}
:codeina {:sources ["src"]
Expand All @@ -73,10 +79,10 @@
["change" "version" "leiningen.release/bump-version"]
["vcs" "commit"]
["vcs" "push"]]
:aliases {"all" ["with-profile" "dev:dev,logging:dev,1.10"]
:aliases {"all" ["with-profile" "dev:dev,logging:dev,1.10:dev,1.11:dev,1.12"]
"start-thingie" ["run"]
"aot-uberjar" ["with-profile" "uberjar" "do" "clean," "ring" "uberjar"]
"test-ancient" ["midje"]
"perf" ["with-profile" "default,dev,perf"]
"deploy!" ^{:doc "Recompile sources, then deploy if tests succeed."}
["do" ["clean"] ["midje"] ["deploy" "clojars"]]})
["do" ["clean"] ["midje"] ["deploy" "clojars"]]})
27 changes: 27 additions & 0 deletions src/compojure/api/async.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(ns compojure.api.async
(:require [compojure.response :as response]
[compojure.api.common :as common]
compojure.api.routes))

(common/when-ns 'manifold.deferred
;; Compojure is smart enough to get the success value out of deferred by
;; itself, but we want to catch the exceptions as well.
(extend-protocol compojure.response/Sendable
manifold.deferred.IDeferred
(send* [deferred request respond raise]
(manifold.deferred/on-realized deferred #(response/send % request respond raise) raise))))

(common/when-ns 'clojure.core.async
(extend-protocol compojure.response/Sendable
clojure.core.async.impl.channels.ManyToManyChannel
(send* [channel request respond raise]
(clojure.core.async/go
(let [message (clojure.core.async/<! channel)]
(if (instance? Throwable message)
(raise message)
(response/send message request respond raise)))))))

(extend-protocol compojure.response/Sendable
compojure.api.routes.Route
(send* [this request respond raise]
((.handler this) request #(response/send % request respond raise) raise)))
99 changes: 99 additions & 0 deletions src/compojure/api/coercion.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
(ns compojure.api.coercion
(:require [clojure.walk :as walk]
[compojure.api.exception :as ex]
[compojure.api.request :as request]
[compojure.api.coercion.core :as cc]
;; side effects
compojure.api.coercion.register-schema
compojure.api.coercion.register-spec)
(:import (compojure.api.coercion.core CoercionError)))

(def default-coercion :schema)

(defn set-request-coercion [request coercion]
(assoc request ::request/coercion coercion))

(defn get-request-coercion [request]
(if-let [entry (find request ::request/coercion)]
(val entry)
default-coercion))

(defn resolve-coercion [coercion]
(cond
(nil? coercion) nil
(keyword? coercion) (cc/named-coercion coercion)
(satisfies? cc/Coercion coercion) coercion
:else (throw (ex-info (str "invalid coercion " coercion) {:coercion coercion}))))

(defn get-apidocs [maybe-coercion spec info]
(if-let [coercion (resolve-coercion maybe-coercion)]
(cc/get-apidocs coercion spec info)))

(defn coerce-request! [model in type keywordize? open? request]
(let [transform (if keywordize? walk/keywordize-keys identity)
value (transform (in request))]
(if-let [coercion (-> request
(get-request-coercion)
(resolve-coercion))]
(let [model (if open? (cc/make-open coercion model) model)
format (some-> request :muuntaja/request :format)
result (cc/coerce-request coercion model value type format request)]
(if (instance? CoercionError result)
(throw (ex-info
(str "Request validation failed: " (pr-str result))
(merge
(into {} result)
{:type ::ex/request-validation
:coercion coercion
:value value
:in [:request in]
:request request})))
result))
value)))

(defn coerce-response! [request {:keys [status body] :as response} responses]
(if-let [model (or (:schema (get responses status))
(:schema (get responses :default)))]
(if-let [coercion (-> request
(get-request-coercion)
(resolve-coercion))]
(let [format (or (-> response :muuntaja/content-type)
(some-> request :muuntaja/response :format))
accept? (cc/accept-response? coercion model)]
(if accept?
(let [result (cc/coerce-response coercion model body :response format response)]
(if (instance? CoercionError result)
(throw (ex-info
(str "Response validation failed: " (pr-str result))
(merge
(into {} result)
{:type ::ex/response-validation
:coercion coercion
:value body
:in [:response :body]
:request request
:response response})))
(assoc response
:compojure.api.meta/serializable? true
:body result)))
response))
response)
response))

;;
;; middleware
;;

(defn wrap-coerce-response [handler responses]
(fn
([request]
(coerce-response! request (handler request) responses))
([request respond raise]
(handler
request
(fn [response]
(try
(respond (coerce-response! request response responses))
(catch Exception e
(raise e))))
raise))))
23 changes: 23 additions & 0 deletions src/compojure/api/coercion/core.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
(ns compojure.api.coercion.core)

(defprotocol Coercion
(get-name [this])
(get-apidocs [this model data])
(make-open [this model])
(encode-error [this error])
(coerce-request [this model value type format request])
(accept-response? [this model])
(coerce-response [this model value type format request]))

(defrecord CoercionError [])

(defmulti named-coercion identity :default ::default)

(defmethod named-coercion ::default [x]
(let [message (if (= :spec x)
(str "spec-coercion is not enabled. "
"you most likely are missing the "
"required deps: org.clojure/clojure 1.9+ "
"and metosin/spec-tools.")
(str "cant find named-coercion for " x))]
(throw (ex-info message {:name x}))))
8 changes: 8 additions & 0 deletions src/compojure/api/coercion/register_schema.clj
Original file line number Diff line number Diff line change
@@ -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)))))
8 changes: 8 additions & 0 deletions src/compojure/api/coercion/register_spec.clj
Original file line number Diff line number Diff line change
@@ -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)))))
88 changes: 88 additions & 0 deletions src/compojure/api/coercion/schema.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
(ns compojure.api.coercion.schema
(:require [schema.coerce :as sc]
[schema.utils :as su]
[ring.swagger.coerce :as coerce]
[compojure.api.coercion.core :as cc]
[clojure.walk :as walk]
[schema.core :as s]
[compojure.api.common :as common]
;; side effects
compojure.api.coercion.register-schema)
(:import (java.io File)
(schema.core OptionalKey RequiredKey)
(schema.utils ValidationError NamedError)))

(def string-coercion-matcher coerce/query-schema-coercion-matcher)
(def json-coercion-matcher coerce/json-schema-coercion-matcher)

(defn stringify
"Stringifies Schema records recursively."
[error]
(walk/prewalk
(fn [x]
(cond
(class? x) (.getName ^Class x)
(instance? OptionalKey x) (pr-str (list 'opt (:k x)))
(instance? RequiredKey x) (pr-str (list 'req (:k x)))
(and (satisfies? s/Schema x) (record? x)) (try (pr-str (s/explain x)) (catch Exception _ x))
(instance? ValidationError x) (str (su/validation-error-explain x))
(instance? NamedError x) (str (su/named-error-explain x))
:else x))
error))

(def memoized-coercer
(common/fifo-memoize sc/coercer 1000))

;; don't use coercion for certain types
(defmulti coerce-response? identity :default ::default)
(defmethod coerce-response? ::default [_] true)
(defmethod coerce-response? File [_] false)

(defrecord SchemaCoercion [name options]
cc/Coercion
(get-name [_] name)

(get-apidocs [_ _ data] data)

(make-open [_ schema]
(if (map? schema)
(assoc schema s/Keyword s/Any)
schema))

(encode-error [_ error]
(-> error
(update :schema pr-str)
(update :errors stringify)))

(coerce-request [_ schema value type format request]
(let [type-options (options type)]
(if-let [matcher (or (get (get type-options :formats) format)
(get type-options :default))]
(let [coerce (memoized-coercer schema matcher)
coerced (coerce value)]
(if (su/error? coerced)
(let [errors (su/error-val coerced)]
(cc/map->CoercionError
{:schema schema
:errors errors}))
coerced))
value)))

(accept-response? [_ model]
(coerce-response? model))

(coerce-response [this schema value type format request]
(cc/coerce-request this schema value type format request)))

(def default-options
{:body {:default (constantly nil)
:formats {"application/json" json-coercion-matcher
"application/msgpack" json-coercion-matcher
"application/x-yaml" json-coercion-matcher}}
:string {:default string-coercion-matcher}
:response {:default (constantly nil)}})

(defn create-coercion [options]
(->SchemaCoercion :schema options))

(def default-coercion (create-coercion default-options))
Loading

0 comments on commit 0c48047

Please sign in to comment.