├── .gitignore ├── .travis.yml ├── LICENSE.html ├── README.md ├── project.clj ├── src └── clout │ ├── core.cljc │ └── internal │ └── regex.cljs └── test └── clout ├── core_test.cljc └── test_runner.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | script: 3 | - jdk_switcher use openjdk6 4 | - lein with-profile dev:dev,1.7:dev,1.8:dev,1.9 test 5 | - lein with-profile 1.7:1.8 doo node once 6 | - jdk_switcher use openjdk7 7 | - lein with-profile dev:dev,1.7:dev,1.8:dev,1.9 test 8 | - lein with-profile 1.7:1.8 doo node once 9 | - jdk_switcher use openjdk8 10 | - lein with-profile dev:dev,1.7:dev,1.8:dev,1.9 test 11 | - lein with-profile 1.7:1.8:1.9 doo node once 12 | -------------------------------------------------------------------------------- /LICENSE.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Eclipse Public License - Version 1.0 8 | 25 | 26 | 27 | 28 | 29 | 30 |

Eclipse Public License - v 1.0

31 | 32 |

THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 33 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR 34 | DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS 35 | AGREEMENT.

36 | 37 |

1. DEFINITIONS

38 | 39 |

"Contribution" means:

40 | 41 |

a) in the case of the initial Contributor, the initial 42 | code and documentation distributed under this Agreement, and

43 |

b) in the case of each subsequent Contributor:

44 |

i) changes to the Program, and

45 |

ii) additions to the Program;

46 |

where such changes and/or additions to the Program 47 | originate from and are distributed by that particular Contributor. A 48 | Contribution 'originates' from a Contributor if it was added to the 49 | Program by such Contributor itself or anyone acting on such 50 | Contributor's behalf. Contributions do not include additions to the 51 | Program which: (i) are separate modules of software distributed in 52 | conjunction with the Program under their own license agreement, and (ii) 53 | are not derivative works of the Program.

54 | 55 |

"Contributor" means any person or entity that distributes 56 | the Program.

57 | 58 |

"Licensed Patents" mean patent claims licensable by a 59 | Contributor which are necessarily infringed by the use or sale of its 60 | Contribution alone or when combined with the Program.

61 | 62 |

"Program" means the Contributions distributed in accordance 63 | with this Agreement.

64 | 65 |

"Recipient" means anyone who receives the Program under 66 | this Agreement, including all Contributors.

67 | 68 |

2. GRANT OF RIGHTS

69 | 70 |

a) Subject to the terms of this Agreement, each 71 | Contributor hereby grants Recipient a non-exclusive, worldwide, 72 | royalty-free copyright license to reproduce, prepare derivative works 73 | of, publicly display, publicly perform, distribute and sublicense the 74 | Contribution of such Contributor, if any, and such derivative works, in 75 | source code and object code form.

76 | 77 |

b) Subject to the terms of this Agreement, each 78 | Contributor hereby grants Recipient a non-exclusive, worldwide, 79 | royalty-free patent license under Licensed Patents to make, use, sell, 80 | offer to sell, import and otherwise transfer the Contribution of such 81 | Contributor, if any, in source code and object code form. This patent 82 | license shall apply to the combination of the Contribution and the 83 | Program if, at the time the Contribution is added by the Contributor, 84 | such addition of the Contribution causes such combination to be covered 85 | by the Licensed Patents. The patent license shall not apply to any other 86 | combinations which include the Contribution. No hardware per se is 87 | licensed hereunder.

88 | 89 |

c) Recipient understands that although each Contributor 90 | grants the licenses to its Contributions set forth herein, no assurances 91 | are provided by any Contributor that the Program does not infringe the 92 | patent or other intellectual property rights of any other entity. Each 93 | Contributor disclaims any liability to Recipient for claims brought by 94 | any other entity based on infringement of intellectual property rights 95 | or otherwise. As a condition to exercising the rights and licenses 96 | granted hereunder, each Recipient hereby assumes sole responsibility to 97 | secure any other intellectual property rights needed, if any. For 98 | example, if a third party patent license is required to allow Recipient 99 | to distribute the Program, it is Recipient's responsibility to acquire 100 | that license before distributing the Program.

