Package list comidi-clojure / 41e28a8
(PE-8647) Refactor to support upcoming metrics work This commit introduces some refactoring that is intended to make it easier to capture metrics on routes in the future. The main changes are: * Add a unique identifier, `:route-id` for each route, in route metadata. * Create a separate middleware function for matching the route and adding the match info into the request map, so that other middleware can have access to the match information before the request is processed. Chris Price 6 years ago
2 changed file(s) with 256 addition(s) and 120 deletion(s). Raw diff Collapse all Expand all
55 [compojure.response :as compojure-response]
66 [ring.util.response :as ring-response]
77 [schema.core :as schema]
8 [puppetlabs.kitchensink.core :as ks])
8 [puppetlabs.kitchensink.core :as ks]
9 [clojure.string :as str])
910 (:import (java.util.regex Pattern)))
1011
1112 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
3738 {:path [PathElement]
3839 :request-method RequestMethod})
3940
41 (def RouteInfoWithId
42 (merge RouteInfo
43 {:route-id schema/Str}))
44
4045 (def Handler
4146 (schema/conditional
4247 keyword? schema/Keyword
4449 map? {RequestMethod (schema/recursive #'Handler)}))
4550
4651 (def RouteMetadata
47 {:routes [RouteInfo]
48 :handlers {Handler RouteInfo}})
52 {:routes [RouteInfoWithId]
53 :handlers {Handler RouteInfoWithId}})
4954
5055 (def BidiPattern
5156 (schema/conditional
6368 [(schema/one BidiPattern "pattern")
6469 (schema/one BidiRouteDestination "destination")])
6570
66
67 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
68 ;;; Private
69
70 (defmacro handler-fn*
71 "Helper macro, used by the compojure-like macros (GET/POST/etc.) to generate
72 a function that provides compojure's destructuring and rendering support."
73 [bindings body]
74 `(fn [request#]
75 (compojure-response/render
76 (compojure/let-request [~bindings request#] ~@body)
77 request#)))
78
79 (defn route-with-method*
80 "Helper function, used by the compojure-like macros (GET/POST/etc.) to generate
81 a bidi route that includes a wrapped handler function."
82 [method pattern bindings body]
83 `[~pattern {~method (handler-fn* ~bindings ~body)}])
71 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
72 ;;; Private - route id computation
73
74 (defn slashes->dashes
75 "Convert all forward slashes to hyphens"
76 [s]
77 (str/replace s #"\/" "-"))
78
79 (defn remove-leading-and-trailing-dashes
80 [s]
81 (-> s
82 (str/replace #"^-" "")
83 (str/replace #"-$" "")))
84
85 (defn special-chars->underscores
86 "Convert all non-alpha chars except * and - to underscores"
87 [s]
88 (str/replace s #"[^\w\*\-]" "_"))
89
90 (defn collapse-consecutive-underscores
91 [s]
92 (str/replace s #"_+" "_"))
93
94 (defn remove-leading-and-trailing-underscores
95 [s]
96 (-> s
97 (str/replace #"^_" "")
98 (str/replace #"_$" "")))
99
100 (defn add-regex-symbols
101 "Wrap a regex pattern with forward slashes to make it easier to recognize as a regex"
102 [s]
103 (str "/" s "/"))
104
105 (schema/defn ^:always-validate
106 path-element->route-id-element :- schema/Str
107 "Given a String path element from comidi route metadata, convert it into a string
108 suitable for use in building a route id string."
109 [path-element :- schema/Str]
110 (-> path-element
111 slashes->dashes
112 remove-leading-and-trailing-dashes
113 special-chars->underscores
114 collapse-consecutive-underscores
115 remove-leading-and-trailing-underscores))
116
117 (schema/defn ^:always-validate
118 regex-path-element->route-id-element :- schema/Str
119 "Given a Regex path element from comidi route metadata, convert it into a string
120 suitable for use in building a route id string."
121 [path-element :- RegexPathElement]
122 (-> path-element
123 first
124 str
125 path-element->route-id-element
126 add-regex-symbols))
127
128 (schema/defn ^:always-validate
129 route-path-element->route-id-element :- schema/Str
130 "Given a route path element from comidi route metadata, convert it into a string
131 suitable for use in building a route id string. This function is mostly
132 responsible for determining the type of the path element and dispatching to
133 the appropriate function."
134 [path-element :- PathElement]
135 (cond
136 (string? path-element)
137 (path-element->route-id-element path-element)
138
139 (keyword? path-element)
140 (pr-str path-element)
141
142 (nil? (schema/check RegexPathElement path-element))
143 (regex-path-element->route-id-element path-element)
144
145 :else
146 (throw (IllegalStateException. (str "Unrecognized path element: " path-element)))))
147
148 (schema/defn ^:always-validate
149 route-path->route-id :- schema/Str
150 "Given a route path (from comidi route-metadata), build a route-id string for
151 the route. This route-id can be used as a unique identifier for a route."
152 [route-path :- BidiPattern]
153 (->> route-path
154 (map route-path-element->route-id-element)
155 (filter #(not (empty? %)))
156 (str/join "-")))
157
158 (schema/defn ^:always-validate
159 add-route-name :- RouteInfoWithId
160 "Given a RouteInfo, compute a route-id and return a RouteInfoWithId."
161 [route-info :- RouteInfo]
162 (assoc route-info :route-id (route-path->route-id (:path route-info))))
84163
85164 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
86165 ;;; Private - route metadata computation
132211 (-> loc zip/down zip/right zip/down))
133212
134213 :else
135 (let [route-info (update-route-info* route-info pattern)]
214 (let [route-info (-> (update-route-info* route-info pattern)
215 add-route-name)]
136216 (-> route-meta
137217 (update-in [:routes] conj route-info)
138218 (assoc-in [:handlers matched] route-info))))))
151231 routes))))
152232
153233 (schema/defn ^:always-validate
154 route-metadata :- RouteMetadata
234 route-metadata* :- RouteMetadata
155235 "Traverses a Bidi route tree and returns route metadata, which includes a list
156236 of RouteInfo objects (one per route), plus a mechanism to look up the
157237 RouteInfo for a given handler."
158238 [routes :- BidiRoute]
159 (let [route-info {:path []
239 (let [route-info {:path []
160240 :request-method :any}
161 loc (-> [routes] zip/vector-zip zip/down)]
162 (breadth-route-metadata* {:routes []
241 loc (-> [routes] zip/vector-zip zip/down)]
242 (breadth-route-metadata* {:routes []
163243 :handlers {}} route-info loc)))
164244
165 (schema/defn ^:always-validate
166 make-handler :- (schema/pred fn?)
167 "Create a Ring handler from the route definition data structure. (This code
168 is largely borrowed from bidi core.) Arguments:
169
170 - route-meta: metadata about the routes; allows us to look up the route info
171 by handler. You can get this by calling `route-metadata`.
172 - routes: the Bidi route tree
173 - handler-fn: this fn will be called on all of the handlers found in the bidi
174 route tree; it is expected to return a ring handler fn for that
175 route. If you are using the compojure-like macros in this
176 namespace or have nested your ring handler functions in the bidi
177 tree by other means, you can just pass `identity` here, or pass
178 in some middleware fn to wrap around the nested ring handlers.
179 The handlers will have access to the `RouteInfo` of the matching
180 bidi route via the `:route-info` key in the request map."
181 [route-meta :- RouteMetadata
182 routes :- BidiRoute
183 handler-fn :- (schema/pred fn?)]
184 (let [compiled-routes (bidi/compile-route routes)]
245 (def memoized-route-metadata*
246 (memoize route-metadata*))
247
248 (defn make-handler
249 "Create a Ring handler from the route definition data
250 structure. Matches a handler from the uri in the request, and invokes
251 it with the request as a parameter. (This code is largely copied from the
252 bidi upstream, but we add support for inserting the match-context via
253 middleware.)"
254 ([route handler-fn]
255 (fn [{:keys [uri path-info] :as req}]
256 (let [path (or path-info uri)
257 {:keys [handler route-params] :as match-context}
258 (or (:match-context req)
259 (apply bidi/match-route route path (apply concat (seq req))))]
260 (when handler
261 (bidi-ring/request
262 (handler-fn handler)
263 (-> req
264 (update-in [:params] merge route-params)
265 (update-in [:route-params] merge route-params))
266 (apply dissoc match-context :handler (keys req))
267 )))))
268 ([route] (make-handler route identity)))
269
270 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
271 ;;; Private - helpers for compojure-like syntax
272
273 (defmacro handler-fn*
274 "Helper macro, used by the compojure-like macros (GET/POST/etc.) to generate
275 a function that provides compojure's destructuring and rendering support."
276 [bindings body]
277 `(fn [request#]
278 (compojure-response/render
279 (compojure/let-request [~bindings request#] ~@body)
280 request#)))
281
282 (defn route-with-method*
283 "Helper function, used by the compojure-like macros (GET/POST/etc.) to generate
284 a bidi route that includes a wrapped handler function."
285 [method pattern bindings body]
286 `[~pattern {~method (handler-fn* ~bindings ~body)}])
287
288 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
289 ;;;; Public - core functions
290
291 (schema/defn ^:always-validate
292 route-metadata :- RouteMetadata
293 "Build up a map of metadata describing the routes. This metadata map can be
294 used for introspecting the routes after building the handler, and can also
295 be used with the `wrap-with-route-metadata` middleware."
296 [routes :- BidiRoute]
297 (memoized-route-metadata* routes))
298
299 (schema/defn ^:always-validate
300 wrap-with-route-metadata :- (schema/pred fn?)
301 "Ring middleware; adds the comidi route-metadata to the request map, as well
302 as a :route-info key that can be used to determine which route a given request
303 matches."
304 [app :- (schema/pred fn?)
305 routes :- BidiRoute]
306 (let [compiled-routes (bidi/compile-route routes)
307 route-meta (route-metadata routes)]
185308 (fn [{:keys [uri path-info] :as req}]
186309 (let [path (or path-info uri)
187 {:keys [handler route-params] :as match-context}
188 (apply bidi/match-route compiled-routes path (apply concat (seq req)))]
189 (when handler
190 (let [req (-> req
191 (update-in [:params] merge route-params)
192 (update-in [:route-params] merge route-params)
193 (assoc-in [:route-info] (get-in route-meta
194 [:handlers handler])))]
195 (bidi-ring/request
196 (handler-fn handler)
197 req
198 (apply dissoc match-context :handler (keys req)))))))))
199
200
201 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
202 ;;;; Public - core functions
310 {:keys [handler] :as match-context}
311 (apply bidi/match-route compiled-routes path (apply concat (seq req)))
312 route-info (get-in route-meta [:handlers handler])]
313 (app (assoc req
314 :route-metadata route-meta
315 :route-info route-info
316 :match-context match-context))))))
203317
204318 (schema/defn ^:always-validate
205319 routes :- BidiRoute
223337 (schema/defn ^:always-validate
224338 routes->handler :- (schema/pred fn?)
225339 "Given a bidi route tree, converts into a ring request handler function. You
226 may pass an optional middleware function that will be wrapped around the
227 request handling; the middleware fn will have access to the `RouteInfo` of the
228 matching bidi route via the `:route-info` key in the request map."
340 may pass an optional handler function which will be wrapped around the
341 bidi leaf."
229342 ([routes :- BidiRoute
230 route-middleware-fn :- (schema/maybe (schema/pred fn?))]
231 (let [route-meta (route-metadata routes)]
232 (with-meta
233 (make-handler route-meta
234 routes
235 route-middleware-fn)
236 {:route-metadata route-meta})))
343 handler-fn :- (schema/maybe (schema/pred fn?))]
344 (let [compiled-routes (bidi/compile-route routes)]
345 (make-handler compiled-routes handler-fn)))
237346 ([routes]
238347 (routes->handler routes identity)))
239
240 (schema/defn ^:always-validate
241 context-handler :- (schema/pred fn?)
242 "Convenience function that effectively composes `context` and `routes->handler`."
243 [url-prefix :- BidiPattern
244 & routes :- [BidiRoute]]
245 (routes->handler
246 (apply context url-prefix routes)))
247
248348
249349 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
250350 ;;; Public - compojure-like convenience macros
279379 (defn not-found
280380 [body]
281381 [[[#".*" :rest]] (fn [request]
282 (-> (compojure-response/render body request)
283 (ring-response/status 404)))])
382 (-> (compojure-response/render body request)
383 (ring-response/status 404)))])
00 (ns puppetlabs.comidi-test
11 (require [clojure.test :refer :all]
2 [puppetlabs.comidi :as comidi]
2 [puppetlabs.comidi :as comidi :refer :all]
33 [schema.test :as schema-test]
4 [puppetlabs.comidi :refer :all]
54 [schema.core :as schema]
65 [clojure.zip :as zip]))
76
9089 ["/bam" {:put :bam-handler}]
9190 ["/bap" {:any :bap-handler}]]]
9291 ["/buzz" {:post :buzz-handler}]]]
93 expected-foo-meta {:path '("" "/foo/" :foo)
92 expected-foo-meta {:path ["" "/foo/" :foo]
93 :route-id "foo-:foo"
9494 :request-method :any}
95 expected-baz-meta {:path '("" "/bar/" :bar "/baz")
95 expected-baz-meta {:path ["" "/bar/" :bar "/baz"]
96 :route-id "bar-:bar-baz"
9697 :request-method :get}
97 expected-bam-meta {:path '("" "/bar/" :bar "/bam")
98 expected-bam-meta {:path ["" "/bar/" :bar "/bam"]
99 :route-id "bar-:bar-bam"
98100 :request-method :put}
99 expected-bap-meta {:path '("" "/bar/" :bar "/bap")
101 expected-bap-meta {:path ["" "/bar/" :bar "/bap"]
102 :route-id "bar-:bar-bap"
100103 :request-method :any}
101 expected-buzz-meta {:path '("" "/buzz")
104 expected-buzz-meta {:path ["" "/buzz"]
105 :route-id "buzz"
102106 :request-method :post}]
103107 (is (= (comidi/route-metadata routes)
104108 {:routes [expected-foo-meta
157161 (fn [req] (:route-params req))]))]
158162 (is (= {:foo "hi"
159163 :bar "there"}
160 (handler {:uri "/foo/hi/bar/there"})))))
161 (testing "route metadata is added to fn metadata"
162 (let [foo-handler (fn [req] :foo)
163 handler (routes->handler ["/foo" {:get foo-handler}])]
164 (let [route-meta (:route-metadata (meta handler))]
165 (is (= {:routes [{:path ["/foo"]
166 :request-method :get}]
167 :handlers {foo-handler {:path ["/foo"]
168 :request-method :get}}}
169 route-meta))))))
170
171 (deftest routes->handler-middleware-test
164 (handler {:uri "/foo/hi/bar/there"}))))))
165
166 (deftest routes->handler-fn-test
172167 (let [handler (routes->handler
173168 (context ["/foo/" :foo]
174169 [["/bar/" :bar]
175170 (fn [req] (:route-params req))])
176171 (fn [f]
177172 (fn [req]
178 {:result (f req)
179 :route-info (:route-info req)})))]
180 (is (= {:result {:foo "hi"
181 :bar "there"}
182 :route-info {:path ["/foo/" :foo "/bar/" :bar]
183 :request-method :any}}
173 {:result (into {} (map (fn [[k v]] [k (str "wrapped " v)])
174 (f req)))})))]
175 (is (= {:result {:foo "wrapped hi"
176 :bar "wrapped there"}}
184177 (handler {:uri "/foo/hi/bar/there"})))))
185
186 (deftest context-handler-test
187 (let [handler (context-handler ["/foo/" :foo]
188 [["/bar/" :bar]
189 (fn [req] (:route-params req))])]
190 (is (= {:foo "hi"
191 :bar "there"}
192 (handler {:uri "/foo/hi/bar/there"})))))
193
194178
195179 (deftest compojure-macros-test
196180 (let [routes (context ["/foo/" :foo]
278262 (is (= "hi/there"
279263 (handler {:uri "/foo/boo/hi/there"})))))
280264
281
282
283
265 (deftest route-names-test
266 (let [test-routes (routes
267 (GET ["/foo/something/"] request
268 "foo!")
269 (POST ["/bar"] request
270 "bar!")
271 (PUT ["/baz" [#".*" :rest]] request
272 "baz!")
273 (ANY ["/bam/" [#"(?:bip|bap)" :rest]] request
274 "bam!")
275 (HEAD ["/bang/" [#".*" :rest] "/pow/" :pow] request
276 "bang!"))
277 route-meta (route-metadata test-routes)
278 route-ids (map :route-id (:routes route-meta))]
279 (is (= #{"foo-something" "bar" "baz-/*/" "bam-/bip_bap/" "bang-/*/-pow-:pow"}
280 (set route-ids)))))
281
282 (deftest wrap-with-route-metadata-test
283 (let [test-routes (routes
284 (ANY ["/foo/" :foo] request
285 "foo!")
286 (GET ["/bar/" :bar] request
287 "bar!"))
288 route-meta (route-metadata test-routes)
289 test-atom (atom {})
290 test-middleware (fn [f]
291 (fn [req]
292 (reset! test-atom (select-keys req [:route-info :route-metadata]))
293 (f req)))
294 handler (-> (routes->handler test-routes)
295 test-middleware
296 (wrap-with-route-metadata test-routes))]
297 (handler {:uri "/foo/something"})
298 (is (= (-> test-atom deref :route-info :route-id) "foo-:foo"))
299 (is (= (-> test-atom deref :route-metadata) route-meta))
300
301 (handler {:uri "/bar/something" :request-method :get})
302 (is (= (-> test-atom deref :route-info :route-id) "bar-:bar"))
303 (is (= (-> test-atom deref :route-metadata) route-meta))))
304
305 (deftest route-handler-uses-existing-match-context-test
306 (testing "Middleware can provide match-context for comidi handler"
307 (let [routes (routes
308 (GET ["/foo-:foo"] request
309 "foo!"))
310 fake-match-context {:handler (fn [req] (-> req :route-params :bar))
311 :route-params {:bar "bar!"}}
312 wrap-with-fake-match-context (fn [app]
313 (fn [req]
314 (let [req (assoc req :match-context fake-match-context)]
315 (app req))))
316 handler (-> routes
317 routes->handler
318 wrap-with-fake-match-context)]
319 (is (= "bar!" (handler {:uri "/bunk"}))))))