Skip to content

Commit

Permalink
resupport fn? coercions
Browse files Browse the repository at this point in the history
  • Loading branch information
frenchy64 committed Jun 6, 2024
1 parent 23b96cb commit 113263b
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 11 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
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))
* Add compatibility for 1.x coercions
* implies Schema backend
* fn from request->field->schema->coercer
* mostly often just `(constantly nil)` or (constantly {:body matcher})

## 2.0.0-alpha34-SNAPSHOT
* **BREAKING CHANGE**: `:formatter :muuntaja` sometimes required for `api{-middleware}` options
Expand Down
1 change: 0 additions & 1 deletion src/compojure/api/api.clj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
[compojure.api.request :as request]
[compojure.api.routes :as routes]
[compojure.api.common :as common]
[compojure.api.request :as request]
[ring.swagger.common :as rsc]
[ring.swagger.middleware :as rsm]))

Expand Down
3 changes: 2 additions & 1 deletion src/compojure/api/coercion.clj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
[compojure.api.request :as request]
[compojure.api.coercion.core :as cc]
;; side effects
compojure.api.coercion.schema
[compojure.api.coercion.schema :as cschema]
compojure.api.coercion.spec)
(:import (compojure.api.coercion.core CoercionError)))

Expand All @@ -23,6 +23,7 @@
(nil? coercion) nil
(keyword? coercion) (cc/named-coercion coercion)
(satisfies? cc/Coercion coercion) coercion
(fn? coercion) (cschema/create-coercion coercion)
:else (throw (ex-info (str "invalid coercion " coercion) {:coercion coercion}))))

(defn get-apidocs [maybe-coercion spec info]
Expand Down
14 changes: 11 additions & 3 deletions src/compojure/api/coercion/schema.clj
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,15 @@
(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 [legacy? (fn? options)
type-options (if legacy?
(when-let [provider (options request)]
(provider type))
(options type))]
(if-let [matcher (if legacy?
type-options
(or (get (get type-options :formats) format)
(get type-options :default)))]
(let [coerce (memoized-coercer schema matcher)
coerced (coerce value)]
(if (su/error? coerced)
Expand All @@ -85,6 +91,8 @@
:response {:default (constantly nil)}})

(defn create-coercion [options]
{:pre [(or (map? options)
(fn? options))]}
(->SchemaCoercion :schema options))

(def default-coercion (create-coercion default-options))
Expand Down
4 changes: 3 additions & 1 deletion src/compojure/api/middleware.clj
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@
:string coerce/query-schema-coercion-matcher
:response coerce/json-schema-coercion-matcher})

;; 1.1.x
(def no-response-coercion
(constantly (dissoc default-coercion-matchers :response)))

(defn coercion-matchers [request]
(let [options (get-options request)]
(if (contains? options :coercion)
Expand Down
120 changes: 116 additions & 4 deletions test/compojure/api/coercion_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
[ring.util.http-response :refer :all]
[clojure.core.async :as a]
[schema.core :as s]
[compojure.api.middleware :as mw]
[compojure.api.coercion.schema :as cs]))

(defn is-has-body [expected value]
(is (= (second value) expected)))

(defn is-fails-with [expected-status [status body]]
(is (= status expected-status))
(is (every? (partial contains? body) [:type :coercion :in :value :schema :errors])))
(is (= status expected-status)
(pr-str body))
(is (every? (partial contains? body) [:type :coercion :in :value :schema :errors])
(pr-str body)))

(deftest schema-coercion-test
(testing "response schemas"
Expand Down Expand Up @@ -74,7 +77,15 @@
ping-route)]
(let [[status body] (get* app "/ping")]
(is (= 200 status))
(is (= {:pong 123} body)))))
(is (= {:pong 123} body))))
(testing "legacy"
(let [app (api
{:formatter :muuntaja
:coercion mw/no-response-coercion}
ping-route)]
(let [[status body] (get* app "/ping")]
(is (= 200 status))
(is (= {:pong 123} body))))))
(testing "all coercion"
(let [app (api
{:formatter :muuntaja
Expand All @@ -101,7 +112,7 @@
(a/go (ok "foo"))))]
(is-fails-with 500 (get* app "/async"))))))))

