M project.clj => project.clj +2 -7
@@ 4,11 4,6 @@
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
:url "https://www.eclipse.org/legal/epl-2.0/"}
:dependencies [[org.clojure/clojure "1.10.0" :scope "provided"]
+ [cheshire "5.10.0" :exclusions [com.fasterxml.jackson.core/jackson-core]]
[com.atlassian.oai/swagger-request-validator-core "2.18.1"]]
- :profiles {:dev {:dependencies [[org.clojure/data.json "2.4.0"]]
- :resource-paths ["dev-resources"]
- :plugins [[lein-codox "0.10.7"]]
- :codox {:metadata {:doc/format :markdown}
- :output-path "codox"}}}
- :deploy-repositories [["releases" {:url "https://repo.clojars.org" :creds :gpg}]])
-
+ :profiles {:dev {:resource-paths ["dev-resources"]}})
M src/nl/zeekat/ring_openapi_validator.clj => src/nl/zeekat/ring_openapi_validator.clj +51 -1
@@ 1,5 1,6 @@
(ns nl.zeekat.ring-openapi-validator
- (:require [clojure.string :as string])
+ (:require [clojure.string :as string]
+ [cheshire.core :as json])
(:import com.atlassian.oai.validator.OpenApiInteractionValidator
com.atlassian.oai.validator.model.Request
com.atlassian.oai.validator.model.Request$Method
@@ 119,6 120,7 @@
([spec]
(openapi-validator spec {})))
+
(defn validate-interaction
"Validate a `request`/`response` pair using the given `validator`.
@@ 142,3 144,51 @@
If any issues are found, returns a report collection"
[validator method path response]
(report->coll (.validateResponse validator path (ring->Method method) (ring->Response response))))
+
+
+;; FIXME:
+;; https://stackoverflow.com/questions/5034311/multiple-readers-for-inputstream-in-java/30262036#30262036
+;; strategy: copy the input steam /once/ so that there can be exactly
+;; one downstream reader, and the memory can be freed after - fully
+;; immutable means there's no way to tell when we're "done" with the
+;; request body.
+
+(defn- immutable-request-body
+ "Replace mutable InputStream body with the equivalent re-readable content.
+
+ Ensures that any multiple handlers/middleware can read the body
+ content without stepping on each other's toes.
+
+ Returns a new ring request"
+ [{:keys [body] :as request}]
+ (if (instance? java.io.InputStream body)
+ (assoc request :body (slurp body))
+ request))
+
+(defn wrap-request-validator
+ "Middleware validating requests against an OOAPI spec.
+
+ - `f` is the handler to wrap
+ - `validator` should be an `OpenApiInteractionValidator` - see
+ [[openapi-validator]].
+
+ Each incoming request is validated using [[validate-request]]. When
+ errors are found, the a 400 Bad Request response is returned with the
+ error collection as the response body. When the request is valid
+ according to the validator, it is passed along to the original
+ handler.
+
+ (-> my-api-handler
+ (wrap-validator (openapi-validator \"path/to/spec.json\"))
+ (wrap-json-response))
+
+ Since the error response body is just clojure map, you need some other
+ middleware like `ring.middleware.json` to turn it into full ring
+ response."
+ [f validator]
+ (fn [request]
+ (let [request (immutable-request-body request)]
+ (if-let [errs (validate-request validator request)]
+ {:status 400 ; bad request
+ :body errs}
+ (f request)))))
M test/nl/zeekat/ring_openapi_validator_test.clj => test/nl/zeekat/ring_openapi_validator_test.clj +3 -3
@@ 1,5 1,5 @@
(ns nl.zeekat.ring-openapi-validator-test
- (:require [clojure.data.json :as json]
+ (:require [cheshire.core :as json]
[clojure.java.io :as io]
[clojure.test :refer [deftest testing is]]
[nl.zeekat.ring-openapi-validator :as validator]))
@@ 30,11 30,11 @@
(def ooapi-response {:headers {"Content-Type" ooapi-content-type}
:status 200
- :body (json/json-str ooapi-resource)})
+ :body (json/generate-string ooapi-resource)})
(def ooapi-invalid-response {:headers {"Content-Type" ooapi-content-type}
:status 200
- :body (json/json-str (dissoc ooapi-resource :owner))})
+ :body (json/generate-string (dissoc ooapi-resource :owner))})
(deftest test-openapi-validator
(let [validator (validator/openapi-validator "ooapi.json" {})]