Skip to content

Commit 2fbb9bd

Browse files
committed
Merge pull request metosin#204 from metosin/route-validation-options
Route-validation as an api-option
2 parents 063b9fb + 2902e69 commit 2fbb9bd

File tree

10 files changed

+126
-61
lines changed

10 files changed

+126
-61
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929

3030
* **BREAKING** Vanilla Compojure routes will not produce any swagger-docs (as they do not satisfy the
3131
`Routing` protocol. They can still be used for handling request, just without docs.
32-
* **TODO**: a new api-level option to declare how to handle routes not satisfying the `Routing` protocol (fail, warn or ignore)
32+
* a new api-level option `[:api :invalid-routes-fn]` to declare how to handle routes not satisfying
33+
the `Routing` protocol. Default implementation logs invalid routes as WARNINGs.
3334

3435
* **BREAKING** compojure.core imports are removed from `compojure.api.sweet`:
3536
* `let-request`, `routing`, `wrap-routes`

src/compojure/api/api.clj

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,46 @@
55
[compojure.api.routes :as routes]
66
[compojure.api.common :as common]
77
[clojure.tools.macro :as macro]
8-
[ring.swagger.swagger2 :as swagger2]))
8+
[ring.swagger.common :as rsc]))
9+
10+
(def api-defaults
11+
(merge
12+
middleware/api-middleware-defaults
13+
{:api {:invalid-routes-fn routes/log-invalid-child-routes}}))
914

1015
(defn
1116
^{:doc (str
1217
"Returns a ring handler wrapped in compojure.api.middleware/api-middlware.
13-
Creates the route-table at run-time and passes that into the request via
14-
ring-swagger middlewares. The mounted api-middleware can be configured by
18+
Creates the route-table at run-time and injects that into the request via
19+
middlewares. Api and the mounted api-middleware can be configured by
1520
optional options map as the first parameter:
1621
1722
(api
1823
{:formats [:json :edn]}
1924
(context \"/api\" []
2025
...))
2126
22-
API middleware options:
27+
### direct api options:
28+
29+
- **:api** All api options are under `:api`.
30+
- **:invalid-routes-fn** A 2-arity function taking handler and a sequence of
31+
invalid routes (not satisfying compojure.api.route.Routing)
32+
setting value to nil ignores invalid routes completely.
33+
defaults to `compojure.api.routes/log-invalid-child-routes`
34+
35+
### api-middleware options
2336
2437
" (:doc (meta #'compojure.api.middleware/api-middleware)))}
2538
api
2639
[& body]
2740
(let [[options handlers] (common/extract-parameters body)
41+
options (rsc/deep-merge api-defaults options)
2842
handler (apply core/routes handlers)
29-
paths (swagger/ring-swagger-paths handler)
30-
lookup (routes/route-lookup-table handler)
43+
routes (routes/get-routes handler (:api options))
44+
paths (-> routes routes/ring-swagger-paths swagger/transform-operations)
45+
lookup (routes/route-lookup-table routes)
3146
api-handler (-> handler
32-
(middleware/api-middleware options)
33-
;; TODO: wrap just the handler
47+
(middleware/api-middleware (dissoc options :api))
3448
(middleware/wrap-options {:paths paths
3549
:lookup lookup}))]
3650
(routes/create nil nil {} [handler] api-handler)))

src/compojure/api/common.clj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,9 @@
3434

3535
:else
3636
[{} (seq c)]))
37+
38+
(defn group-with
39+
"Groups a sequence with predicate returning a tuple of sequences."
40+
[pred coll]
41+
[(seq (filter pred coll))
42+
(seq (remove pred coll))])

src/compojure/api/routes.clj

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[cheshire.core :as json]
55
[compojure.api.middleware :as mw]
66
[compojure.api.impl.logging :as logging]
7+
[compojure.api.common :as common]
78
[ring.swagger.common :as rsc]
89
[clojure.string :as str]
910
[linked.core :as linked]
@@ -14,29 +15,40 @@
1415
;; Route records
1516
;;
1617

17-
(def ^:dynamic *fail-on-missing-route-info* false)
18-
1918
(defn- ->path [path]
2019
(if-not (= path "/") path))
2120

2221
(defn- ->paths [p1 p2]
2322
(->path (str p1 (->path p2))))
2423

2524
(defprotocol Routing
26-
(get-routes [handler]))
25+
(-get-routes [handler options]))
2726

