From de3fc480b42764fca81fe06633ebee0074e6acb9 Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Fri, 7 Sep 2018 19:50:44 +0300 Subject: [PATCH] muuntaja --- .../src/reitit/http/interceptors/muuntaja.clj | 105 +++++++++++++ .../reitit-swagger/src/reitit/swagger.cljc | 5 +- .../http/interceptors/muuntaja_test.clj | 147 ++++++++++++++++++ 3 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 modules/reitit-interceptors/src/reitit/http/interceptors/muuntaja.clj create mode 100644 test/clj/reitit/http/interceptors/muuntaja_test.clj diff --git a/modules/reitit-interceptors/src/reitit/http/interceptors/muuntaja.clj b/modules/reitit-interceptors/src/reitit/http/interceptors/muuntaja.clj new file mode 100644 index 00000000..aaf2d34e --- /dev/null +++ b/modules/reitit-interceptors/src/reitit/http/interceptors/muuntaja.clj @@ -0,0 +1,105 @@ +(ns reitit.http.interceptors.muuntaja + (:require [muuntaja.core :as m] + [muuntaja.interceptor] + [clojure.spec.alpha :as s])) + +(s/def ::muuntaja m/muuntaja?) +(s/def ::spec (s/keys :opt-un [::muuntaja])) + +(defn- displace [x] (with-meta x {:displace true})) +(defn- stripped [x] (select-keys x [:enter :leave :error])) + +(defn format-interceptor + "Interceptor for content-negotiation, request and response formatting. + + Negotiates a request body based on `Content-Type` header and response body based on + `Accept`, `Accept-Charset` headers. Publishes the negotiation results as `:muuntaja/request` + and `:muuntaja/response` keys into the request. + + Decodes the request body into `:body-params` using the `:muuntaja/request` key in request + if the `:body-params` doesn't already exist. + + Encodes the response body using the `:muuntaja/response` key in request if the response + doesn't have `Content-Type` header already set. + + Optionally takes a default muuntaja instance as argument. + + | key | description | + | -------------|-------------| + | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set." + ([] + (format-interceptor nil)) + ([default-muuntaja] + {:name ::format + :spec ::spec + :compile (fn [{:keys [muuntaja]} _] + (if-let [muuntaja (or muuntaja default-muuntaja)] + (merge + (stripped (muuntaja.interceptor/format-interceptor muuntaja)) + {:data {:swagger {:produces (displace (m/encodes muuntaja)) + :consumes (displace (m/decodes muuntaja))}}})))})) + +(defn format-negotiate-interceptor + "Interceptor for content-negotiation. + + Negotiates a request body based on `Content-Type` header and response body based on + `Accept`, `Accept-Charset` headers. Publishes the negotiation results as `:muuntaja/request` + and `:muuntaja/response` keys into the request. + + Optionally takes a default muuntaja instance as argument. + + | key | description | + | -------------|-------------| + | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set." + ([] + (format-negotiate-interceptor nil)) + ([default-muuntaja] + {:name ::format-negotiate + :spec ::spec + :compile (fn [{:keys [muuntaja]} _] + (if-let [muuntaja (or muuntaja default-muuntaja)] + (stripped (muuntaja.interceptor/format-negotiate-interceptor muuntaja))))})) + +(defn format-request-interceptor + "Interceptor for request formatting. + + Decodes the request body into `:body-params` using the `:muuntaja/request` key in request + if the `:body-params` doesn't already exist. + + Optionally takes a default muuntaja instance as argument. + + | key | description | + | -------------|-------------| + | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set." + ([] + (format-request-interceptor nil)) + ([default-muuntaja] + {:name ::format-request + :spec ::spec + :compile (fn [{:keys [muuntaja]} _] + (if-let [muuntaja (or muuntaja default-muuntaja)] + (merge + (stripped (muuntaja.interceptor/format-request-interceptor muuntaja)) + {:data {:swagger {:consumes (displace (m/decodes muuntaja))}}})))})) + +(defn format-response-interceptor + "Interceptor for response formatting. + + Encodes the response body using the `:muuntaja/response` key in request if the response + doesn't have `Content-Type` header already set. + + Optionally takes a default muuntaja instance as argument. + + | key | description | + | -------------|-------------| + | `:muuntaja` | `muuntaja.core/Muuntaja` instance, does not mount if not set." + ([] + (format-response-interceptor nil)) + ([default-muuntaja] + {:name ::format-response + :spec ::spec + :compile (fn [{:keys [muuntaja]} _] + (if-let [muuntaja (or muuntaja default-muuntaja)] + (merge + (stripped (muuntaja.interceptor/format-request-interceptor muuntaja)) + {:data {:swagger {:produces (displace (m/encodes muuntaja))}}})))})) diff --git a/modules/reitit-swagger/src/reitit/swagger.cljc b/modules/reitit-swagger/src/reitit/swagger.cljc index bb92607f..8340b250 100644 --- a/modules/reitit-swagger/src/reitit/swagger.cljc +++ b/modules/reitit-swagger/src/reitit/swagger.cljc @@ -84,11 +84,14 @@ :x-id ids})) accept-route (fn [route] (-> route second :swagger :id (or ::default) ->set (set/intersection ids) seq)) - transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data middleware :middleware}]] + transform-endpoint (fn [[method {{:keys [coercion no-doc swagger] :as data} :data + middleware :middleware + interceptors :interceptors}]] (if (and data (not no-doc)) [method (meta-merge (apply meta-merge (keep (comp :swagger :data) middleware)) + (apply meta-merge (keep (comp :swagger :data) interceptors)) (if coercion (coercion/get-apidocs coercion :swagger data)) (select-keys data [:tags :summary :description]) diff --git a/test/clj/reitit/http/interceptors/muuntaja_test.clj b/test/clj/reitit/http/interceptors/muuntaja_test.clj new file mode 100644 index 00000000..22c402df --- /dev/null +++ b/test/clj/reitit/http/interceptors/muuntaja_test.clj @@ -0,0 +1,147 @@ +(ns reitit.http.interceptors.muuntaja-test + (:require [clojure.test :refer [deftest testing is]] + [reitit.http :as http] + [reitit.http.interceptors.muuntaja :as muuntaja] + [reitit.swagger :as swagger] + [reitit.interceptor.sieppari :as sieppari] + [muuntaja.core :as m])) + +(deftest muuntaja-test + (let [data {:kikka "kukka"} + app (http/ring-handler + (http/router + ["/ping" {:get (constantly {:status 200, :body data})}] + {:data {:muuntaja m/instance + :interceptors [(muuntaja/format-interceptor)]}}) + {:executor sieppari/executor})] + (is (= data (->> {:request-method :get, :uri "/ping"} + (app) + :body + (m/decode m/instance "application/json")))))) + +(deftest muuntaja-swagger-test + (let [with-defaults m/instance + no-edn-decode (m/create (-> m/default-options (update-in [:formats "application/edn"] dissoc :decoder))) + just-edn (m/create (-> m/default-options (m/select-formats ["application/edn"]))) + app (http/ring-handler + (http/router + [["/defaults" + {:get identity}] + ["/explicit-defaults" + {:muuntaja with-defaults + :get identity}] + ["/no-edn-decode" + {:muuntaja no-edn-decode + :get identity}] + ["/just-edn" + {:muuntaja just-edn + :get identity}] + ["/swagger.json" + {:get {:no-doc true + :handler (swagger/create-swagger-handler)}}]] + {:data {:muuntaja m/instance + :interceptors [(muuntaja/format-interceptor)]}}) + {:executor sieppari/executor}) + spec (fn [path] + (let [path (keyword path)] + (-> {:request-method :get :uri "/swagger.json"} + (app) :body + (->> (m/decode m/instance "application/json")) + :paths path :get))) + produces (comp set :produces spec) + consumes (comp set :consumes spec)] + + (testing "with defaults" + (let [path "/defaults"] + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json" + "application/edn"} + (produces path) + (consumes path))))) + + (testing "with explicit muuntaja defaults" + (let [path "/explicit-defaults"] + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json" + "application/edn"} + (produces path) + (consumes path))))) + + (testing "without edn decode" + (let [path "/no-edn-decode"] + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json" + "application/edn"} + (produces path))) + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json"} + (consumes path))))) + + (testing "just edn" + (let [path "/just-edn"] + (is (= #{"application/edn"} + (produces path) + (consumes path))))))) + +(deftest muuntaja-swagger-parts-test + (let [app (http/ring-handler + (http/router + [["/request" + {:interceptors [(muuntaja/format-negotiate-interceptor) + (muuntaja/format-request-interceptor)] + :get identity}] + ["/response" + {:interceptors [(muuntaja/format-negotiate-interceptor) + (muuntaja/format-response-interceptor)] + :get identity}] + ["/both" + {:interceptors [(muuntaja/format-negotiate-interceptor) + (muuntaja/format-response-interceptor) + (muuntaja/format-request-interceptor)] + :get identity}] + + ["/swagger.json" + {:get {:no-doc true + :handler (swagger/create-swagger-handler)}}]] + {:data {:muuntaja m/instance}}) + {:executor sieppari/executor}) + spec (fn [path] + (-> {:request-method :get :uri "/swagger.json"} + (app) :body :paths (get path) :get)) + produces (comp :produces spec) + consumes (comp :consumes spec)] + + (testing "just request formatting" + (let [path "/request"] + (is (nil? (produces path))) + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json" + "application/edn"} + (consumes path))))) + + (testing "just response formatting" + (let [path "/response"] + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json" + "application/edn"} + (produces path))) + (is (nil? (consumes path))))) + + (testing "just response formatting" + (let [path "/both"] + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json" + "application/edn"} + (produces path))) + (is (= #{"application/json" + "application/transit+msgpack" + "application/transit+json" + "application/edn"} + (consumes path)))))))