add coercion for numeric params
CI / Test + coverage (push) Successful in 17s

This commit is contained in:
2026-09-19 06:32:12 -09:00
parent 4e0ddb601d
commit 0e3ea5a714
3 changed files with 51 additions and 18 deletions
+23 -1
View File
@@ -5,7 +5,12 @@
[reitit.ring :as ring]
[reitit.swagger :as swagger]
[reitit.swagger-ui :as swagger-ui]
[reitit.coercion]
[reitit.coercion.malli :as malli]
[reitit.ring.coercion :as coercion]
[reitit.ring.middleware.exception :as exception]
[reitit.ring.middleware.muuntaja :as muuntaja-middleware]
[reitit.ring.middleware.parameters :as parameters]
[muuntaja.core :as m]
[tbd.health.routes :as health]
[tbd.math.routes :as math]))
@@ -21,6 +26,19 @@
(and (map? (:body resp)) (nil? (:definitions (:body resp))))
(update :body dissoc :definitions))))))
(defn- coercion-error-handler
"Invalid request params -> 400 in the same EDN shape the routes use."
[e _req]
{:status 400
:headers {"Content-Type" "application/edn; charset=utf-8"}
:body (pr-str {:error "invalid request parameters"
:details (:humanized (reitit.coercion/encode-error (ex-data e)))})})
(def ^:private exception-middleware
(exception/create-exception-middleware
(assoc exception/default-handlers
:reitit.coercion/request-coercion coercion-error-handler)))
;; Each logical group (health, users, orders, ...) owns a `tbd.<group>.routes`
;; namespace that returns its own route subtree; they're concatenated here.
(def router
@@ -37,7 +55,11 @@
(health/routes)
(math/routes)]
{:data {:muuntaja m/instance
:middleware [muuntaja-middleware/format-middleware]}}))
:coercion malli/coercion
:middleware [parameters/parameters-middleware
muuntaja-middleware/format-middleware
exception-middleware
coercion/coerce-request-middleware]}}))
(def app
(-> (ring/ring-handler
+9 -9
View File
@@ -1,24 +1,24 @@
(ns tbd.math.routes
(:require [tbd.math.core :as math]))
(defn- parse-num [s]
(when s
(or (parse-long s) (parse-double s))))
;; :int first so "2" stays a long (a bare `number?` or :double would decode it to 2.0).
;; The :swagger/* props make the spec say plain `number` instead of the first :or branch.
(def ^:private number-schema
[:or {:swagger/type "number" :swagger/format "double"} :int :double])
(defn- edn-response [status body]
{:status status
:headers {"Content-Type" "application/edn; charset=utf-8"}
:body (pr-str body)})
(defn- add-handler [{:keys [query-params]}]
(let [x (parse-num (get query-params "x"))
y (parse-num (get query-params "y"))]
(if (and x y)
(edn-response 200 {:result (math/add x y)})
(edn-response 400 {:error "x and y query params are required and must be numbers"}))))
(defn- add-handler [{{{:keys [x y]} :query} :parameters}]
(edn-response 200 {:result (math/add x y)}))
(defn routes []
["/api/add"
{:get {:tags ["math"]
:summary "Add two numbers (?x=2&y=3 => {:result 5})"
:parameters {:query [:map
[:x {:description "First addend"} number-schema]
[:y {:description "Second addend"} number-schema]]}
:handler add-handler}}])
+19 -8
View File
@@ -1,16 +1,12 @@
(ns tbd.math.routes-test
(:require [clojure.edn :as edn]
[clojure.test :refer [deftest is testing]]
[reitit.ring :as ring]
[ring.middleware.params :refer [wrap-params]]
[tbd.math.routes :as math]))
;; wrap-params populates :query-params, as wrap-defaults does in tbd.core.
(def handler
(wrap-params (ring/ring-handler (ring/router (math/routes)))))
[muuntaja.core :as m]
[tbd.core :as core]))
;; Coercion/validation is configured on the router in tbd.core, so go through the real app.
(defn add [query]
(let [resp (handler {:request-method :get :uri "/api/add" :query-string query})]
(let [resp (core/app {:request-method :get :uri "/api/add" :query-string query})]
(-> resp
(update :body edn/read-string))))
@@ -27,4 +23,19 @@
(doseq [query [nil "x=2" "y=3" "x=abc&y=3" "x=2&y=abc"]]
(let [resp (add query)]
(is (= 400 (:status resp)) (str "query: " query))
(is (= "application/edn; charset=utf-8" (get-in resp [:headers "Content-Type"])))
(is (contains? (:body resp) :error))))))
(deftest add-route-swagger-test
(let [spec (-> (core/app {:request-method :get :uri "/swagger.json"
:headers {"accept" "application/json"}})
:body
(->> (m/decode "application/json")))
params (->> (:paths spec)
(some (fn [[path v]] (when (= "/api/add" (str (namespace path) "/" (name path))) v)))
:get
:parameters)]
(testing "x and y are documented as required numeric query params"
(is (= [{:name "x" :in "query" :type "number" :required true}
{:name "y" :in "query" :type "number" :required true}]
(map #(select-keys % [:name :in :type :required]) params))))))