101 | 102 |

d) Each Contributor represents that to its knowledge it 103 | has sufficient copyright rights in its Contribution, if any, to grant 104 | the copyright license set forth in this Agreement.

105 | 106 |

3. REQUIREMENTS

107 | 108 |

A Contributor may choose to distribute the Program in object code 109 | form under its own license agreement, provided that:

110 | 111 |

a) it complies with the terms and conditions of this 112 | Agreement; and

113 | 114 |

b) its license agreement:

115 | 116 |

i) effectively disclaims on behalf of all Contributors 117 | all warranties and conditions, express and implied, including warranties 118 | or conditions of title and non-infringement, and implied warranties or 119 | conditions of merchantability and fitness for a particular purpose;

120 | 121 |

ii) effectively excludes on behalf of all Contributors 122 | all liability for damages, including direct, indirect, special, 123 | incidental and consequential damages, such as lost profits;

124 | 125 |

iii) states that any provisions which differ from this 126 | Agreement are offered by that Contributor alone and not by any other 127 | party; and

128 | 129 |

iv) states that source code for the Program is available 130 | from such Contributor, and informs licensees how to obtain it in a 131 | reasonable manner on or through a medium customarily used for software 132 | exchange.

133 | 134 |

When the Program is made available in source code form:

135 | 136 |

a) it must be made available under this Agreement; and

137 | 138 |

b) a copy of this Agreement must be included with each 139 | copy of the Program.

140 | 141 |

Contributors may not remove or alter any copyright notices contained 142 | within the Program.

143 | 144 |

Each Contributor must identify itself as the originator of its 145 | Contribution, if any, in a manner that reasonably allows subsequent 146 | Recipients to identify the originator of the Contribution.

147 | 148 |

4. COMMERCIAL DISTRIBUTION

149 | 150 |

Commercial distributors of software may accept certain 151 | responsibilities with respect to end users, business partners and the 152 | like. While this license is intended to facilitate the commercial use of 153 | the Program, the Contributor who includes the Program in a commercial 154 | product offering should do so in a manner which does not create 155 | potential liability for other Contributors. Therefore, if a Contributor 156 | includes the Program in a commercial product offering, such Contributor 157 | ("Commercial Contributor") hereby agrees to defend and 158 | indemnify every other Contributor ("Indemnified Contributor") 159 | against any losses, damages and costs (collectively "Losses") 160 | arising from claims, lawsuits and other legal actions brought by a third 161 | party against the Indemnified Contributor to the extent caused by the 162 | acts or omissions of such Commercial Contributor in connection with its 163 | distribution of the Program in a commercial product offering. The 164 | obligations in this section do not apply to any claims or Losses 165 | relating to any actual or alleged intellectual property infringement. In 166 | order to qualify, an Indemnified Contributor must: a) promptly notify 167 | the Commercial Contributor in writing of such claim, and b) allow the 168 | Commercial Contributor to control, and cooperate with the Commercial 169 | Contributor in, the defense and any related settlement negotiations. The 170 | Indemnified Contributor may participate in any such claim at its own 171 | expense.

172 | 173 |

For example, a Contributor might include the Program in a commercial 174 | product offering, Product X. That Contributor is then a Commercial 175 | Contributor. If that Commercial Contributor then makes performance 176 | claims, or offers warranties related to Product X, those performance 177 | claims and warranties are such Commercial Contributor's responsibility 178 | alone. Under this section, the Commercial Contributor would have to 179 | defend claims against the other Contributors related to those 180 | performance claims and warranties, and if a court requires any other 181 | Contributor to pay any damages as a result, the Commercial Contributor 182 | must pay those damages.

183 | 184 |

5. NO WARRANTY