2827
(extend-protocol Routing
2928
nil
30-
(get-routes [_] []))
29+
(-get-routes [_ _] []))
30+
31+
(defn filter-routes [{:keys [childs] :as handler} {:keys [invalid-routes-fn]}]
32+
(let [[valid-childs invalid-childs] (common/group-with (partial satisfies? Routing) childs)]
33+
(when (and invalid-routes-fn invalid-childs)
34+
(invalid-routes-fn handler invalid-childs))
35+
valid-childs))
36+
37+
(defn get-routes
38+
([handler]
39+
(get-routes handler nil))
40+
([handler options]
41+
(-get-routes handler options)))
3142

3243
(defrecord Route [path method info childs handler]
3344
Routing
34-
(get-routes [_]
35-
(if (seq childs)
36-
(vec
37-
(for [[p m i] (mapcat get-routes (filter (partial satisfies? Routing) childs))]
38-
[(->paths path p) m (rsc/deep-merge info i)]))
39-
(into [] (if path [[path method info]]))))
45+
(-get-routes [this options]
46+
(let [valid-childs (filter-routes this options)]
47+
(if (seq childs)
48+
(vec
49+
(for [[p m i] (mapcat #(get-routes % options) valid-childs)]
50+
[(->paths path p) m (rsc/deep-merge info i)]))
51+
(into [] (if path [[path method info]])))))
4052

4153
IFn
4254
(invoke [_ request]
@@ -45,18 +57,23 @@
4557
(AFn/applyToHelper this args)))
4658

4759
(defn create [path method info childs handler]
48-
(when-let [invalid-childs (seq (remove (partial satisfies? Routing) childs))]
49-
(let [message "Not all child routes satisfy compojure.api.routing/Routing."
50-
data {:path path
51-
:method method
52-
:info info
53-
:childs childs
54-
:invalid invalid-childs}]
55-
(if *fail-on-missing-route-info*
56-
(throw (ex-info message data))
57-
(logging/log! :warn (str message ": " data)))))
5860
(->Route path method info childs handler))
5961

62+
;;
63+
;; Invalid route handlers
64+
;;
65+
66+
(defn fail-on-invalid-child-routes
67+
[handler invalid-childs]
68+
(throw (ex-info "Not all child routes satisfy compojure.api.routing/Routing."
69+
(merge (select-keys handler [:path :method]) {:invalid (vec invalid-childs)}))))
70+
71+
(defn log-invalid-child-routes [handler invalid-childs]
72+
(logging/log! :warn (str "Not all child routes satisfy compojure.api.routing/Routing. "
73+
(select-keys handler [:path :method]) ", invalid child routes: "
74+
(vec invalid-childs))))
75+
76+
6077
;;
6178
;; Swagger paths
6279
;;
@@ -77,17 +94,17 @@
7794
(update-in info [:parameters :path] #(dissoc (merge (string-path-parameters path) %) s/Keyword))
7895
info))
7996

80-
(defn ring-swagger-paths [handler]
97+
(defn ring-swagger-paths [routes]
8198
{:paths
8299
(reduce
83-
(fn [acc [path method info]]
84-
(update-in
85-
acc [path method]
86-
(fn [old-info]
87-
(let [info (or old-info info)]
88-
(ensure-path-parameters path info)))))
89-
(linked/map)
90-
(get-routes handler))})
100+
(fn [acc [path method info]]
101+
(update-in
102+
acc [path method]
103+
(fn [old-info]
104+
(let [info (or old-info info)]
105+
(ensure-path-parameters path info)))))
106+
(linked/map)
107+
routes)})
91108

92109
;;
93110
;; Route lookup
@@ -97,8 +114,8 @@
97114
(for [[id freq] (frequencies seq)
98115
:when (> freq 1)] id))
99116