(testing "body coersion"
(testing "body coercion"
(let [beer-route (POST "/beer" []
:body [body {:beers #{(s/enum "ipa" "apa")}}]
(ok body))]
Expand Down Expand Up @@ -131,12 +142,36 @@
(is (= 200 status))
(is (= {:beers ["ipa" "apa" "ipa"]} body)))))

(testing "legacy body-coercion can be disabled"
(let [no-body-coercion (constantly (dissoc mw/default-coercion-matchers :body))
app (api
{:formatter :muuntaja
:coercion no-body-coercion}
beer-route)]
(let [[status body] (post* app "/beer" (json-string {:beers ["ipa" "apa" "ipa"]}))]
(is (= 200 status))
(is (= {:beers ["ipa" "apa" "ipa"]} body))))
(let [app (api
{:formatter :muuntaja
:coercion nil}
beer-route)]
(let [[status body] (post* app "/beer" (json-string {:beers ["ipa" "apa" "ipa"]}))]
(is (= 200 status))
(is (= {:beers ["ipa" "apa" "ipa"]} body)))))

(testing "body-coercion can be changed"
(let [nop-body-coercion (cs/create-coercion (assoc cs/default-options :body {:default (constantly nil)}))
app (api
{:formatter :muuntaja
:coercion nop-body-coercion}
beer-route)]
(is-fails-with 400 (post* app "/beer" (json-string {:beers ["ipa" "apa" "ipa"]})))))
(testing "legacy body-coercion can be changed"
(let [nop-body-coercion (constantly (assoc mw/default-coercion-matchers :body (constantly nil)))
app (api
{:formatter :muuntaja
:coercion nop-body-coercion}
beer-route)]
(is-fails-with 400 (post* app "/beer" (json-string {:beers ["ipa" "apa" "ipa"]})))))))

(testing "query coercion"
Expand All @@ -162,12 +197,30 @@
(is (= 200 status))
(is (= {:i "10"} body)))))

(testing "legacy query-coercion can be disabled"
(let [no-query-coercion (constantly (dissoc mw/default-coercion-matchers :string))
app (api
{:formatter :muuntaja
:coercion no-query-coercion}
query-route)]
(let [[status body] (get* app "/query" {:i 10})]
(is (= 200 status))
(is (= {:i "10"} body)))))

(testing "query-coercion can be changed"
(let [nop-query-coercion (cs/create-coercion (assoc cs/default-options :string {:default (constantly nil)}))
app (api
{:formatter :muuntaja
:coercion nop-query-coercion}
query-route)]
(is-fails-with 400 (get* app "/query" {:i 10}))))

(testing "legacy query-coercion can be changed"
(let [nop-query-coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
app (api
{:formatter :muuntaja
:coercion nop-query-coercion}
query-route)]
(is-fails-with 400 (get* app "/query" {:i 10}))))))

(testing "route-specific coercion"
Expand Down Expand Up @@ -207,6 +260,52 @@

(testing "no coercion"
(let [[status body] (get* app "/no-coercion" {:i 10})]
(is (= 200 status))
(is (= {:i "10"} body))))))
(testing "legacy route-specific coercion"
(let [app (api
{:formatter :muuntaja}
(GET "/default" []
:query-params [i :- s/Int]
(ok {:i i}))
(GET "/disabled-coercion" []
:coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
:query-params [i :- s/Int]
(ok {:i i}))
(GET "/no-coercion" []
:coercion (constantly nil)
:query-params [i :- s/Int]
(ok {:i i}))
(GET "/nil-coercion" []
:coercion nil
:query-params [i :- s/Int]
(ok {:i i})))]

(testing "default coercion"
(let [[status body] (get* app "/default" {:i 10})]
(is (= 200 status))
(is (= {:i 10} body))))

(testing "disabled coercion"
(is-fails-with 400 (get* app "/disabled-coercion" {:i 10})))

(testing "exception data"
(let [ex (get* app "/disabled-coercion" {:i 10})]
(is (= 400 (first ex)))
(is (= {:type "compojure.api.exception/request-validation"
:coercion "schema",
:in ["request" "query-params"],
:value {:i "10"}
:schema "{Keyword Any, :i Int}",
:errors {:i "(not (integer? \"10\"))"}}
(select-keys (second ex)
[:type :coercion :in :value :schema :errors])))))

(testing "no coercion"
(let [[status body] (get* app "/no-coercion" {:i 10})]
(is (= 200 status))
(is (= {:i "10"} body)))
(let [[status body] (get* app "/nil-coercion" {:i 10})]
(is (= 200 status))
(is (= {:i "10"} body)))))))

Expand Down Expand Up @@ -237,6 +336,19 @@
(is (= ["1" 2] (:body (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y 2}}))))
(is (thrown? Exception (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y "abba"}})))))

(testing "legacy coercion can be overridden"
(let [app (context "/api" []
:query-params [{y :- Long 0}]
(GET "/ping" []
:coercion (constantly nil)
:query-params [x :- Long]
(ok [x y])))]
(is (thrown? Exception (app {:request-method :get :uri "/api/ping" :query-params {}})))
(is (= ["abba" 0] (:body (app {:request-method :get :uri "/api/ping" :query-params {:x "abba"}}))))
(is (= ["1" 0] (:body (app {:request-method :get :uri "/api/ping" :query-params {:x "1"}}))))
(is (= ["1" 2] (:body (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y 2}}))))
(is (thrown? Exception (app {:request-method :get :uri "/api/ping" :query-params {:x "1", :y "abba"}})))))

(testing "context coercion is used for subroutes"
(let [app (context "/api" []
:coercion nil
Expand Down

0 comments on commit 113263b

Please sign in to comment.