185 | 186 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS 187 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 188 | OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, 189 | ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY 190 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely 191 | responsible for determining the appropriateness of using and 192 | distributing the Program and assumes all risks associated with its 193 | exercise of rights under this Agreement , including but not limited to 194 | the risks and costs of program errors, compliance with applicable laws, 195 | damage to or loss of data, programs or equipment, and unavailability or 196 | interruption of operations.

197 | 198 |

6. DISCLAIMER OF LIABILITY

199 | 200 |

EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT 201 | NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, 202 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING 203 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF 204 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 205 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR 206 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED 207 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

208 | 209 |

7. GENERAL

210 | 211 |

If any provision of this Agreement is invalid or unenforceable under 212 | applicable law, it shall not affect the validity or enforceability of 213 | the remainder of the terms of this Agreement, and without further action 214 | by the parties hereto, such provision shall be reformed to the minimum 215 | extent necessary to make such provision valid and enforceable.

216 | 217 |

If Recipient institutes patent litigation against any entity 218 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 219 | Program itself (excluding combinations of the Program with other 220 | software or hardware) infringes such Recipient's patent(s), then such 221 | Recipient's rights granted under Section 2(b) shall terminate as of the 222 | date such litigation is filed.

223 | 224 |

All Recipient's rights under this Agreement shall terminate if it 225 | fails to comply with any of the material terms or conditions of this 226 | Agreement and does not cure such failure in a reasonable period of time 227 | after becoming aware of such noncompliance. If all Recipient's rights 228 | under this Agreement terminate, Recipient agrees to cease use and 229 | distribution of the Program as soon as reasonably practicable. However, 230 | Recipient's obligations under this Agreement and any licenses granted by 231 | Recipient relating to the Program shall continue and survive.

232 | 233 |

Everyone is permitted to copy and distribute copies of this 234 | Agreement, but in order to avoid inconsistency the Agreement is 235 | copyrighted and may only be modified in the following manner. The 236 | Agreement Steward reserves the right to publish new versions (including 237 | revisions) of this Agreement from time to time. No one other than the 238 | Agreement Steward has the right to modify this Agreement. The Eclipse 239 | Foundation is the initial Agreement Steward. The Eclipse Foundation may 240 | assign the responsibility to serve as the Agreement Steward to a 241 | suitable separate entity. Each new version of the Agreement will be 242 | given a distinguishing version number. The Program (including 243 | Contributions) may always be distributed subject to the version of the 244 | Agreement under which it was received. In addition, after a new version 245 | of the Agreement is published, Contributor may elect to distribute the 246 | Program (including its Contributions) under the new version. Except as 247 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives no 248 | rights or licenses to the intellectual property of any Contributor under 249 | this Agreement, whether expressly, by implication, estoppel or 250 | otherwise. All rights in the Program not expressly granted under this 251 | Agreement are reserved.

252 | 253 |

This Agreement is governed by the laws of the State of New York and 254 | the intellectual property laws of the United States of America. No party 255 | to this Agreement will bring a legal action under this Agreement more 256 | than one year after the cause of action arose. Each party waives its 257 | rights to a jury trial in any resulting litigation.

