diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1fff1ec9..fd565b5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: test: strategy: matrix: - jdk: [8, 11, 17, 21] + jdk: [8, 11, 17, 21, 22] name: Java ${{ matrix.jdk }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 93e25385..4672867b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` diff --git a/project.clj b/project.clj index eb5eb4d4..ac12927b 100644 --- a/project.clj +++ b/project.clj @@ -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"] @@ -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"]]} @@ -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"] @@ -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"] @@ -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"]]}) diff --git a/src/compojure/api/async.clj b/src/compojure/api/async.clj new file mode 100644 index 00000000..0f4bfe1b --- /dev/null +++ b/src/compojure/api/async.clj @@ -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/ 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)))) diff --git a/src/compojure/api/coercion/core.clj b/src/compojure/api/coercion/core.clj new file mode 100644 index 00000000..03508315 --- /dev/null +++ b/src/compojure/api/coercion/core.clj @@ -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})))) diff --git a/src/compojure/api/coercion/register_schema.clj b/src/compojure/api/coercion/register_schema.clj new file mode 100644 index 00000000..e1e8f993 --- /dev/null +++ b/src/compojure/api/coercion/register_schema.clj @@ -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))))) diff --git a/src/compojure/api/coercion/register_spec.clj b/src/compojure/api/coercion/register_spec.clj new file mode 100644 index 00000000..143320fb --- /dev/null +++ b/src/compojure/api/coercion/register_spec.clj @@ -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))))) diff --git a/src/compojure/api/coercion/schema.clj b/src/compojure/api/coercion/schema.clj new file mode 100644 index 00000000..9a7e01b0 --- /dev/null +++ b/src/compojure/api/coercion/schema.clj @@ -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)) diff --git a/src/compojure/api/coercion/spec.clj b/src/compojure/api/coercion/spec.clj new file mode 100644 index 00000000..b5d6ad31 --- /dev/null +++ b/src/compojure/api/coercion/spec.clj @@ -0,0 +1,153 @@ +(ns compojure.api.coercion.spec + (:require [schema.core] + [clojure.spec.alpha :as s] + [spec-tools.core :as st] + [spec-tools.data-spec :as ds] + [clojure.walk :as walk] + [compojure.api.coercion.core :as cc] + [spec-tools.swagger.core :as swagger] + [compojure.api.common :as common] + ;; side effects + compojure.api.coercion.register-spec) + (:import (clojure.lang IPersistentMap) + (schema.core RequiredKey OptionalKey) + (spec_tools.core Spec) + (spec_tools.data_spec Maybe))) + +(def string-transformer + (st/type-transformer + st/string-transformer + st/strip-extra-keys-transformer + {:name :string})) + +(def json-transformer + (st/type-transformer + st/json-transformer + st/strip-extra-keys-transformer + {:name :json})) + +(defn default-transformer + ([] (default-transformer :default)) + ([name] (st/type-transformer {:name name}))) + +(defprotocol Specify + (specify [this name])) + +(extend-protocol Specify + IPersistentMap + (specify [this name] + (-> (->> + (walk/postwalk + (fn [x] + (if (and (map? x) (not (record? x))) + (->> (for [[k v] (dissoc x schema.core/Keyword) + :let [k (cond + ;; Schema required + (instance? RequiredKey k) + (ds/req (schema.core/explicit-schema-key k)) + + ;; Schema options + (instance? OptionalKey k) + (ds/opt (schema.core/explicit-schema-key k)) + + :else + k)]] + [k v]) + (into {})) + x)) + this) + (ds/spec name)) + (dissoc :name))) + + Maybe + (into-spec [this name] + (ds/spec name this)) + + Spec + (specify [this _] this) + + Object + (specify [this _] + (st/create-spec {:spec this}))) + +(def memoized-specify + (common/fifo-memoize #(specify %1 (keyword "spec" (name (gensym "")))) 1000)) + +(defn maybe-memoized-specify [spec] + (if (keyword? spec) + (specify spec nil) + (memoized-specify spec))) + +(defn stringify-pred [pred] + (str (if (instance? clojure.lang.LazySeq pred) + (seq pred) + pred))) + +(defmulti coerce-response? identity :default ::default) +(defmethod coerce-response? ::default [_] true) + +(defrecord SpecCoercion [name options] + cc/Coercion + (get-name [_] name) + + (get-apidocs [_ _ {:keys [parameters responses] :as info}] + (cond-> (dissoc info :parameters :responses) + parameters (assoc + ::swagger/parameters + (into + (empty parameters) + (for [[k v] parameters] + [k (maybe-memoized-specify v)]))) + responses (assoc + ::swagger/responses + (into + (empty responses) + (for [[k response] responses] + [k (update response :schema #(some-> % maybe-memoized-specify))]))))) + + (make-open [_ spec] spec) + + (encode-error [_ error] + (let [problems (-> error :problems ::s/problems)] + (-> error + (update :spec (comp str s/form)) + (assoc :problems (mapv #(update % :pred stringify-pred) problems))))) + + (coerce-request [_ spec value type format _] + (let [spec (maybe-memoized-specify spec) + type-options (options type)] + (if-let [transformer (or (get (get type-options :formats) format) + (get type-options :default))] + (let [coerced (st/coerce spec value transformer)] + (if (s/valid? spec coerced) + coerced + (let [conformed (st/conform spec coerced transformer)] + (if (s/invalid? conformed) + (let [problems (st/explain-data spec coerced transformer)] + (cc/map->CoercionError + {:spec spec + :problems problems})) + (s/unform spec conformed))))) + value))) + + (accept-response? [_ spec] + (boolean (coerce-response? spec))) + + (coerce-response [this spec value type format request] + (cc/coerce-request this spec value type format request))) + +(def default-options + {:body {:default (default-transformer) + :formats {"application/json" json-transformer + "application/msgpack" json-transformer + "application/x-yaml" json-transformer}} + :string {:default string-transformer} + :response {:default (default-transformer) + :formats {"application/json" (default-transformer :json) + "application/msgpack" (default-transformer :json) + "application/x-yaml" (default-transformer :json)}}}) + +(defn create-coercion [options] + (->SpecCoercion :spec options)) + +(def default-coercion (create-coercion default-options)) diff --git a/src/compojure/api/common.clj b/src/compojure/api/common.clj index 4b143c5c..73f64f94 100644 --- a/src/compojure/api/common.clj +++ b/src/compojure/api/common.clj @@ -1,4 +1,5 @@ -(ns compojure.api.common) +(ns compojure.api.common + (:require [linked.core :as linked])) (defn plain-map? "checks whether input is a map, but not a record" @@ -41,3 +42,39 @@ (if (get v 1) (apply merge v) (get v 0))) + +(defn fast-map-merge + [x y] + (reduce-kv + (fn [m k v] + (assoc m k v)) + x + y)) + +(defn fifo-memoize [f size] + "Returns a memoized version of a referentially transparent f. The + memoized version of the function keeps a cache of the mapping from arguments + to results and, when calls with the same arguments are repeated often, has + higher performance at the expense of higher memory use. FIFO with size entries." + (let [cache (atom (linked/map))] + (fn [& xs] + (or (@cache xs) + (let [value (apply f xs)] + (swap! cache (fn [mem] + (let [mem (assoc mem xs value)] + (if (>= (count mem) size) + (dissoc mem (-> mem first first)) + mem)))) + value))))) + +;; NB: when-ns eats all exceptions inside the body, including those about +;; unresolvable symbols. Keep this in mind when debugging the definitions below. + +(defmacro when-ns [ns & body] + `(try + (eval + '(do + (require ~ns) + ~@body)) + (catch Exception ~'_))) + diff --git a/src/compojure/api/compojure_compat.clj b/src/compojure/api/compojure_compat.clj new file mode 100644 index 00000000..f8a09cbb --- /dev/null +++ b/src/compojure/api/compojure_compat.clj @@ -0,0 +1,38 @@ +(ns compojure.api.compojure-compat + "Compatibility for older Compojure versions." + (:require [clout.core :as clout] + [compojure.core :as c])) + +;; Copy-pasted from Compojure 1.6 to maintain backwards-compatibility with +;; Compojure 1.5. Essentially the same code existed in Compojure 1.5 but with a +;; different name. +;; + +(defn- context-request [request route] + (if-let [params (clout/route-matches route request)] + (let [uri (:uri request) + path (:path-info request uri) + context (or (:context request) "") + subpath (:__path-info params) + params (dissoc params :__path-info)] + (-> request + (#'c/assoc-route-params (#'c/decode-route-params params)) + (assoc :path-info (if (= subpath "") "/" subpath) + :context (#'c/remove-suffix uri subpath)))))) + +(defn ^:no-doc make-context [route make-handler] + (letfn [(handler + ([request] + ((make-handler request) request)) + ([request respond raise] + ((make-handler request) request respond raise)))] + (if (#{":__path-info" "/:__path-info"} (:source route)) + handler + (fn + ([request] + (if-let [request (context-request request route)] + (handler request))) + ([request respond raise] + (if-let [request (context-request request route)] + (handler request respond raise) + (respond nil))))))) diff --git a/src/compojure/api/core.clj b/src/compojure/api/core.clj index 12801465..07b22a14 100644 --- a/src/compojure/api/core.clj +++ b/src/compojure/api/core.clj @@ -1,18 +1,25 @@ (ns compojure.api.core (:require [compojure.api.meta :as meta] + [compojure.api.async] [compojure.api.routes :as routes] [compojure.api.middleware :as mw] [compojure.core :as compojure] [clojure.tools.macro :as macro])) -(defn- handle [handlers request] - (some #(% request) handlers)) +(defn ring-handler + "Creates vanilla ring-handler from any invokable thing (e.g. compojure-api route)" + [handler] + (fn + ([request] (handler request)) + ([request respond raise] (handler request respond raise)))) (defn routes "Create a Ring handler by combining several handlers into one." [& handlers] - (let [handlers (seq (keep identity handlers))] - (routes/create nil nil {} (vec handlers) (partial handle handlers)))) + (let [handlers (seq (keep identity (flatten handlers)))] + (routes/map->Route + {:childs (vec handlers) + :handler (meta/routing handlers)}))) (defmacro defroutes "Define a Ring handler function from a sequence of routes. @@ -35,7 +42,7 @@ not satisfying compojure.api.routes/Routing -protocol." [& handlers] (let [handlers (keep identity handlers)] - (routes/create nil nil {} nil (partial handle handlers)))) + (routes/map->Route {:handler (meta/routing handlers)}))) (defmacro middleware "Wraps routes with given middlewares using thread-first macro. @@ -67,7 +74,7 @@ {:childs [handler] :handler x-handler}))) -(defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true})) +(defmacro context {:style/indent 2} [& args] (meta/restructure nil args {:context? true :&form &form :&env &env})) (defmacro GET {:style/indent 2} [& args] (meta/restructure :get args nil)) (defmacro ANY {:style/indent 2} [& args] (meta/restructure nil args nil)) diff --git a/src/compojure/api/help.clj b/src/compojure/api/help.clj new file mode 100644 index 00000000..1d110f5e --- /dev/null +++ b/src/compojure/api/help.clj @@ -0,0 +1,71 @@ +(ns compojure.api.help + (:require [schema.core :as s] + [clojure.string :as str])) + +(def Topic (s/maybe s/Keyword)) +(def Subject (s/maybe (s/cond-pre s/Str s/Keyword s/Symbol))) + +;; +;; content formatting +;; + +(defn text [& s] + (->> s + (map #(if (seq? %) (apply text %) %)) + (str/join "\n"))) + +(defn title [& s] + (str "\u001B[32m" (text s) "\u001B[0m")) + +(defn code [& s] + (str "\u001B[33m" (text s) "\u001B[0m")) + +(defmulti help-for (fn [topic subject] [topic subject]) :default ::default) + +(defn- subject-text [topic subject] + (text + "" + (title subject) + "" + (help-for topic subject) + "")) + +(defn- topic-text [topic] + (let [subjects (-> (methods help-for) + (dissoc ::default) + (keys) + (->> (filter #(-> % first (= topic)))))] + (text + "Topic:\n" + (title topic) + "\nSubjects:" + (->> subjects + (map (partial apply subject-text)) + (map (partial str "\n")))))) + +(defn- help-text [] + (let [methods (dissoc (methods help-for) ::default)] + (text + "Usage:" + "" + (code + "(help)" + "(help topic)" + "(help topic subject)") + "\nTopics:\n" + (title (->> methods keys (map first) (distinct) (sort))) + "\nTopics & subjects:\n" + (title (->> methods keys (map (partial str/join " ")) (sort)))))) + +(defmethod help-for ::default [_ _] (help-text)) + +(s/defn ^:always-validate help + ([] + (println "------------------------------------------------------------") + (println (help-text))) + ([topic :- Topic] + (println "------------------------------------------------------------") + (println (topic-text topic))) + ([topic :- Topic, subject :- Subject] + (println "------------------------------------------------------------") + (println (subject-text topic subject)))) diff --git a/src/compojure/api/impl/json.clj b/src/compojure/api/impl/json.clj new file mode 100644 index 00000000..a16b193d --- /dev/null +++ b/src/compojure/api/impl/json.clj @@ -0,0 +1,6 @@ +(ns ^:no-doc compojure.api.impl.json + "Internal JSON formatting" + (:require [muuntaja.core :as m])) + +(def muuntaja + (m/create)) diff --git a/src/compojure/api/meta.clj b/src/compojure/api/meta.clj index c7e6a2ac..1b093871 100644 --- a/src/compojure/api/meta.clj +++ b/src/compojure/api/meta.clj @@ -239,6 +239,11 @@ `(do ~@body) (reverse (partition 2 bindings)))) +(defn routing [handlers] + (if-let [handlers (seq (keep identity (flatten handlers)))] + (apply compojure.core/routes handlers) + (fn ([_] nil) ([_ respond _] (respond nil))))) + ;; ;; Api ;; diff --git a/src/compojure/api/methods.clj b/src/compojure/api/methods.clj new file mode 100644 index 00000000..14be25a6 --- /dev/null +++ b/src/compojure/api/methods.clj @@ -0,0 +1,3 @@ +(ns compojure.api.methods) + +(def all-methods #{:get :head :patch :delete :options :post :put}) diff --git a/src/compojure/api/request.clj b/src/compojure/api/request.clj new file mode 100644 index 00000000..4f736afb --- /dev/null +++ b/src/compojure/api/request.clj @@ -0,0 +1,25 @@ +(ns compojure.api.request) + +(def coercion + "Request-scoped coercion" + ::coercion) + +(def swagger + "Vector of extra swagger data" + ::swagger) + +(def ring-swagger + "Ring-swagger options" + ::ring-swagger) + +(def paths + "Paths" + ::paths) + +(def lookup + "Reverse routing tree" + ::lookup) + +(def muuntaja + "Muuntaja instance" + ::muuntaja) diff --git a/src/compojure/api/validator.clj b/src/compojure/api/validator.clj index ce18ca4b..34cf7fc9 100644 --- a/src/compojure/api/validator.clj +++ b/src/compojure/api/validator.clj @@ -1,11 +1,12 @@ (ns compojure.api.validator (:require [compojure.api.swagger :as swagger] - [cheshire.core :as cheshire] + [compojure.api.impl.json :as json] [ring.swagger.validator :as rsv] + [muuntaja.core :as m] [compojure.api.middleware :as mw])) (defn validate - "Validates a api. If the api is Swagger-enabled, the swagger-spec + "Validates an api. If the api is Swagger-enabled, the swagger-spec is requested and validated against the JSON Schema. Returns either the (valid) api or throws an exception. Requires lazily the ring.swagger.validator -namespace allowing it to be excluded, #227" @@ -13,8 +14,8 @@ (when-let [uri (swagger/swagger-spec-path api)] (let [{status :status :as response} (api {:request-method :get :uri uri - mw/rethrow-exceptions? true}) - body (-> response :body slurp (cheshire/parse-string true))] + ::mw/rethrow-exceptions? true}) + body (->> response :body (m/decode json/muuntaja "application/json"))] (when-not (= status 200) (throw (ex-info (str "Coudn't read swagger spec from " uri)