Package list comidi-clojure / e82130b
New upstream version 0.3.1 Apollon Oikonomopoulos 4 years ago
10 changed file(s) with 1137 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 pom.xml
1 pom.xml.asc
2 *jar
3 /lib/
4 /classes/
5 /target/
6 /checkouts/
7 .lein-deps-sum
8 .lein-repl-history
9 .lein-plugins/
10 .lein-failures
11 .nrepl-port
0 language: clojure
1 lein: lein2
2 jdk:
3 - oraclejdk7
4 - openjdk7
5 script: ./ext/travisci/test.sh
6 notifications:
7 email: false
8 hipchat:
9 rooms:
10 - qiD4Yo5b7i1tz3kUW0ttsqoH0a1ZYVFihzThWtPT@322656
11 template:
12 - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}"
13 - "Change view: %{compare_url}"
14 - "Build details: %{build_url}"
0 ## 0.3.1
1
2 This is a minor bugfix release.
3
4 * [TK-297](https://tickets.puppetlabs.com/browse/TK-297) : Fix bug where `wrap-routes` didn't work properly with certain kinds of bidi route trees - Michal Růžička <michal.ruza@gmail.com> (159ef0f)
5 * Update dependencies on clojure, bidi, compojure, schema
6
7 ## 0.3.0
8
9 Added mechanism for wrapping intermediate comidi routes with middleware.
10 Added support for "true/false" Bidi patterns.
11
12 ## 0.2.2
13
14 Same content as 0.3.0 - please prefer that version
15
16 ## 0.2.1
17
18 Added 'resources' route utility fn.
19 Routes can now only map to Ring handlers.
20
21 ## 0.2.0
22
23 Make use of schema introduced in Bidi 1.20.0 (SERVER-777).
24 LICENSE and CONTRIBUTING updates.
25
26 ## 0.1.3
27
28 Improved dependency specification.
29
30 ## 0.1.2
31
32 Upgrade compojure to 1.3.3 for Clojure 1.7.0 compatibility.
33
34 ## 0.1.1
35
36 Make repository public, deploy to clojars.
37
38 ## 0.1.0
39
40 Initial release, with the goal of soliticing API feedback.
0 # How to contribute
1
2 * Make sure you have a [GitHub account](https://github.com/signup/free)
3 * Fork the repository on GitHub
4
5 ## Making Changes
6
7 * Create a topic branch from where you want to base your work (this is almost
8 definitely the master branch).
9 * To quickly create a topic branch based on master; `git branch
10 fix/master/my_contribution master` then checkout the new branch with `git
11 checkout fix/master/my_contribution`.
12 * Please avoid working directly on the
13 `master` branch.
14 * Make commits of logical units.
15 * Check for unnecessary whitespace with `git diff --check` before committing.
16 * Make sure your commit messages are in the proper format.
17
18 ````
19 Make the example in CONTRIBUTING imperative and concrete
20
21 Without this patch applied the example commit message in the CONTRIBUTING
22 document is not a concrete example. This is a problem because the
23 contributor is left to imagine what the commit message should look like
24 based on a description rather than an example. This patch fixes the
25 problem by making the example concrete and imperative.
26
27 The first line is a real life imperative statement with a ticket number
28 from our issue tracker. The body describes the behavior without the patch,
29 why this is a problem, and how the patch fixes the problem when applied.
30 ````
31
32 * Make sure you have added the necessary tests for your changes.
33 * Run _all_ the tests to assure nothing else was accidentally broken.
34
35 ## Submitting Changes
36
37 * Sign the [Contributor License Agreement](http://links.puppetlabs.com/cla).
38 * Push your changes to a topic branch in your fork of the repository.
39 * Submit a pull request to the repository in the puppetlabs organization.
40
41 # Additional Resources
42
43 * [Contributor License Agreement](http://links.puppetlabs.com/cla)
44 * [General GitHub documentation](http://help.github.com/)
45 * [GitHub pull request documentation](http://help.github.com/send-pull-requests/)
46
0 Copyright (C) 2005-2015 Puppet Labs Inc
1
2 Puppet Labs can be contacted at: info@puppetlabs.com
3
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
0 # comidi
1
2 A committee approach to defining Clojure HTTP routes.
3
4 [![Build Status](https://travis-ci.org/puppetlabs/comidi.svg?branch=master)](https://travis-ci.org/puppetlabs/comidi)
5
6 Comidi is a library containing utility functions and [compojure](https://github.com/weavejester/compojure)-like syntax-sugar
7 wrappers around the [bidi](https://github.com/juxt/bidi) web routing library.
8 It aims to provide a way to define your web routes that takes advantage of the
9 strengths of both bidi and compojure:
10
11 * Route definitions are, at the end of the day, simple data structures (like bidi),
12 so you can compose / introspect them.
13 * Helper functions / macros for defining routes still provide the nice syntax
14 of compojure; destructuring the request in a simple binding form, 'rendering'
15 the response whether you define it as a string/map literal, a function reference,
16 an inline body form, etc.
17
18 ## Quick Start
19
20 [![Clojars Project](http://clojars.org/puppetlabs/comidi/latest-version.svg)](http://clojars.org/puppetlabs/comidi)
21
22 ```clj
23 (let [my-routes (context "/my-app/"
24 (routes
25 (GET "/foo/something" request
26 "foo!")
27 (POST ["/bar/" :bar] [bar]
28 (str "bar:" bar))
29 (PUT ["/baz/" [#".*" :rest]] request
30 (call-baz-fn request))
31 (ANY ["/bam/" [#"(bip|bap)" :rest]] request
32 {:orig-req request
33 :rest (-> request :route-params :rest)))
34 app (-> (routes->handler my-routes)
35 wrap-with-my-middleware)]
36 (add-ring-handler app))
37 ```
38
39 Notable differences from compojure above:
40
41 * use vectors to separate segments of a route, rather than using special syntax
42 inside of a string (e.g. `["/bar/" :bar]` instead of compojure's `"/bar/:bar")
43 * use a nested vector with a regex (e.g. `[".*" :rest]`) to match a regex
44 * `context` macro does not provide a binding form for request vars like compojure's
45 does.
46
47 Other than those differences, the API should be very close to compojure's.
48
49 You can apply a Ring middlware to all the handlers at the leaves of a route using ```wrap-routes```, which has behaviour analagous to its counterpart in compojure. Multiple middlewares can be applied to the same routes, with those applied later wrapped around those applied earlier. This allows you to create multiple routes wrapped with different middleware yet still combine them into one overarching route that can be introspected.
50
51 ```clj
52 (let [my-routes ...
53 my-singly-wrapped-routes (wrap-routes my-routes inner-middleware)
54 my-doubly-wrapped-routes (wrap-routes my-singly-wrapped-routes outer-middleware)
55 my-other-routes ...
56 my-wrapped-other-routes (wrap-routes my-other-routes other-middleware)
57 my-combined-routes (routes my-doubly-wrapped-routes my-wrapped-other-routes)]
58 ```
59
60 ## What does Comidi do?
61
62 Comidi provides some macros and functions that are intended to feel very similar
63 to the compojure routing macros / functions, but under the hood they construct,
64 compose, and return bidi route trees rather than compojure handler functions.
65
66 This way, you can define your routes with almost exactly the same syntax you've
67 been using (or port over a compojure app with minimal effort), but end up with
68 an introspectable route tree data structure that you can do all sorts of cool
69 things with before you wrap it as a ring handler.
70
71 Under the hood: comidi uses bidi to do all of the work for routing, and uses
72 a few functions from compojure to maintain some of the nice syntax. Specifically,
73 it uses compojure's route destructuring to bind local variables for parameters
74 from the requests, and it uses compojure's "rendering" functions to allow you
75 to define the implementation of your route flexibly (so, just like in compojure,
76 your route definition can be a literal return value, a reference to a function,
77 a call to a function, a String, etc.)
78
79 Comidi also provides a function called `route-metadata`. This function
80 walks over your route tree and generates a metadata structure that gives you
81 information about the all of the routes; e.g.:
82
83 ```clj
84 (clojure.pprint/pprint
85 (-> (route-metadata (routes
86 (GET "/foo" request
87 "foo!")
88 (PUT ["/bar/" :bar] [bar]
89 (str "bar: " bar))))
90 :routes))
91 ```
92
93 ```
94 [{:route-id "foo", :path ["" "/foo"], :request-method :get}
95 {:route-id "bar-:bar", :path ["" "/bar/" :bar], :request-method :put}]
96 ```
97
98 Comidi also provides its own middleware function, `wrap-with-route-metadata`. If
99 you use this middleware, your ring request map will be supplemented with two
100 extra keys: `:route-metadata`, which gives you access to the metadata for all of
101 the routes in your route tree, and `:route-info`, which tells you which of those
102 routes the request matches. e.g.:
103
104 ```clj
105 (clojure.pprint/pprint
106 (let [my-routes (routes
107 (ANY "/foo" request
108 {:route-info (:route-info request)}))
109 handler (-> my-routes
110 routes->handler
111 (wrap-with-route-metadata my-routes))]
112 (:route-info (handler {:uri "/foo"}))))
113 ```
114
115 ```clj
116 {:route-id "foo", :path ["" "/foo"], :request-method :any}
117 ```
118
119 ## Trapperkeeper / Metrics Integration
120
121 The [`trapperkeeper-comidi-metrics`](https://github.com/puppetlabs/trapperkeeper-comidi-metrics) contains some middleware that will automatically generate and track [metrics](https://github.com/dropwizard/metrics) for all of the routes in your comidi/bidi route tree, as well as easy integration into [`trapperkeeper`](https://github.com/puppetlabs/trapperkeeper).
122
123 ## What's next?
124
125 * API docs: looking into swagger integration. I could swear I found some bidi-swagger
126 bindings somewhere a while back, but am not finding them at the moment. It
127 might be possible to re-use some of the code from `compojure-api` because of
128 the similarity between the comidi API and the compojure API.
129
130 * You tell me! This is pre-1.0 and the API should still be considered fungible.
131 If there's something you need that this library isn't doing, we can probably
132 do it. Ping us or submit a PR.
133
134 ## Support
135
136 We use the
137 [Trapperkeeper project on JIRA](https://tickets.puppetlabs.com/browse/TK)
138 for tickets on Comidi, although Github issues are welcome too.
0 #!/bin/bash
1
2 lein2 test :all
0 (defproject puppetlabs/comidi "0.3.1"
1 :description "Puppet Labs utility functions and compojure-like wrappers for use with the bidi web routing library"
2 :url "https://github.com/puppetlabs/comidi"
3
4 :pedantic? :abort
5
6 :dependencies [[org.clojure/clojure "1.7.0"]
7
8 ;; begin version conflict resolution dependencies
9 [clj-time "0.10.0"]
10 ;; end version conflict resolution dependencies
11
12 [bidi "1.23.1" :exclusions [org.clojure/clojurescript]]
13 [compojure "1.4.0"]
14 [prismatic/schema "1.0.4"]
15
16 [puppetlabs/kitchensink "1.1.0"]]
17
18 :deploy-repositories [["releases" {:url "https://clojars.org/repo"
19 :username :env/clojars_jenkins_username
20 :password :env/clojars_jenkins_password
21 :sign-releases false}]])
0 (ns puppetlabs.comidi
1 (:require [bidi.ring :as bidi-ring]
2 [bidi.schema :as bidi-schema]
3 [bidi.bidi :as bidi]
4 [clojure.zip :as zip]
5 [compojure.core :as compojure]
6 [compojure.response :as compojure-response]
7 [ring.util.mime-type :as mime]
8 [ring.util.response :as ring-response]
9 [schema.core :as schema]
10 [puppetlabs.kitchensink.core :as ks]
11 [clojure.string :as str])
12 (:import (java.util.regex Pattern)))
13
14 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
15 ;;; Schemas
16
17 (defn pattern?
18 [x]
19 (instance? Pattern x))
20
21 (def Zipper
22 (schema/pred ks/zipper?))
23
24 (def RequestMethod
25 (schema/enum :any :get :post :put :delete :head :options))
26
27 ; Derived from bidi-schema PatternSegment
28 (def RegexPatternSegment
29 (schema/pair schema/Regex "qual" schema/Keyword "id"))
30
31 (def RouteInfo
32 {:path [bidi-schema/PatternSegment]
33 :request-method RequestMethod})
34
35 (def RouteInfoWithId
36 (merge RouteInfo
37 {:route-id schema/Str}))
38
39 (def Handler
40 (schema/conditional
41 keyword? schema/Keyword
42 fn? (schema/pred fn?)
43 map? {RequestMethod (schema/recursive #'Handler)}))
44
45 (def RouteMetadata
46 {:routes [RouteInfoWithId]
47 :handlers {Handler RouteInfoWithId}})
48
49 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
50 ;;; Private - route id computation
51
52 (defn slashes->dashes
53 "Convert all forward slashes to hyphens"
54 [s]
55 (str/replace s #"\/" "-"))
56
57 (defn remove-leading-and-trailing-dashes
58 [s]
59 (-> s
60 (str/replace #"^-" "")
61 (str/replace #"-$" "")))
62
63 (defn special-chars->underscores
64 "Convert all non-alpha chars except ! * and - to underscores"
65 [s]
66 (str/replace s #"[^\w\!\*\-]" "_"))
67
68 (defn collapse-consecutive-underscores
69 [s]
70 (str/replace s #"_+" "_"))
71
72 (defn remove-leading-and-trailing-underscores
73 [s]
74 (-> s
75 (str/replace #"^_" "")
76 (str/replace #"_$" "")))
77
78 (defn add-regex-symbols
79 "Wrap a regex pattern with forward slashes to make it easier to recognize as a regex"
80 [s]
81 (str "/" s "/"))
82
83 (schema/defn ^:always-validate
84 path-element->route-id-element :- schema/Str
85 "Given a String path element from comidi route metadata, convert it into a string
86 suitable for use in building a route id string."
87 [path-element :- schema/Str]
88 (-> path-element
89 slashes->dashes
90 remove-leading-and-trailing-dashes
91 special-chars->underscores
92 collapse-consecutive-underscores
93 remove-leading-and-trailing-underscores))
94
95 (schema/defn ^:always-validate
96 regex-path-element->route-id-element :- schema/Str
97 "Given a Regex path element from comidi route metadata, convert it into a string
98 suitable for use in building a route id string."
99 [path-element :- RegexPatternSegment]
100 (-> path-element
101 first
102 str
103 path-element->route-id-element
104 add-regex-symbols))
105
106 (schema/defn ^:always-validate
107 route-path-element->route-id-element :- schema/Str
108 "Given a route path element from comidi route metadata, convert it into a string
109 suitable for use in building a route id string. This function is mostly
110 responsible for determining the type of the path element and dispatching to
111 the appropriate function."
112 [path-element]
113 (cond
114 (string? path-element)
115 (path-element->route-id-element path-element)
116
117 (keyword? path-element)
118 (pr-str path-element)
119
120 (nil? (schema/check RegexPatternSegment path-element))
121 (regex-path-element->route-id-element path-element)
122
123 :else
124 (throw (IllegalStateException. (str "Unrecognized path element: " path-element)))))
125
126 (schema/defn ^:always-validate
127 route-path->route-id :- schema/Str
128 "Given a route path (from comidi route-metadata), build a route-id string for
129 the route. This route-id can be used as a unique identifier for a route."
130 [route-path :- [bidi-schema/PatternSegment]]
131 (->> route-path
132 (map route-path-element->route-id-element)
133 (filter #(not (empty? %)))
134 (str/join "-")))
135
136 (schema/defn ^:always-validate
137 add-route-name :- RouteInfoWithId
138 "Given a RouteInfo, compute a route-id and return a RouteInfoWithId."
139 [route-info :- RouteInfo]
140 (assoc route-info :route-id (route-path->route-id (:path route-info))))
141
142 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
143 ;;; Private - route metadata computation
144
145 (def http-methods
146 #{:any :get :post :put :delete :head :options})
147
148 (schema/defn ^:always-validate
149 update-route-info* :- RouteInfo
150 "Helper function, used to maintain a RouteInfo data structure that represents
151 the current path elements of a route as we traverse the Bidi route tree via
152 zipper."
153 [route-info :- RouteInfo
154 pattern :- bidi-schema/Pattern]
155 (cond
156 (contains? http-methods pattern)
157 (assoc-in route-info [:request-method] pattern)
158
159 (nil? (schema/check RegexPatternSegment pattern))
160 (update-in route-info [:path] concat [pattern])
161
162 (= true pattern)
163 (update-in route-info [:path] conj "*")
164
165 (= false pattern)
166 (update-in route-info [:path] conj "!")
167
168 (sequential? pattern)
169 (if-let [next (first pattern)]
170 (update-route-info*
171 (update-in route-info [:path] conj next)
172 (rest pattern))
173 route-info)
174
175 :else
176 (update-in route-info [:path] conj pattern)))
177
178 (declare breadth-route-metadata*)
179
180 (schema/defn ^:always-validate
181 depth-route-metadata* :- RouteMetadata
182 "Helper function used to traverse branches of the Bidi route tree, depth-first."
183 [route-meta :- RouteMetadata
184 route-info :- RouteInfo
185 loc :- Zipper]
186 (let [[pattern matched] (zip/node loc)]
187 (cond
188 (map? matched)
189 (depth-route-metadata*
190 route-meta
191 route-info
192 (-> loc zip/down zip/right (zip/edit #(into [] %)) zip/up))
193
194 (vector? matched)
195 (breadth-route-metadata*
196 route-meta
197 (update-route-info* route-info pattern)
198 (-> loc zip/down zip/right zip/down))
199
200 :else
201 (let [route-info (-> (update-route-info* route-info pattern)
202 add-route-name)]
203 (-> route-meta
204 (update-in [:routes] conj route-info)
205 (assoc-in [:handlers matched] route-info))))))
206
207 (schema/defn ^:always-validate
208 breadth-route-metadata* :- RouteMetadata
209 "Helper function used to traverse branches of the Bidi route tree, breadth-first."
210 [route-meta :- RouteMetadata
211 route-info :- RouteInfo
212 loc :- Zipper]
213 (loop [route-meta route-meta
214 loc loc]
215 (let [routes (depth-route-metadata* route-meta route-info loc)]
216 (if-let [next (zip/right loc)]
217 (recur routes next)
218 routes))))
219
220 (schema/defn ^:always-validate
221 route-metadata* :- RouteMetadata
222 "Traverses a Bidi route tree and returns route metadata, which includes a list
223 of RouteInfo objects (one per route), plus a mechanism to look up the
224 RouteInfo for a given handler."
225 [routes :- bidi-schema/RoutePair]
226 (let [route-info {:path []
227 :request-method :any}
228 loc (-> [routes] zip/vector-zip zip/down)]
229 (breadth-route-metadata* {:routes []
230 :handlers {}} route-info loc)))
231
232 (def memoized-route-metadata*
233 (memoize route-metadata*))
234
235 (defn make-handler
236 "Create a Ring handler from the route definition data
237 structure. Matches a handler from the uri in the request, and invokes
238 it with the request as a parameter. (This code is largely copied from the
239 bidi upstream, but we add support for inserting the match-context via
240 middleware.)"
241 [route]
242 (fn [{:keys [uri path-info] :as req}]
243 (let [path (or path-info uri)
244 {:keys [handler route-params] :as match-context}
245 (or (:match-context req)
246 (apply bidi/match-route route path (apply concat (seq req))))]
247 (when handler
248 (bidi-ring/request
249 handler
250 (-> req
251 (update-in [:params] merge route-params)
252 (update-in [:route-params] merge route-params))
253 (apply dissoc match-context :handler (keys req)))))))
254
255 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
256 ;;; Private - helpers for compojure-like syntax
257
258 (defn- add-mime-type [response path options]
259 (if-let [mime-type (mime/ext-mime-type path (:mime-types options {}))]
260 (ring-response/content-type response mime-type)
261 response))
262
263 (defmacro handler-fn*
264 "Helper macro, used by the compojure-like macros (GET/POST/etc.) to generate
265 a function that provides compojure's destructuring and rendering support."
266 [bindings body]
267 `(fn [request#]
268 (compojure-response/render
269 (compojure/let-request [~bindings request#] ~@body)
270 request#)))
271
272 (defn route-with-method*
273 "Helper function, used by the compojure-like macros (GET/POST/etc.) to generate
274 a bidi route that includes a wrapped handler function."
275 [method pattern bindings body]
276 `[~pattern {~method (handler-fn* ~bindings ~body)}])
277
278 (defn route-tree-zip
279 "Returns a zipper for a bidi route tree i.e. for an arbitrarily nested structure of
280 `bidi.schema/RoutePair`s"
281 [root]
282 (zip/zipper
283 (fn [[_ matched]]
284 (or (vector? matched) (map? matched)))
285
286 (fn [[_ matched]]
287 (seq matched))
288
289 (fn [[pattern matched :as node] children]
290 (with-meta
291 [pattern (into (if (vector? matched) [] {}) children)]
292 (meta node)))
293
294 root))
295
296 (defn wrap-routes*
297 "Help function, used by compojure-like wrap-routes function to wrap leaf handlers
298 in the bidi route with the middleware"
299 [loc middleware]
300 (if (zip/end? loc)
301 loc
302 (let [loc (if (zip/branch? loc) ; we only want modify the leaf nodes
303 loc
304 (let [[pattern matched] (zip/node loc)]
305 (if (fn? matched)
306 (zip/replace loc [pattern (middleware matched)])
307 loc)))]
308 (recur (zip/next loc) middleware))))
309
310 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
311 ;;;; Public - core functions
312
313 (schema/defn ^:always-validate
314 route-metadata :- RouteMetadata
315 "Build up a map of metadata describing the routes. This metadata map can be
316 used for introspecting the routes after building the handler, and can also
317 be used with the `wrap-with-route-metadata` middleware."
318 [routes :- bidi-schema/RoutePair]
319 (memoized-route-metadata* routes))
320
321 (schema/defn ^:always-validate
322 wrap-with-route-metadata :- (schema/pred fn?)
323 "Ring middleware; adds the comidi route-metadata to the request map, as well
324 as a :route-info key that can be used to determine which route a given request
325 matches."
326 [app :- (schema/pred fn?)
327 routes :- bidi-schema/RoutePair]
328 (let [compiled-routes (bidi/compile-route routes)
329 route-meta (route-metadata routes)]
330 (fn [{:keys [uri path-info] :as req}]
331 (let [path (or path-info uri)
332 {:keys [handler] :as match-context}
333 (apply bidi/match-route compiled-routes path (apply concat (seq req)))
334 route-info (get-in route-meta [:handlers handler])]
335 (app (assoc req
336 :route-metadata route-meta
337 :route-info route-info
338 :match-context match-context))))))
339
340 (schema/defn ^:always-validate
341 wrap-routes :- bidi-schema/RoutePair
342 "Wraps middleware around the handlers at every leaf in the route in a manner
343 analagous to compojure's wrap-routes function"
344 [routes :- bidi-schema/RoutePair
345 middleware :- (schema/pred fn?)]
346 (-> routes
347 route-tree-zip
348 (wrap-routes* middleware)
349 zip/root))
350
351 (schema/defn ^:always-validate
352 routes :- bidi-schema/RoutePair
353 "Combines multiple bidi routes into a single data structure; this is largely
354 just a convenience function for grouping several routes together as a single
355 object that can be passed around."
356 [& routes :- [bidi-schema/RoutePair]]
357 ["" (vec routes)])
358
359 (schema/defn ^:always-validate
360 context :- bidi-schema/RoutePair
361 "Combines multiple bidi routes together into a single data structure, but nests
362 them all under the given url-prefix. This is similar to compojure's `context`
363 macro, but does not accept a binding form. You can still destructure variables
364 by passing a bidi pattern for `url-prefix`, and the variables will be available
365 to all nested routes."
366 [url-prefix :- bidi-schema/Pattern
367 & routes :- [bidi-schema/RoutePair]]
368 [url-prefix (vec routes)])
369
370 (schema/defn ^:always-validate
371 routes->handler :- (schema/pred fn?)
372 "Given a bidi route tree, converts into a ring request handler function"
373 [routes :- bidi-schema/RoutePair]
374 (let [compiled-routes (bidi/compile-route routes)]
375 (make-handler compiled-routes)))
376
377 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
378 ;;; Public - compojure-like convenience macros
379
380 (defmacro ANY
381 [pattern bindings & body]
382 `[~pattern (handler-fn* ~bindings ~body)])
383
384 (defmacro GET
385 [pattern bindings & body]
386 (route-with-method* :get pattern bindings body))
387
388 (defmacro HEAD
389 [pattern bindings & body]
390 (route-with-method* :head pattern bindings body))
391
392 (defmacro PUT
393 [pattern bindings & body]
394 (route-with-method* :put pattern bindings body))
395
396 (defmacro POST
397 [pattern bindings & body]
398 (route-with-method* :post pattern bindings body))
399
400 (defmacro DELETE
401 [pattern bindings & body]
402 (route-with-method* :delete pattern bindings body))
403
404 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
405 ;;; Public - pre-built routes
406
407 (defn not-found
408 [body]
409 [[[#".*" :rest]] (fn [request]
410 (-> (compojure-response/render body request)
411 (ring-response/status 404)))])
412
413 (defn resources
414 "A route for serving resources on the classpath. Accepts the following
415 keys:
416 :root - the root prefix path of the resources, defaults to 'public'
417 :mime-types - an optional map of file extensions to mime types"
418 [path & [options]]
419 (GET [path [#".*" :resource-path]] [resource-path]
420 (let [root (:root options "public")]
421 (some-> (ring-response/resource-response (str root "/" resource-path))
422 (add-mime-type resource-path options)))))
0 (ns puppetlabs.comidi-test
1 (require [clojure.test :refer :all]
2 [puppetlabs.comidi :as comidi :refer :all]
3 [schema.test :as schema-test]
4 [schema.core :as schema]
5 [clojure.zip :as zip]
6 [bidi.bidi :as bidi]))
7
8 (use-fixtures :once schema-test/validate-schemas)
9
10 (defn replace-regexes-for-equality-check
11 [xs]
12 (loop [loc (zip/vector-zip xs)]
13 (if (zip/end? loc)
14 (zip/root loc)
15 (recur
16 (let [node (zip/node loc)]
17 (if (pattern? node)
18 (zip/edit loc #(str "REGEX: " (.pattern %)))
19 (zip/next loc)))))))
20
21 (deftest update-route-info-test
22 (let [orig-route-info {:path []
23 :request-method :any}]
24 (testing "HTTP verb keyword causes request-method to be updated"
25 (doseq [verb [:get :post :put :delete :head]]
26 (is (= {:path []
27 :request-method verb}
28 (update-route-info* orig-route-info verb)))))
29 (testing "string path gets added to the path"
30 (is (= {:path ["/foo"]
31 :request-method :any}
32 (update-route-info* orig-route-info "/foo"))))
33 (testing "string path elements get added to the path"
34 (is (= {:path ["/foo"]
35 :request-method :any}
36 (update-route-info* orig-route-info ["/foo"]))))
37 (testing "keyword path elements get added to the path"
38 (is (= {:path [:foo]
39 :request-method :any}
40 (update-route-info* orig-route-info [:foo]))))
41 (testing "vector path elements get flattened and added to the path"
42 (is (= {:path ["/foo/" :foo]
43 :request-method :any}
44 (update-route-info* orig-route-info ["/foo/" :foo]))))
45 (testing "boolean true is handled specially"
46 (is (= {:path ["*"]
47 :request-method :any}
48 (update-route-info* orig-route-info true))))
49 (testing "boolean false is handled specially"
50 (is (= {:path ["!"]
51 :request-method :any}
52 (update-route-info* orig-route-info false))))
53 (testing "regex path element gets added to the path"
54 (is (= {:path ["/foo/" ["REGEX: .*" :foo]]
55 :request-method :any}
56 (-> (update-route-info* orig-route-info ["/foo/" [#".*" :foo]])
57 (update-in [:path] replace-regexes-for-equality-check)))))))
58
59 (deftest route-metadata-test
60 (testing "route metadata includes ordered list of routes and lookup by handler"
61 (let [routes ["" [[["/foo/" :foo] :foo-handler]
62 [["/bar/" :bar]
63 [["/baz" {:get :baz-handler}]
64 ["/bam" {:put :bam-handler}]
65 ["/boop" :boop-handler]
66 ["/bap" {:options :bap-handler}]]]
67 ["/buzz" {:post :buzz-handler}]
68 [true {:get :true-handler}]]]
69 expected-foo-meta {:path ["" "/foo/" :foo]
70 :route-id "foo-:foo"
71 :request-method :any}
72 expected-baz-meta {:path ["" "/bar/" :bar "/baz"]
73 :route-id "bar-:bar-baz"
74 :request-method :get}
75 expected-bam-meta {:path ["" "/bar/" :bar "/bam"]
76 :route-id "bar-:bar-bam"
77 :request-method :put}
78 expected-boop-meta {:path ["" "/bar/" :bar "/boop"]
79 :route-id "bar-:bar-boop"
80 :request-method :any}
81 expected-bap-meta {:path ["" "/bar/" :bar "/bap"]
82 :route-id "bar-:bar-bap"
83 :request-method :options}
84 expected-buzz-meta {:path ["" "/buzz"]
85 :route-id "buzz"
86 :request-method :post}
87 expected-true-meta {:path ["" "*"]
88 :route-id "*"
89 :request-method :get}]
90 (is (= (comidi/route-metadata* routes)
91 {:routes [expected-foo-meta
92 expected-baz-meta
93 expected-bam-meta
94 expected-boop-meta
95 expected-bap-meta
96 expected-buzz-meta
97 expected-true-meta]
98 :handlers {:foo-handler expected-foo-meta
99 :baz-handler expected-baz-meta
100 :bam-handler expected-bam-meta
101 :boop-handler expected-boop-meta
102 :bap-handler expected-bap-meta
103 :buzz-handler expected-buzz-meta
104 :true-handler expected-true-meta}})))))
105
106 (deftest routes-test
107 (is (= ["" [["/foo" :foo-handler]
108 [["/bar/" :bar] :bar-handler]]]
109 (routes ["/foo" :foo-handler]
110 [["/bar/" :bar] :bar-handler]))))
111
112 (deftest context-test
113 (testing "simple context"
114 (is (= ["/foo" [["/bar" :bar-handler]
115 [["/baz" :baz] :baz-handler]]]
116 (context "/foo"
117 ["/bar" :bar-handler]
118 [["/baz" :baz] :baz-handler]))))
119 (testing "context with variable"
120 (is (= [["/foo" :foo] [["/bar" :bar-handler]
121 [["/baz" :baz] :baz-handler]]]
122 (context ["/foo" :foo]
123 ["/bar" :bar-handler]
124 [["/baz" :baz] :baz-handler])))))
125
126 (deftest routes->handler-test
127 (testing "routes are matched against a request properly, with route params"
128 (let [handler (routes->handler ["/foo"
129 [[""
130 [["/bar"
131 (fn [req] :bar)]
132 [["/baz/" :baz]
133 (fn [req]
134 {:endpoint :baz
135 :route-params (:route-params req)})]
136 [true
137 (fn [req] :true)]]]]])]
138 (is (= :true (handler {:uri "/foo/something/else"})))
139 (is (= :bar (handler {:uri "/foo/bar"})))
140 (is (= {:endpoint :baz
141 :route-params {:baz "howdy"}}
142 (handler {:uri "/foo/baz/howdy"})))))
143 (testing "request-methods are honored"
144 (let [handler (routes->handler ["/foo" {:get (fn [req] :foo)}])]
145 (is (nil? (handler {:uri "/foo"})))
146 (is (= :foo (handler {:uri "/foo" :request-method :get})))))
147 (testing "contexts can bind route variables"
148 (let [handler (routes->handler
149 (context ["/foo/" :foo]
150 [["/bar/" :bar]
151 (fn [req] (:route-params req))]))]
152 (is (= {:foo "hi"
153 :bar "there"}
154 (handler {:uri "/foo/hi/bar/there"}))))))
155
156 (deftest compojure-macros-test
157 (let [routes (context ["/foo/" :foo]
158 (ANY ["/any/" :any] [foo any]
159 (str "foo: " foo " any: " any))
160 (GET ["/get/" :get] [foo get]
161 (fn [req] {:foo foo
162 :get get}))
163 (HEAD ["/head/" :head] [foo head]
164 {:foo foo
165 :head head})
166 (PUT "/put" [foo]
167 {:status 500
168 :body foo})
169 (POST ["/post/" :post] [post]
170 post)
171 (DELETE ["/delete/" :delete] [foo delete]
172 (atom {:foo foo
173 :delete delete})))
174 handler (routes->handler routes)]
175 (is (nil? (handler {:uri "/foo/hi/get/there" :request-method :post})))
176 (is (nil? (handler {:uri "/foo/hi/head/there" :request-method :get})))
177 (is (nil? (handler {:uri "/foo/hi/put" :request-method :get})))
178 (is (nil? (handler {:uri "/foo/hi/post/there" :request-method :get})))
179 (is (nil? (handler {:uri "/foo/hi/delete/there" :request-method :get})))
180
181 (is (= "foo: hi any: there" (:body (handler {:uri "/foo/hi/any/there"}))))
182 (is (= {:foo "hi"
183 :get "there"}
184 (select-keys
185 (handler {:uri "/foo/hi/get/there" :request-method :get})
186 [:foo :get])))
187 (is (= {:foo "hi"
188 :head "there"}
189 (select-keys
190 (handler {:uri "/foo/hi/head/there" :request-method :head})
191 [:foo :head])))
192 (is (= {:status 500
193 :body "hi"}
194 (select-keys
195 (handler {:uri "/foo/hi/put" :request-method :put})
196 [:status :body])))
197 (is (= {:status 200
198 :body "there"}
199 (select-keys
200 (handler {:uri "/foo/hi/post/there" :request-method :post})
201 [:status :body])))
202 (is (= {:status 200
203 :foo "hi"
204 :delete "there"}
205 (select-keys
206 (handler {:uri "/foo/hi/delete/there" :request-method :delete})
207 [:status :foo :delete])))))
208
209 (deftest not-found-test
210 (testing "root not-found handler"
211 (let [handler (routes->handler (not-found "nobody's home, yo"))]
212 (is (= {:status 404
213 :body "nobody's home, yo"}
214 (select-keys
215 (handler {:uri "/hi/there"})
216 [:body :status])))))
217 (testing "nested not-found handler"
218 (let [handler (routes->handler
219 (routes
220 ["/bar" [["" (fn [req] :bar)]
221 (not-found "nothing else under bar!")]]
222 (not-found "nothing else under root!")))]
223 (is (= :bar (handler {:uri "/bar"})))
224 (is (= {:status 404
225 :body "nothing else under bar!"}
226 (select-keys
227 (handler {:uri "/bar/baz"})
228 [:status :body])))
229 (is (= {:status 404
230 :body "nothing else under root!"}
231 (select-keys
232 (handler {:uri "/yo/mang"})
233 [:status :body]))))))
234
235 (deftest regex-test
236 (let [handler (routes->handler
237 ["/foo" [[["/boo/" [#".*" :rest]]
238 (fn [req] (:rest (:route-params req)))]]])]
239 (is (= "hi/there"
240 (handler {:uri "/foo/boo/hi/there"})))))
241
242 (deftest route-names-test
243 (let [test-routes (routes
244 ; Bidi Pattern: Path
245 (GET "/foo" request
246 "foo!")
247 (GET ["/foo/something/"] request
248 "foo something!")
249 ; Bidi Pattern: [ PatternSegment+ ]
250 (POST ["/bar"] request
251 "bar!")
252 (POST ["/bar" "bie"] request
253 "barbie!")
254 (PUT ["/baz" [#".*" :rest]] request
255 "baz!")
256 (ANY ["/bam/" [#"(?:bip|bap)" :rest]] request
257 "bam!")
258 (HEAD ["/bang/" [#".*" :rest] "/pow/" :pow] request
259 "bang!")
260 ; Bidi Pattern: false
261 (GET false request
262 "catch none!")
263 (GET "/false" request
264 "omg why would you do this?")
265 (GET ["/is" :false] request
266 "it hurts so bad")
267 ; Bidi Pattern: true
268 (GET true request
269 "catch all!")
270 (GET "/true" request
271 "omg why would you do this?")
272 (GET ["/is" :true] request
273 "it hurts so bad"))
274 route-meta (route-metadata test-routes)
275 route-ids (map :route-id (:routes route-meta))]
276 (is (= #{"foo"
277 "foo-something"
278 "bar"
279 "bar-bie"
280 "baz-/*/"
281 "bam-/bip_bap/"
282 "bang-/*/-pow-:pow"
283 "*"
284 "true"
285 "is-:true"
286 "!"
287 "false"
288 "is-:false"}
289 (set route-ids)))))
290
291 (deftest wrap-with-route-metadata-test
292 (let [test-routes (routes
293 (ANY ["/foo/" :foo] request
294 "foo!")
295 (GET ["/bar/" :bar] request
296 "bar!")
297 (GET "/false" request "falsefalse!")
298 (GET false request "truefalse!")
299 (GET "/true" request "falsetrue!")
300 (GET true request "truetrue!"))
301 route-meta (route-metadata test-routes)
302 test-atom (atom {})
303 test-middleware (fn [f]
304 (fn [req]
305 (reset! test-atom (select-keys req [:route-info :route-metadata]))
306 (f req)))
307 handler (-> (routes->handler test-routes)
308 test-middleware
309 (wrap-with-route-metadata test-routes))]
310 (handler {:uri "/foo/something"})
311 (is (= (-> test-atom deref :route-info :route-id) "foo-:foo"))
312 (is (= (-> test-atom deref :route-metadata) route-meta))
313
314 (handler {:uri "/bar/something" :request-method :get})
315 (is (= (-> test-atom deref :route-info :route-id) "bar-:bar"))
316 (is (= (-> test-atom deref :route-metadata) route-meta))
317
318 (handler {:uri "/false" :request-method :get})
319 (is (= (-> test-atom deref :route-info :route-id) "false"))
320 (is (= (-> test-atom deref :route-metadata) route-meta))
321
322 (handler {:uri "/true" :request-method :get})
323 (is (= (-> test-atom deref :route-info :route-id) "true"))
324 (is (= (-> test-atom deref :route-metadata) route-meta))
325
326 (handler {:uri "/wat" :request-method :get})
327 (is (= (-> test-atom deref :route-info :route-id) "*"))
328 (is (= (-> test-atom deref :route-metadata) route-meta))))
329
330 (deftest route-handler-uses-existing-match-context-test
331 (testing "Middleware can provide match-context for comidi handler"
332 (let [routes (routes
333 (GET ["/foo-:foo"] request
334 "foo!"))
335 fake-match-context {:handler (fn [req] (-> req :route-params :bar))
336 :route-params {:bar "bar!"}}
337 wrap-with-fake-match-context (fn [app]
338 (fn [req]
339 (let [req (assoc req :match-context fake-match-context)]
340 (app req))))
341 handler (-> routes
342 routes->handler
343 wrap-with-fake-match-context)]
344 (is (= "bar!" (handler {:uri "/bunk"}))))))
345
346 (deftest wrap-leaves-with-middleware-test
347 (let [inner-middleware (fn [handler]
348 (fn [request]
349 (update-in (handler request) [:body] #(str "inner-" %))))
350 bb-wrapper-middleware (fn [handler]
351 (fn [request]
352 (update-in (handler request) [:body] #(str "bb-wrapper-" %))))
353 outer-middleware (fn [handler]
354 (fn [request]
355 (update-in (handler request) [:body] #(str "outer-" %))))
356 aa-route (GET "/aa" request "aa!")
357 bb-route (ANY "/bb" request "bb!")
358 cc-route (ANY "/cc" request "cc!")
359 dd-route (DELETE "/dd" request "dd!")
360 ee-route (GET "/ee" request "ee!")
361 ff-route (ANY "/ff" request "ff!")
362 gh-route (ANY (bidi/alts "/gg" "/hh") request "gg-or-hh!")
363 left-routes (context "/left" aa-route bb-route)
364 middle-routes (context "/middle" cc-route dd-route)
365 right-routes (context "/right" ee-route ff-route)
366 alternate-routes ["/alts" [gh-route]]
367 handler (-> (routes left-routes middle-routes right-routes alternate-routes) routes->handler)]
368 (testing "Routes without middleware applied"
369 (is (= (:body (handler {:uri "/left/aa" :request-method :get})) "aa!"))
370 (is (= (:body (handler {:uri "/left/bb" :request-method :post})) "bb!"))
371 (is (= (:body (handler {:uri "/middle/cc" :request-method :get})) "cc!"))
372 (is (= (:body (handler {:uri "/middle/dd" :request-method :delete})) "dd!"))
373 (is (= (:body (handler {:uri "/right/ee" :request-method :get})) "ee!"))
374 (is (= (:body (handler {:uri "/right/ff" :request-method :delete})) "ff!"))
375 (is (= (:body (handler {:uri "/alts/gg" :request-method :put})) "gg-or-hh!"))
376 (is (= (:body (handler {:uri "/alts/hh" :request-method :post})) "gg-or-hh!"))
377 (is (= (:body (handler {:uri "/alts/ii" :request-method :post})) nil)))
378 (testing "Routes but now with middleware applied"
379 (let [wrapped-bb-route (-> bb-route (wrap-routes bb-wrapper-middleware))
380 left-routes (-> (context "/left" aa-route wrapped-bb-route)
381 (wrap-routes inner-middleware)
382 (wrap-routes outer-middleware))
383 right-routes (-> right-routes (wrap-routes outer-middleware))
384 alternate-routes (-> alternate-routes (wrap-routes inner-middleware) (wrap-routes outer-middleware))
385 handler (-> (routes left-routes middle-routes right-routes alternate-routes) routes->handler)]
386 (is (= (:body (handler {:uri "/left/aa" :request-method :get})) "outer-inner-aa!"))
387 (is (= (:body (handler {:uri "/left/bb" :request-method :post})) "outer-inner-bb-wrapper-bb!"))
388 (is (= (:body (handler {:uri "/middle/cc" :request-method :get})) "cc!"))
389 (is (= (:body (handler {:uri "/middle/dd" :request-method :delete})) "dd!"))
390 (is (= (:body (handler {:uri "/right/ee" :request-method :get})) "outer-ee!"))
391 (is (= (:body (handler {:uri "/right/ff" :request-method :delete})) "outer-ff!"))
392 (is (= (:body (handler {:uri "/alts/gg" :request-method :delete})) "outer-inner-gg-or-hh!"))
393 (is (= (:body (handler {:uri "/alts/hh" :request-method :delete})) "outer-inner-gg-or-hh!"))))))
394
395 (deftest destructuring-test
396 (testing "Compojure-style destructuring works as expected"
397 (let [test-request {:uri "/foo"
398 :params {:aa "aa"
399 :bb "bb"}}]
400 (testing "A single binding outside a vector is the whole request"
401 ((routes->handler (ANY "/foo" request
402 (is (= test-request (select-keys request [:uri :params])))
403 "foo!")) test-request))
404 (testing "A vector binding called 'request' tries to bind to non-existent query param of the same name"
405 ((routes->handler (ANY "/foo" [request]
406 (is (nil? request))
407 "foo!")) test-request))
408 (testing "A vector binding with two params binds as expected"
409 ((routes->handler (ANY "/foo" [aa bb]
410 (is (= aa "aa"))
411 (is (= bb "bb"))
412 "foo!")) test-request))
413 (testing "A vector binding with two valid params, one invalid param and an ':as request' segment binds as expected"
414 ((routes->handler (ANY "/foo" [aa bb cc :as request]
415 (is (= aa "aa"))
416 (is (= bb "bb"))
417 (is (nil? cc))
418 (is (= test-request (select-keys request [:uri :params])))
419 "foo!")) test-request)))))