258 | 259 | 260 | 261 | 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clout 2 | 3 | [![Build Status](https://travis-ci.org/weavejester/clout.svg?branch=master)](https://travis-ci.org/weavejester/clout) 4 | 5 | Clout is a library for matching [Ring][1] HTTP requests. It uses the same 6 | routing syntax as used by popular Ruby web frameworks like Ruby on Rails and 7 | Sinatra. 8 | 9 | [1]: https://github.com/ring-clojure/ring 10 | 11 | ## Installation 12 | 13 | Add the following to your project.clj dependencies: 14 | 15 | ```clj 16 | [clout "2.2.1"] 17 | ``` 18 | 19 | ## Usage 20 | 21 | Require Clout in the normal way: 22 | 23 | ```clj 24 | (require '[clout.core :as clout]) 25 | ``` 26 | 27 | These following examples also make use of the [Ring-Mock][2] library 28 | to generate Ring request maps: 29 | 30 | [2]: https://github.com/ring-clojure/ring-mock 31 | 32 | ```clj 33 | (require '[ring.mock.request :as mock]) 34 | ``` 35 | 36 | Routes can match by keyword: 37 | 38 | ```clj 39 | (clout/route-matches 40 | "/article/:title" 41 | (mock/request :get "/article/clojure")) 42 | 43 | => {:title "clojure"} 44 | ``` 45 | 46 | Or with wildcards: 47 | 48 | ```clj 49 | (clout/route-matches 50 | "/public/*" 51 | (mock/request :get "/public/style/screen.css")) 52 | 53 | => {:* "style/screen.css"} 54 | ``` 55 | 56 | Clout can also match absolute routes: 57 | 58 | ```clj 59 | (clout/route-matches 60 | "http://subdomain.example.com/" 61 | (mock/request :get "http://subdomain.example.com/")) 62 | 63 | => {} 64 | ``` 65 | And scheme-relative routes: 66 | 67 | ```clj 68 | (clout/route-matches 69 | "//subdomain.example.com/" 70 | (mock/request :get "http://subdomain.example.com/")) 71 | 72 | => {} 73 | 74 | (clout/route-matches 75 | "//subdomain.example.com/" 76 | (mock/request :get "https://subdomain.example.com/")) 77 | 78 | => {} 79 | ``` 80 | 81 | Clout supports both keywords and wildcards. Keywords (like ":title") will 82 | match any character but the following: `/ . , ; ?`. Wildcards (*) will match 83 | anything. 84 | 85 | If a route does not match, nil is returned: 86 | 87 | ```clj 88 | (clout/route-matches "/products" (mock/request :get "/articles")) 89 | 90 | => nil 91 | ``` 92 | 93 | For additional performance, you can choose to pre-compile a route: 94 | 95 | ```clj 96 | (def user-route 97 | (clout/route-compile "/user/:id")) 98 | 99 | (clout/route-matches user-route (mock/request :get "/user/10")) 100 | 101 | => {:id "10"} 102 | ``` 103 | 104 | When compiling a route, you can specify a map of regular expressions to use 105 | for different keywords. This allows more specific routing: 106 | 107 | ```clj 108 | (def user-route 109 | (clout/route-compile "/user/:id" {:id #"\d+"})) 110 | 111 | (clout/route-matches user-route (mock/request :get "/user/10")) 112 | 113 | => {:user "10"} 114 | 115 | (clout/route-matches user-route (mock/request :get "/user/jsmith")) 116 | 117 | => nil 118 | ``` 119 | 120 | You can also specify regular expressions inline in braces after the 121 | keyword: 122 | 123 | ```clj 124 | (def user-route 125 | (clout/route-compile "/user/:id{\\d+}")) 126 | ``` 127 | 128 | Note that regular expression escape sequences (like `\d`) need to be 129 | double-escaped when placed inline in a string. 130 | 131 | ## License 132 | 133 | Copyright © 2018 James Reeves 134 | 135 | Distributed under the Eclipse Public License either version 1.0 or (at 136 | your option) any later version. 137 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject clout "2.2.1" 2 | :description "A HTTP route matching library" 3 | :url "https://github.com/weavejester/clout" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.7.0"] 7 | [org.clojure/clojurescript "1.7.228" :scope "provided"] 8 | [instaparse "1.4.8" :exclusions [org.clojure/clojure]]] 9 | :plugins [[lein-doo "0.1.7"] 10 | [lein-cljsbuild "1.1.4"]] 11 | :cljsbuild {:builds 12 | {:test 13 | {:source-paths ["src" "test"] 14 | :compiler {:main clout.test-runner 15 | :output-dir "target/out" 16 | :output-to "target/test/advanced.js" 17 | :target :nodejs 18 | :optimizations :advanced}}}} 19 | :doo {:build "test"} 20 | :profiles 21 | {:dev {:jvm-opts ^:replace [] 22 | :dependencies [[ring/ring-mock "0.2.0"] 23 | [criterium "0.4.2"]]} 24 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"] 25 | [org.clojure/clojurescript "1.7.228"]]} 26 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"] 27 | [org.clojure/clojurescript "1.8.51"]]} 28 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0"] 29 | [org.clojure/clojurescript "1.9.946"]]}}) 30 | -------------------------------------------------------------------------------- /src/clout/core.cljc: -------------------------------------------------------------------------------- 1 | (ns clout.core 2 | "A small language for routing." 3 | (:require [clojure.string :as string] 4 | [clojure.set :as set] 5 | [instaparse.core :as insta] 6 | #?(:cljs [clout.internal.regex :refer [re-unicode-letter]]))) 7 | 8 | (def ^:private re-chars (set "\\.*+|?()[]{}$^")) 9 | #?(:cljs (def ^:private re-char-escapes 10 | (into {} (for [c re-chars] [c (str "\\" c)])))) 11 | 12 | (defn- re-escape [s] 13 | (string/escape s #?(:clj #(if (re-chars %) (str \\ %)) :cljs re-char-escapes))) 14 | 15 | (defn- re-match-groups [re s] 16 | #?(:clj 17 | (let [matcher (re-matcher re s)] 18 | (when (.matches matcher) 19 | (for [i (range (.groupCount matcher))] 20 | (.group matcher (int (inc i)))))) 21 | :cljs 22 | (if (string? s) 23 | (let [matches (.exec re s)] 24 | (when (= (first matches) s) 25 | (rest matches))) 26 | (throw (js/TypeError. "re-match-groups must match against string."))))) 27 | 28 | (defn- assoc-conj [m k v] 29 | (assoc m k 30 | (if-let [cur (get m k)] 31 | (if (vector? cur) 32 | (conj cur v) 33 | [cur v]) 34 | v))) 35 | 36 | (defn- assoc-keys-with-groups [groups keys] 37 | (reduce (fn [m [k v]] (assoc-conj m k v)) 38 | {} 39 | (map vector keys groups))) 40 | 41 | (defn- request-url [request] 42 | (str (name (:scheme request)) 43 | "://" 44 | (get-in request [:headers "host"]) 45 | (:uri request))) 46 | 47 | (defn- path-info [request] 48 | (or (:path-info request) 49 | (:uri request))) 50 | 51 | (defprotocol Route 52 | (route-matches [route request] 53 | "If the route matches the supplied request, the matched keywords are 54 | returned as a map. Otherwise, nil is returned.")) 55 | 56 | (defrecord CompiledRoute [source re keys absolute?] 57 | Route 58 | (route-matches [_ request] 59 | (let [path-info (if absolute? 60 | (request-url request) 61 | (path-info request))] 62 | (let [groups (re-match-groups re path-info)] 63 | (when groups 64 | (assoc-keys-with-groups groups keys))))) 65 | Object 66 | (toString [_] source)) 67 | 68 | (def ^:private route-parser 69 | (insta/parser 70 | (let [letter #?(:clj "\\p{L}" :cljs re-unicode-letter)] 71 | (str 72 | "route = (scheme / part) part* 73 | scheme = #'(https?:)?//' 74 | = literal | escaped | wildcard | param 75 | literal = #'(:[^" letter "_*{}\\\\]|[^:*{}\\\\])+' 76 | escaped = #'\\\\.' 77 | wildcard = '*' 78 | param = key pattern? 79 | key = <':'> #'([" letter "_][" letter "_0-9-]*)' 80 | pattern = '{' (#'(?:[^{}\\\\]|\\\\.)+' | pattern)* '}'")) 81 | :no-slurp true)) 82 | 83 | (defn- parse [parser text] 84 | (let [result (insta/parse parser text)] 85 | (if (insta/failure? result) 86 | (throw (ex-info "Parse error in route string" {:failure result})) 87 | result))) 88 | 89 | (defn- find-route-key [form] 90 | (case (first form) 91 | :wildcard :* 92 | :param (-> form second second keyword))) 93 | 94 | (defn- route-keys [parse-tree] 95 | (->> (rest parse-tree) 96 | (filter (comp #{:param :wildcard} first)) 97 | (map find-route-key))) 98 | 99 | (defn- trim-pattern [pattern] 100 | (some-> pattern (subs 1 (dec (count pattern))))) 101 | 102 | (defn- param-regex [regexs key & [pattern]] 103 | (let [pattern (or (trim-pattern pattern) (regexs key) "[^/,;?]+")] 104 | (str "(" #?(:clj pattern 105 | :cljs (if (instance? js/RegExp pattern) 106 | (.-source pattern) 107 | pattern)) ")"))) 108 | 109 | (defn- route-regex [parse-tree regexs] 110 | (insta/transform 111 | {:route (comp re-pattern #?(:cljs #(str "^" % "$")) str) 112 | :scheme #(if (= % "//") "https?://" %) 113 | :literal re-escape 114 | :escaped #(re-escape (subs % 1)) 115 | :wildcard (constantly "(.*?)") 116 | :param (partial param-regex regexs) 117 | :key keyword 118 | :pattern str} 119 | parse-tree)) 120 | 121 | (defn- absolute-url? [path] 122 | (boolean (re-matches #"(https?:)?//.*" path))) 123 | 124 | (defn route-compile 125 | "Compile a route string for more efficient route matching." 126 | ([path] 127 | (route-compile path {})) 128 | ([path regexs] 129 | (let [ast (parse route-parser path) 130 | ks (route-keys ast)] 131 | (assert (set/subset? (set (keys regexs)) (set ks)) 132 | "unused keys in regular expression map") 133 | (CompiledRoute. 134 | path 135 | (route-regex ast regexs) 136 | (vec ks) 137 | (absolute-url? path))))) 138 | 139 | (extend-type #?(:clj String :cljs string) 140 | Route 141 | (route-matches [route request] 142 | (route-matches (route-compile route) request))) 143 | -------------------------------------------------------------------------------- /src/clout/internal/regex.cljs: -------------------------------------------------------------------------------- 1 | (ns ^:no-doc clout.internal.regex) 2 | 3 | (def re-unicode-letter 4 | (str 5 | "A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1" 6 | "\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D" 7 | "\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481" 8 | "\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0" 9 | "-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6" 10 | "\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5" 11 | "\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824" 12 | "\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950" 13 | "\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990" 14 | "\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC" 15 | "\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-" 16 | "\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-" 17 | "\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8" 18 | "\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1" 19 | "\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33" 20 | "\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-" 21 | "\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F" 22 | "\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-" 23 | "\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59" 24 | "\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3" 25 | "\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C" 26 | "\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F" 27 | "\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01" 28 | "-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88" 29 | "\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7" 30 | "\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6" 31 | "\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000" 32 | "-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-" 33 | "\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA" 34 | "\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260" 35 | "-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE" 36 | "\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318" 37 | "-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F" 38 | "\\u1681-\\u169A\\u16A0-\\u16EA\\u1700-\\u170C\\u170E-\\u1711\\u1720-" 39 | "\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7" 40 | "\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C" 41 | "\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-" 42 | "\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0" 43 | "\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D" 44 | "\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15" 45 | "\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B" 46 | "\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4" 47 | "\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-" 48 | "\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-" 49 | "\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-" 50 | "\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E" 51 | "\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-" 52 | "\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6" 53 | "\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-" 54 | "\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035" 55 | "\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-" 56 | "\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF" 57 | "\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-" 58 | "\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-" 59 | "\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793" 60 | "\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-" 61 | "\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-" 62 | "\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-" 63 | "\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF" 64 | "\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-" 65 | "\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16" 66 | "\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-" 67 | "\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13" 68 | "-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40" 69 | "\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-" 70 | "\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41" 71 | "-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7" 72 | "\\uFFDA-\\uFFDC")) 73 | -------------------------------------------------------------------------------- /test/clout/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns clout.core-test 2 | (:import #?@(:clj [[clojure.lang ExceptionInfo] 3 | [java.util.regex PatternSyntaxException]] 4 | :cljs [[goog Uri]])) 5 | (:require #?@(:clj [[clojure.test :refer :all] 6 | [ring.mock.request :refer [request]]] 7 | :cljs [[cljs.test :refer-macros [is are deftest testing use-fixtures]]]) 8 | [clout.core :refer [route-matches route-compile]])) 9 | 10 | #?(:cljs 11 | (defn request 12 | "Naive implementation of the Ring Mock Request in ClojureScript." 13 | [method uri] 14 | (let [uri (Uri. uri) 15 | host (if-not (empty? (.getDomain uri)) (.getDomain uri) "localhost") 16 | port (.getPort uri) 17 | scheme (.getScheme uri) 18 | path (js/encodeURI (.getPath uri))] 19 | {:server-port (or port 80) 20 | :server-name host 21 | :remote-addr "localhost" 22 | :uri (if (clojure.string/blank? path) "/" path) 23 | :query-string (.getQuery uri) 24 | :scheme (if-not (empty? scheme) (keyword scheme) :http) 25 | :request-method method 26 | :headers {"host" (if port 27 | (str host ":" port) 28 | host)}}))) 29 | 30 | (deftest fixed-path 31 | (are [path] (route-matches path (request :get path)) 32 | "/" 33 | "/foo" 34 | "/foo/bar" 35 | "/foo/bar.html")) 36 | 37 | (deftest keyword-paths 38 | (are [path uri params] (= (route-matches path (request :get uri)) params) 39 | "/:x" "/foo" {:x "foo"} 40 | "/foo/:x" "/foo/bar" {:x "bar"} 41 | "/a/b/:c" "/a/b/c" {:c "c"} 42 | "/:a/b/:c" "/a/b/c" {:a "a", :c "c"})) 43 | 44 | (deftest keywords-match-extensions 45 | (are [path uri params] (= (route-matches path (request :get uri)) params) 46 | "/foo.:ext" "/foo.txt" {:ext "txt"} 47 | "/:x.:y" "/foo.txt" {:x "foo", :y "txt"})) 48 | 49 | (deftest hyphen-keywords 50 | (are [path uri params] (= (route-matches path (request :get uri)) params) 51 | "/:foo-bar" "/baz" {:foo-bar "baz"} 52 | "/:foo-" "/baz" {:foo- "baz"})) 53 | 54 | (deftest underscore-keywords 55 | (are [path uri params] (= (route-matches path (request :get uri)) params) 56 | "/:foo_bar" "/baz" {:foo_bar "baz"} 57 | "/:_foo" "/baz" {:_foo "baz"})) 58 | 59 | (deftest urlencoded-keywords 60 | (are [path uri params] (= (route-matches path (request :get uri)) params) 61 | "/:x" "/foo%20bar" {:x "foo%20bar"} 62 | "/:x" "/foo+bar" {:x "foo+bar"} 63 | "/:x" "/foo%5Cbar" {:x "foo%5Cbar"})) 64 | 65 | (deftest same-keyword-many-times 66 | (are [path uri params] (= (route-matches path (request :get uri)) params) 67 | "/:x/:x/:x" "/a/b/c" {:x ["a" "b" "c"]} 68 | "/:x/b/:x" "/a/b/c" {:x ["a" "c"]})) 69 | 70 | (deftest non-ascii-keywords 71 | (are [path uri params] (= (route-matches path (request :get uri)) params) 72 | "/:äñßOÔ" "/abc" {:äñßOÔ "abc"} 73 | "/:ÁäñßOÔ" "/abc" {:ÁäñßOÔ "abc"} 74 | "/:ä/:ش" "/foo/bar" {:ä "foo" :ش "bar"} 75 | "/:ä/:ä" "/foo/bar" {:ä ["foo" "bar"]} 76 | "/:Ä-ü" "/baz" {:Ä-ü "baz"} 77 | "/:Ä_ü" "/baz" {:Ä_ü "baz"})) 78 | 79 | (deftest wildcard-paths 80 | (are [path uri params] (= (route-matches path (request :get uri)) params) 81 | "/*" "/foo" {:* "foo"} 82 | "/*" "/foo.txt" {:* "foo.txt"} 83 | "/*" "/foo/bar" {:* "foo/bar"} 84 | "/foo/*" "/foo/bar/baz" {:* "bar/baz"} 85 | "/a/*/d" "/a/b/c/d" {:* "b/c"})) 86 | 87 | (deftest escaped-chars 88 | (are [path uri params] (= (route-matches path (request :get uri)) params) 89 | "/\\:foo" "/foo" nil 90 | "/\\:foo" "/:foo" {})) 91 | 92 | (deftest inline-regexes 93 | (are [path uri params] (= (route-matches path (request :get uri)) params) 94 | "/:x{\\d+}" "/foo" nil 95 | "/:x{\\d+}" "/10" {:x "10"} 96 | "/:x{\\d{2}}" "/2" nil 97 | "/:x{\\d{2}}" "/20" {:x "20"} 98 | "/:x{\\d}/b" "/3/b" {:x "3"} 99 | "/:x{\\d}/b" "/a/b" nil 100 | "/a/:x{\\d}" "/a/4" {:x "4"} 101 | "/a/:x{\\d}" "/a/b" nil)) 102 | 103 | (deftest compiled-routes 104 | (is (= (route-matches (route-compile "/foo/:id") (request :get "/foo/bar")) 105 | {:id "bar"}))) 106 | 107 | (deftest url-paths 108 | (is (route-matches 109 | "http://localhost/" 110 | {:scheme :http 111 | :headers {"host" "localhost"} 112 | :uri "/"})) 113 | (is (route-matches 114 | "//localhost/" 115 | {:scheme :http 116 | :headers {"host" "localhost"} 117 | :uri "/"})) 118 | (is (route-matches 119 | "//localhost/" 120 | {:scheme :https 121 | :headers {"host" "localhost"} 122 | :uri "/"}))) 123 | 124 | (deftest url-port-paths 125 | (let [req (request :get "http://localhost:8080/")] 126 | (is (route-matches "http://localhost:8080/" req)) 127 | (is (not (route-matches "http://localhost:7070/" req))))) 128 | 129 | (deftest unmatched-paths 130 | (is (nil? (route-matches "/foo" (request :get "/bar"))))) 131 | 132 | (deftest path-info-matches 133 | (is (route-matches "/bar" (-> (request :get "/foo/bar") 134 | (assoc :path-info "/bar"))))) 135 | 136 | (deftest custom-matches 137 | (let [route (route-compile "/foo/:bar" {:bar #"\d+"})] 138 | (is (not (route-matches route (request :get "/foo/bar")))) 139 | (is (not (route-matches route (request :get "/foo/1x")))) 140 | (is (route-matches route (request :get "/foo/10"))))) 141 | 142 | (deftest unused-regex-keys 143 | (is (thrown? #?(:clj AssertionError :cljs js/Error) 144 | (route-compile "/:foo" {:foa #"\d+"}))) 145 | (is (thrown? #?(:clj AssertionError :cljs js/Error) 146 | (route-compile "/:foo" {:foo #"\d+" :bar #".*"})))) 147 | 148 | (deftest invalid-inline-patterns 149 | (is (thrown? ExceptionInfo (route-compile "/:foo{"))) 150 | (is (thrown? ExceptionInfo (route-compile "/:foo{\\d{2}"))) 151 | (is (thrown? #?(:clj PatternSyntaxException :cljs js/Error) 152 | (route-compile "/:foo{[a-z}")))) 153 | 154 | (deftest to-string-method 155 | (are [path regexs] (= (str (route-compile path regexs)) path) 156 | "/foo" {} 157 | "/foo/:bar" {} 158 | "/foo/:bar" {:bar #"\d+"} 159 | "/foo/:bar{\\d+}" {})) 160 | -------------------------------------------------------------------------------- /test/clout/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns clout.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [clout.core-test])) 4 | 5 | (doo-tests 'clout.core-test) 6 | --------------------------------------------------------------------------------