100-
(defn route-lookup-table [handler]
101-
(let [entries (for [[path endpoints] (:paths (ring-swagger-paths handler))
117+
(defn route-lookup-table [routes]
118+
(let [entries (for [[path endpoints] (-> routes ring-swagger-paths :paths)
102119
[method {:keys [x-name parameters]}] endpoints
103120
:let [params (:path parameters)]
104121
:when x-name]

src/compojure/api/swagger.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,8 @@
3737
;; routes
3838
;;
3939

40-
(defn ring-swagger-paths [handler]
41-
(->> handler
42-
routes/ring-swagger-paths
40+
(defn transform-operations [swagger]
41+
(->> swagger
4342
(swagger2/transform-operations routes/non-nil-routes)
4443
(swagger2/transform-operations routes/strip-no-doc-endpoints)))
4544

@@ -91,6 +90,7 @@
9190

9291
(defn swagger-spec-path [app]
9392
(some-> app
93+
routes/get-routes
9494
routes/route-lookup-table
9595
::swagger
9696
keys

test/compojure/api/common_test.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,7 @@
55
(fact "map-of"
66
(let [a 1 b true c [:abba :jabba]]
77
(map-of a b c) => {:a 1 :b true :c [:abba :jabba]}))
8+
9+
(fact "group-with"
10+
(group-with pos? [1 -10 2 -4 -1 999]) => [[1 2 999] [-10 -4 -1]]
11+
(group-with pos? [1 2 999]) => [[1 2 999] nil])

test/compojure/api/integration_test.clj

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
[ring.swagger.core :as rsc]
1010
[ring.util.http-status :as status]
1111
[compojure.api.middleware :as mw]
12-
[ring.swagger.middleware :as rsm]))
12+
[ring.swagger.middleware :as rsm]
13+
[compojure.api.routes :as routes]))
1314

1415
;;
1516
;; Data
@@ -1106,3 +1107,19 @@
11061107
(let [[status body] (get* app "/ping")]
11071108
status => 200
11081109
body => 1))))
1110+
1111+
(fact "handling invalid routes with api"
1112+
(let [invalid-routes (routes (constantly nil))]
1113+
1114+
(fact "by default, logs the exception"
1115+
(api invalid-routes) => truthy
1116+
(provided
1117+
(compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 1))
1118+
1119+
(fact "ignoring invalid routes doesn't log"
1120+
(api {:api {:invalid-routes-fn nil}} invalid-routes) => truthy
1121+
(provided
1122+
(compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 0))
1123+
1124+
(fact "throwing exceptions"
1125+
(api {:api {:invalid-routes-fn routes/fail-on-invalid-child-routes}} invalid-routes)) => throws))

test/compojure/api/routes_test.clj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,19 @@
8686
(routes/get-routes (routes (routes))) => []
8787
(routes/get-routes (routes (swagger-ui))) => []
8888
(routes/get-routes (routes (routes (GET "/ping" [] "pong")))) => [["/ping" :get {}]])
89+
90+
(fact "invalid route options"
91+
(let [r (routes (constantly nil))]
92+
93+
(fact "ignore 'em all"
94+
(routes/get-routes r) => []
95+
(routes/get-routes r nil) => []
96+
(routes/get-routes r {:invalid-routes-fn nil}) => [])
97+
98+
(fact "log warnings"
99+
(routes/get-routes r {:invalid-routes-fn routes/log-invalid-child-routes}) => []
100+
(provided
101+
(compojure.api.impl.logging/log! :warn irrelevant) => irrelevant :times 1))
102+
103+
(fact "throw exception"
104+
(routes/get-routes r {:invalid-routes-fn routes/fail-on-invalid-child-routes})) => throws))

test/compojure/api/swagger_test.clj

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,12 @@
5555
(GET+ "/true" [] identity))) => {"/api/xxx/true" {:get {}}})
5656

5757
(fact "Vanilla Compojure defroutes are NOT followed"
58-
(ignore-non-documented-route-warning
59-
(compojure.core/defroutes even-more-routes (GET "/even" [] identity))
60-
(compojure.core/defroutes more-routes (context "/more" [] even-more-routes))
61-
(extract-paths
62-
(context "/api" []
63-
(GET "/true" [] identity)
64-
more-routes)) => {"/api/true" {:get {}}}))
58+
(compojure.core/defroutes even-more-routes (GET "/even" [] identity))
59+
(compojure.core/defroutes more-routes (context "/more" [] even-more-routes))
60+
(extract-paths
61+
(context "/api" []
62+
(GET "/true" [] identity)
63+
more-routes)) => {"/api/true" {:get {}}})
6564

6665
(fact "Compojure Api defroutes and def routes are followed"
6766
(def even-more-routes (GET "/even" [] identity))

test/compojure/api/test_utils.clj

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,21 +80,12 @@
8080
(let [[status body] (raw-post* app uri "" nil headers)]
8181
[status (parse-body body)]))
8282

83-
;;
84-
;; Route compilation
85-
;;
86-
87-
(defmacro ignore-non-documented-route-warning [& body]
88-
`(with-out-str
89-
(binding [routes/*fail-on-missing-route-info* false]
90-
~@body)))
91-
9283
;;
9384
;; get-spec
9485
;;
9586

9687
(defn extract-paths [app]
97-
(:paths (routes/ring-swagger-paths app)))
88+
(-> app routes/get-routes routes/ring-swagger-paths :paths))
9889

9990
(defn get-spec [app]
10091
(let [[status spec] (get* app "/swagger.json" {})]

0 commit comments

Comments
 (0)