New upstream version 0.3.1
Apollon Oikonomopoulos
6 years ago
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 | [](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 | [](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 | (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))))) |