├── .gitignore ├── .travis.yml ├── test-doo └── calfpath │ └── runner.cljs ├── src └── calfpath │ ├── type.cljc │ ├── route │ ├── defaults.cljc │ ├── uri_index_match_impl.cljs │ ├── uri_index_match.cljc │ ├── uri_token_match_impl.cljs │ ├── uri_token_match.cljc │ └── uri_match.cljc │ ├── core.cljc │ └── internal.cljc ├── java-src └── calfpath │ ├── VolatileInt.java │ ├── route │ ├── UriMatch.java │ ├── UriIndexContext.java │ └── UriTokenContext.java │ └── Util.java ├── test └── calfpath │ ├── internal_test.cljc │ ├── route_reverse_test.cljc │ ├── route_prepare_test.cljc │ ├── route_handler_test.cljc │ └── core_test.cljc ├── project.clj ├── README.md ├── CHANGES.md ├── doc └── intro.md ├── LICENSE └── perf └── calfpath ├── perf_test.clj └── long_perf_test.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | /.classpath 13 | /.project 14 | /.settings 15 | /*.swp 16 | /bench*.png 17 | /.idea 18 | /*.iml 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: clojure 3 | jdk: 4 | - openjdk8 5 | - openjdk10 6 | - openjdk11 7 | env: 8 | - LEIN_PROFILE=c08 9 | - LEIN_PROFILE=c09 10 | - LEIN_PROFILE=c10 11 | script: 12 | - lein with-profile ${LEIN_PROFILE} test 13 | matrix: 14 | exclude: 15 | - jdk: openjdk7 16 | env: LEIN_PROFILE=c10 17 | cache: 18 | directories: 19 | - $HOME/.m2 20 | -------------------------------------------------------------------------------- /test-doo/calfpath/runner.cljs: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.runner 11 | (:require 12 | [doo.runner :refer-macros [doo-tests]] 13 | [calfpath.core-test] 14 | [calfpath.internal-test] 15 | [calfpath.route-handler-test] 16 | [calfpath.route-prepare-test] 17 | [calfpath.route-reverse-test])) 18 | 19 | 20 | (enable-console-print!) 21 | 22 | (try 23 | (doo-tests 24 | 'calfpath.core-test 25 | 'calfpath.internal-test 26 | 'calfpath.route-handler-test 27 | 'calfpath.route-prepare-test 28 | 'calfpath.route-reverse-test) 29 | (catch js/Error e 30 | (.log js/Console (.-stack e)))) 31 | -------------------------------------------------------------------------------- /src/calfpath/type.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.type 11 | "Type related arifacts.") 12 | 13 | 14 | (defprotocol IRouteMatcher 15 | (-parse-uri-template [this uri-template]) 16 | (-get-static-uri-template [this uri-pattern-tokens] "Return URI token(s) if static URI template, nil otherwise") 17 | (-initialize-request [this request params-key]) 18 | (-static-uri-partial-match [this request static-tokens params-key]) 19 | (-static-uri-full-match [this request static-tokens params-key]) 20 | (-dynamic-uri-partial-match [this request uri-template params-key]) 21 | (-dynamic-uri-full-match [this request uri-template params-key])) 22 | -------------------------------------------------------------------------------- /java-src/calfpath/VolatileInt.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shantanu Kumar. All rights reserved. 2 | // The use and distribution terms for this software are covered by the 3 | // Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | // which can be found in the file LICENSE at the root of this distribution. 5 | // By using this software in any fashion, you are agreeing to be bound by 6 | // the terms of this license. 7 | // You must not remove this notice, or any other, from this software. 8 | 9 | 10 | package calfpath; 11 | 12 | public class VolatileInt { 13 | 14 | public /*volatile*/ int value = 0; 15 | 16 | public static VolatileInt create(int init) { 17 | return new VolatileInt(init); 18 | } 19 | 20 | public VolatileInt(int init) { 21 | this.value = init; 22 | } 23 | 24 | public int get() { 25 | return value; 26 | } 27 | 28 | public static long deref(VolatileInt v) { 29 | return v.value; 30 | } 31 | 32 | public void set(int newValue) { 33 | this.value = newValue; 34 | } 35 | 36 | public static void reset(VolatileInt v, int newValue) { 37 | v.value = newValue; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /test/calfpath/internal_test.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.internal-test 11 | (:require 12 | #?(:cljs [cljs.test :refer-macros [deftest is testing]] 13 | :clj [clojure.test :refer [deftest is testing]]) 14 | #?(:cljs [calfpath.internal :as i :include-macros true] 15 | :clj [calfpath.internal :as i]))) 16 | 17 | 18 | (deftest test-path-parsing 19 | (is (= [["/user/" :id "/profile/" :descriptor "/"] false] 20 | (i/parse-uri-template "/user/:id/profile/:descriptor/") 21 | (i/parse-uri-template "/user/{id}/profile/{descriptor}/")) "variables with trailing /") 22 | (is (= [["/user/" :id "/profile/" :descriptor] false] 23 | (i/parse-uri-template "/user/:id/profile/:descriptor") 24 | (i/parse-uri-template "/user/{id}/profile/{descriptor}")) "variables without trailing /") 25 | (is (= [["/foo"] true] 26 | (i/parse-uri-template "/foo*")) "simple one token, partial") 27 | (is (= [["/bar/" :bar-id] true] 28 | (i/parse-uri-template "/bar/:bar-id*") 29 | (i/parse-uri-template "/bar/{bar-id}*")) "one token, one param, partial") 30 | (is (= [[""] false] 31 | (i/parse-uri-template "")) "empty string") 32 | (is (= [[""] true] 33 | (i/parse-uri-template "*")) "empty string, partial")) 34 | -------------------------------------------------------------------------------- /src/calfpath/route/defaults.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route.defaults 11 | "Internal namespace for data-driven route defaults." 12 | (:require 13 | [calfpath.type :as t] 14 | [calfpath.route.uri-index-match :as uim] 15 | [calfpath.route.uri-token-match :as utm])) 16 | 17 | 18 | (def router utm/route-matcher) 19 | 20 | 21 | (defn d-parse-uri-template [uri-pattern] 22 | (t/-parse-uri-template router uri-pattern)) 23 | 24 | 25 | (defn d-get-static-uri-template [uri-pattern-tokens] 26 | (t/-get-static-uri-template router uri-pattern-tokens)) 27 | 28 | 29 | (defn d-initialize-request [request params-key] 30 | (t/-initialize-request router request params-key)) 31 | 32 | 33 | (defn d-static-uri-partial-match 34 | [request static-tokens params-key] 35 | (t/-static-uri-partial-match router request static-tokens params-key)) 36 | 37 | 38 | (defn d-static-uri-full-match 39 | [request static-tokens params-key] 40 | (t/-static-uri-full-match router request static-tokens params-key)) 41 | 42 | 43 | (defn d-dynamic-uri-partial-match 44 | [request uri-template params-key] 45 | (t/-dynamic-uri-partial-match router request uri-template params-key)) 46 | 47 | 48 | (defn d-dynamic-uri-full-match 49 | [request uri-template params-key] 50 | (t/-dynamic-uri-full-match router request uri-template params-key)) 51 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject calfpath "0.8.1" 2 | :description "A la carte ring request matching" 3 | :url "https://github.com/kumarshantanu/calfpath" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :global-vars {*warn-on-reflection* true 7 | *assert* true 8 | *unchecked-math* :warn-on-boxed} 9 | :pedantic? :warn 10 | :java-source-paths ["java-src"] 11 | :javac-options ["-target" "1.7" "-source" "1.7" "-Xlint:-options"] 12 | :profiles {:provided {:dependencies [[org.clojure/clojure "1.8.0"]]} 13 | :cljs {:plugins [[lein-cljsbuild "1.1.7"] 14 | [lein-doo "0.1.10" :exclusions [org.clojure/clojure]]] 15 | :doo {:build "test"} 16 | :cljsbuild {:builds {:test {:source-paths ["src" "test" "test-doo"] 17 | :compiler {:main calfpath.runner 18 | :output-dir "target/out" 19 | :output-to "target/test/core.js" 20 | :target :nodejs 21 | :optimizations :none 22 | :source-map true 23 | :pretty-print true}}}} 24 | :prep-tasks [["cljsbuild" "once"]] 25 | :hooks [leiningen.cljsbuild]} 26 | :c08 {:dependencies [[org.clojure/clojure "1.8.0"]]} 27 | :c09 {:dependencies [[org.clojure/clojure "1.9.0"]]} 28 | :c10 {:dependencies [[org.clojure/clojure "1.10.2"]]} 29 | :dln {:jvm-opts ["-Dclojure.compiler.direct-linking=true"]} 30 | :s09 {:dependencies [[org.clojure/clojure "1.9.0"] 31 | [org.clojure/clojurescript "1.9.946"]]} 32 | :s10 {:dependencies [[org.clojure/clojure "1.9.0"] 33 | [org.clojure/clojurescript "1.10.773" :exclusions [com.google.code.findbugs/jsr305]]]} 34 | :perf {:dependencies [[ataraxy "0.4.2" :exclusions [[org.clojure/clojure] 35 | [ring/ring-core]]] 36 | [bidi "2.1.6" :exclusions [ring/ring-core]] 37 | [compojure "1.6.2" :exclusions [[org.clojure/clojure] 38 | [ring/ring-core] 39 | [ring/ring-codec]]] 40 | [metosin/reitit-ring "0.5.5"] 41 | [citius "0.2.4"]] 42 | :test-paths ["perf"] 43 | :jvm-opts ^:replace ["-server" "-Xms2048m" "-Xmx2048m"]}} 44 | :aliases {"clj-test" ["with-profile" "c08:c09:c10" "test"] 45 | "cljs-test" ["with-profile" "cljs,s09:cljs,s10" "doo" "node" "once"] 46 | "stest" ["with-profile" "cljs,s10" "doo" "node" "once"] ; test with latest CLJS 47 | "perf-test" ["with-profile" "c10,perf" "test"]}) 48 | -------------------------------------------------------------------------------- /test/calfpath/route_reverse_test.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route-reverse-test 11 | (:require 12 | #?(:cljs [cljs.test :refer-macros [deftest is testing]] 13 | :clj [clojure.test :refer [deftest is testing]]) 14 | #?(:cljs [calfpath.route :as r :include-macros true] 15 | :clj [calfpath.route :as r]))) 16 | 17 | 18 | (def indexable-routes 19 | [{:uri "/info/:token" :method :get :id :info} 20 | {:uri "/album/:lid/artist/:rid/" :method :get :id :album} 21 | {:uri "/user/:id*" 22 | :nested [{:uri "/auth" :id :auth-user} 23 | {:uri "/permissions/" :nested [{:method :get :id :read-perms} 24 | {:method :post :id :save-perms} 25 | {:method :put :id :update-perms}]} 26 | {:uri "/profile/:type/" :nested [{:method :get :id :read-profile} 27 | {:method :patch :id :patch-profile} 28 | {:method :delete :id :remove-profile}]} 29 | {:uri "" :handler identity }]} 30 | {:uri "/public/*" :method :get :id :public-file}]) 31 | 32 | 33 | (deftest test-index-routes 34 | (let [routing-index (r/make-index indexable-routes)] 35 | (is (= {:info {:uri ["/info/" :token] :request-method :get} 36 | :album {:uri ["/album/" :lid "/artist/" :rid "/"] :request-method :get} 37 | :auth-user {:uri ["/user/" :id "/auth"] :request-method :get} 38 | :read-perms {:uri ["/user/" :id "/permissions/"] :request-method :get} 39 | :save-perms {:uri ["/user/" :id "/permissions/"] :request-method :post} 40 | :update-perms {:uri ["/user/" :id "/permissions/"] :request-method :put} 41 | :read-profile {:uri ["/user/" :id "/profile/" :type "/"] :request-method :get} 42 | :patch-profile {:uri ["/user/" :id "/profile/" :type "/"] :request-method :patch} 43 | :remove-profile {:uri ["/user/" :id "/profile/" :type "/"] :request-method :delete} 44 | :public-file {:uri ["/public/" :*] :request-method :get}} 45 | routing-index)) 46 | (is (= {:uri "/album/10/artist/20/" 47 | :request-method :get} 48 | (-> (:album routing-index) 49 | (r/template->request {:uri-params {:lid 10 :rid 20}})))) 50 | (is (= {:uri "/public/foo.html" 51 | :request-method :get} 52 | (-> (:public-file routing-index) 53 | (r/template->request {:uri-params {:* "foo.html"}})))) 54 | (is (= {:uri "https://myapp.com/user/10/permissions/?q=beer&country=in" 55 | :request-method :post} 56 | (-> (:save-perms routing-index) 57 | (r/template->request {:uri-params {:id 10} 58 | :uri-prefix "https://myapp.com" 59 | :uri-suffix "?q=beer&country=in"})))) 60 | (is (thrown-with-msg? 61 | #?(:cljs js/Error 62 | :clj clojure.lang.ExceptionInfo) 63 | #"Expected URI param for key \:id, but found .*" 64 | (-> (:save-perms routing-index) 65 | (r/template->request {:uri-params {:user-id 10}})))))) 66 | -------------------------------------------------------------------------------- /java-src/calfpath/route/UriMatch.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shantanu Kumar. All rights reserved. 2 | // The use and distribution terms for this software are covered by the 3 | // Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | // which can be found in the file LICENSE at the root of this distribution. 5 | // By using this software in any fashion, you are agreeing to be bound by 6 | // the terms of this license. 7 | // You must not remove this notice, or any other, from this software. 8 | 9 | 10 | package calfpath.route; 11 | 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | /** 17 | * Internal, URI matching utility class. 18 | * 19 | */ 20 | public class UriMatch { 21 | 22 | public static final int NO_URI_MATCH_INDEX = -2; 23 | 24 | public static boolean isPos(long n) { 25 | return n > 0; 26 | } 27 | 28 | // ----- match methods ----- 29 | 30 | public static int dynamicUriMatch(String uri, int beginIndex, Map paramsMap, List patternTokens, 31 | boolean attemptPartialMatch) { 32 | final int uriLength = uri.length(); 33 | if (beginIndex >= uriLength) { // may happen when previous partial-segment matched fully 34 | return NO_URI_MATCH_INDEX; 35 | } 36 | final Map pathParams = new HashMap(); 37 | StringBuilder sb = null; 38 | int uriIndex = beginIndex; 39 | OUTER: 40 | for (final Object token: patternTokens) { 41 | if (uriIndex >= uriLength) { 42 | if (attemptPartialMatch) { 43 | paramsMap.putAll(pathParams); 44 | return /* full match */ uriLength; 45 | } 46 | return NO_URI_MATCH_INDEX; 47 | } 48 | if (token instanceof String) { 49 | final String tokenStr = (String) token; 50 | if (uri.startsWith(tokenStr, uriIndex)) { 51 | uriIndex += tokenStr.length(); 52 | // at this point, uriIndex == uriLength if last string token 53 | } else { // 'string token mismatch' implies no match 54 | return NO_URI_MATCH_INDEX; 55 | } 56 | } else { 57 | if (sb == null) { 58 | sb = new StringBuilder(); 59 | } 60 | sb.setLength(0); // reset buffer before use 61 | for (int j = uriIndex; j < uriLength; j++) { 62 | final char ch = uri.charAt(j); 63 | if (ch == '/') { 64 | pathParams.put(token, sb.toString()); 65 | uriIndex = j; 66 | continue OUTER; 67 | } else { 68 | sb = sb.append(ch); 69 | } 70 | } 71 | pathParams.put(token, sb.toString()); 72 | uriIndex = uriLength; // control reaching this point means URI template ending in ":param*" 73 | } 74 | } 75 | if (uriIndex < uriLength) { // 'tokens finished but URI still in progress' implies partial or no match 76 | if (attemptPartialMatch) { 77 | paramsMap.putAll(pathParams); 78 | return /* full match */ uriIndex; 79 | } 80 | return NO_URI_MATCH_INDEX; 81 | } 82 | paramsMap.putAll(pathParams); 83 | return /* full match */ uriLength; 84 | } 85 | 86 | public static int dynamicUriPartialMatch(String uri, int beginIndex, Map paramsMap, List patternTokens) { 87 | return dynamicUriMatch(uri, beginIndex, paramsMap, patternTokens, true); 88 | } 89 | 90 | public static int dynamicUriFullMatch(String uri, int beginIndex, Map paramsMap, List patternTokens) { 91 | return dynamicUriMatch(uri, beginIndex, paramsMap, patternTokens, false); 92 | } 93 | 94 | public static int staticUriPartialMatch(String uri, int beginIndex, String token) { 95 | final int uriLength = uri.length(); 96 | if (uri.startsWith(token, beginIndex)) { 97 | final int tokenLength = token.length(); 98 | return (uriLength - beginIndex) == tokenLength? 99 | /* full match */ uriLength: /* partial match */ (beginIndex + tokenLength); 100 | } 101 | return NO_URI_MATCH_INDEX; 102 | } 103 | 104 | public static int staticUriFullMatch(String uri, int beginIndex, String token) { 105 | final int uriLength = uri.length(); 106 | if (beginIndex == 0) { 107 | return uri.equals(token)? /* full match */ uriLength: NO_URI_MATCH_INDEX; 108 | } 109 | return (uri.startsWith(token, beginIndex) && (uriLength - beginIndex == token.length()))? 110 | /* full match */ uriLength: NO_URI_MATCH_INDEX; 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /test/calfpath/route_prepare_test.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route-prepare-test 11 | (:require 12 | [clojure.walk :as walk] 13 | #?(:cljs [cljs.test :refer-macros [deftest is testing]] 14 | :clj [clojure.test :refer [deftest is testing]]) 15 | #?(:cljs [calfpath.route :as r :include-macros true] 16 | :clj [calfpath.route :as r]))) 17 | 18 | 19 | ;; ----- utility ----- 20 | 21 | 22 | (defn handler [ks] (fn [request] 23 | (select-keys request (conj ks :request-method)))) 24 | 25 | 26 | (def er-handler (handler [:path-params :uri :method])) 27 | 28 | 29 | (defn deep-dissoc [data k] (walk/prewalk (fn [node] 30 | (if (map? node) 31 | (dissoc node k) 32 | node)) 33 | data)) 34 | 35 | 36 | (defn remove-handler [routes] (deep-dissoc routes :handler)) 37 | 38 | 39 | ;; ----- easy-route tests ----- 40 | 41 | 42 | (def easy-routes1 43 | [{["/album/:lid/artist/:rid/" :get] er-handler} 44 | {["/album/:lid/artist/:rid/" :put] er-handler} 45 | {"/hello/1234/" er-handler} 46 | {["/info/:token/" :get] er-handler} 47 | {"/user/:id*" [{"/auth" er-handler} 48 | {"/permissions/" [{:get er-handler} 49 | {:post er-handler} 50 | {:put er-handler}]} 51 | {"/profile/:type/" [{:get er-handler} 52 | {:patch er-handler} 53 | {:delete er-handler}]} 54 | {"" er-handler}]}]) 55 | 56 | 57 | (def flat-routes1 ; flat-routes are the NON-easy regular version of easy-routes 58 | [{:uri "/album/:lid/artist/:rid/" :method :get} 59 | {:uri "/album/:lid/artist/:rid/" :method :put} 60 | {:uri "/hello/1234/" } 61 | {:uri "/info/:token/" :method :get} 62 | {:uri "/user/:id*" :nested [{:uri "/auth"} 63 | {:uri "/permissions/" :nested [{:method :get } 64 | {:method :post} 65 | {:method :put }]} 66 | {:uri "/profile/:type/" :nested [{:method :get} 67 | {:method :patch} 68 | {:method :delete}]} 69 | {:uri ""}]}]) 70 | 71 | 72 | (def easy-routes2 73 | [{["/user" :post] er-handler} 74 | {"/user/:id" [{:get er-handler} 75 | {:put er-handler}]}]) 76 | 77 | 78 | (def flat-routes2 79 | [{:uri "/user" :method :post} 80 | {:uri "/user/:id" :nested [{:method :get} 81 | {:method :put}]}]) 82 | 83 | 84 | (deftest test-easy 85 | (is (= flat-routes1 (-> easy-routes1 86 | (r/easy-routes :uri :method) 87 | remove-handler))) 88 | (is (= flat-routes2 (-> easy-routes2 89 | (r/easy-routes :uri :method) 90 | remove-handler)))) 91 | 92 | 93 | ;; ----- tidy-route tests ----- 94 | 95 | 96 | (def tidy-routes1 97 | [{:uri "/album/:lid/artist/:rid/" :nested [{:method :get} 98 | {:method :put}]} 99 | {:uri "/hello/1234/" } 100 | {:uri "/info/:token/" :method :get} 101 | {:uri "/user/:id*" :nested [{:uri "/auth" } 102 | {:uri "/permissions/" :nested [{:method :get } 103 | {:method :post } 104 | {:method :put }]} 105 | {:uri "/profile/:type/" :nested [{:method :get } 106 | {:method :patch } 107 | {:method :delete}]} 108 | {:uri "" }]}]) 109 | 110 | 111 | (def tidy-routes2 112 | [{:uri "/user*" :nested [{:uri "/:id" :nested [{:method :get} 113 | {:method :put}]} 114 | {:uri "" :method :post}]}]) 115 | 116 | 117 | (deftest test-routes->wildcard-tidy 118 | (is (= tidy-routes1 (-> flat-routes1 119 | (r/update-routes r/routes->wildcard-tidy {:tidy-threshold 2})))) 120 | (is (= tidy-routes2 (-> flat-routes2 121 | (r/update-routes r/routes->wildcard-tidy {:tidy-threshold 1}))))) 122 | -------------------------------------------------------------------------------- /src/calfpath/route/uri_index_match_impl.cljs: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route.uri-index-match-impl 11 | "CLJS implementation for URI-index based URI matching." 12 | (:require 13 | [clojure.string :as string])) 14 | 15 | 16 | (defrecord IndexContext [#_immutable uri 17 | #_immutable ^long uri-length 18 | ^:mutable ^long uri-begin-index 19 | ^:mutable path-params]) 20 | 21 | 22 | (defn make-context 23 | ^IndexContext [uri] 24 | (IndexContext. uri (count uri) 0 {})) 25 | 26 | 27 | (defn get-path-params 28 | [^IndexContext context] 29 | (.-path-params context)) 30 | 31 | 32 | ;; ----- URI matching ----- 33 | 34 | 35 | (def ^:const FULL-URI-MATCH-INDEX -1) 36 | (def ^:const NO-URI-MATCH-INDEX -2) 37 | 38 | 39 | (defn partial-uri-match 40 | ([^IndexContext context uri-index] 41 | (set! (.-uri-begin-index context) uri-index) 42 | uri-index) 43 | ([^IndexContext context path-params uri-index] 44 | (set! (.-uri-begin-index context) uri-index) 45 | (set! (.-path-params context) (conj (.-path-params context) path-params)) 46 | uri-index)) 47 | 48 | 49 | (defn full-uri-match 50 | ([^IndexContext context] 51 | (set! (.-uri-begin-index context) (.-uri-length context)) 52 | FULL-URI-MATCH-INDEX) 53 | ([^IndexContext context path-params] 54 | (set! (.-uri-begin-index context) (.-uri-length context)) 55 | (set! (.-path-params context) (conj (.-path-params context) path-params)) 56 | FULL-URI-MATCH-INDEX)) 57 | 58 | 59 | (defn remaining-uri 60 | ([^IndexContext context] (subs (.-uri context) (.-uri-begin-index context))) 61 | ([^IndexContext context uri-index] (subs (.-uri context) uri-index))) 62 | 63 | 64 | ;; ~~~ match fns ~~~ 65 | 66 | 67 | (defn dynamic-uri-match 68 | [^IndexContext context pattern-tokens partial?] 69 | (if (>= (.-uri-begin-index context) (.-uri-length context)) 70 | NO-URI-MATCH-INDEX 71 | (loop [uri-index (.-uri-begin-index context) 72 | path-params (transient {}) 73 | next-tokens (seq pattern-tokens)] 74 | (if next-tokens 75 | (if (>= uri-index (.-uri-length context)) 76 | (if partial? 77 | (partial-uri-match context (persistent! path-params) (.-uri-length context)) 78 | NO-URI-MATCH-INDEX) 79 | (let [token (first next-tokens)] 80 | (if (string? token) 81 | ;; string token 82 | (if (string/starts-with? (remaining-uri context uri-index) token) 83 | (recur (unchecked-add uri-index (count token)) path-params (next next-tokens)) 84 | NO-URI-MATCH-INDEX) 85 | ;; must be a keyword 86 | (let [[pp ui] (loop [sb "" ; string buffer 87 | j uri-index] 88 | (if (< j (.-uri-length context)) 89 | (let [ch (get (.-uri context) j)] 90 | (if (= \/ ch) 91 | [(assoc! path-params token sb) 92 | j] 93 | (recur (str sb ch) (unchecked-inc j)))) 94 | [(assoc! path-params token sb) 95 | (.-uri-length context)]))] 96 | (recur ui pp (next next-tokens)))))) 97 | (if (< uri-index (.-uri-length context)) 98 | (if partial? 99 | (partial-uri-match context (persistent! path-params) uri-index) 100 | NO-URI-MATCH-INDEX) 101 | (full-uri-match context (persistent! path-params))))))) 102 | 103 | 104 | (defn dynamic-uri-partial-match 105 | [^IndexContext context pattern-tokens] 106 | (dynamic-uri-match context pattern-tokens true)) 107 | 108 | 109 | (defn dynamic-uri-full-match 110 | [^IndexContext context pattern-tokens] 111 | (dynamic-uri-match context pattern-tokens false)) 112 | 113 | 114 | (defn static-uri-partial-match 115 | [^IndexContext context static-token] 116 | (let [rem-uri (remaining-uri context) 117 | rem-len (count rem-uri)] 118 | (if (string/starts-with? rem-uri static-token) 119 | (let [token-length (count static-token)] 120 | (if (= rem-len token-length) 121 | (full-uri-match context) 122 | (partial-uri-match context (unchecked-add (.-uri-begin-index context) token-length)))) 123 | NO-URI-MATCH-INDEX))) 124 | 125 | 126 | (defn static-uri-full-match 127 | [^IndexContext context static-token] 128 | (if (= 0 (.-uri-begin-index context)) 129 | (if (= static-token (.-uri context)) 130 | FULL-URI-MATCH-INDEX 131 | NO-URI-MATCH-INDEX) 132 | (let [rem-uri (remaining-uri context) 133 | rem-len (count rem-uri)] 134 | (if (= rem-uri static-token) 135 | FULL-URI-MATCH-INDEX 136 | NO-URI-MATCH-INDEX)))) 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # calfpath 2 | 3 | [![Build Status](https://travis-ci.org/kumarshantanu/calfpath.svg)](https://travis-ci.org/kumarshantanu/calfpath) 4 | [![cljdoc badge](https://cljdoc.org/badge/calfpath/calfpath)](https://cljdoc.org/d/calfpath/calfpath) 5 | 6 | A Clojure/Script library for _à la carte_ (orthogonal) [Ring](https://github.com/ring-clojure/ring) request matching, 7 | routing and reverse-routing. 8 | 9 | (_Calf path_ is a synonym for [Desire path](http://en.wikipedia.org/wiki/Desire_path). 10 | [The Calf-Path](http://www.poets.org/poetsorg/poem/calf-path) is a poem by _Sam Walter Foss_.) 11 | 12 | 13 | ## Rationale 14 | 15 | - Ring has no built-in routing mechanism; Calfpath delivers this essential feature. 16 | - Orthogonality - match URI patterns, HTTP methods or anything in a Ring request. 17 | - Calfpath is fast (benchmarks included) - there is no cost to what you do not use. 18 | - Available as both dispatch macros and extensible, bi-directional, data-driven routes. 19 | 20 | 21 | ## Usage 22 | 23 | Leiningen dependency: `[calfpath "0.8.1"]` (requires Clojure 1.8 or later, Java 7 or later) 24 | 25 | Require namespace: 26 | ```clojure 27 | (require '[calfpath.core :refer [->uri ->method 28 | ->get ->head ->options ->patch ->put ->post ->delete]]) 29 | (require '[calfpath.route :as r]) 30 | ``` 31 | 32 | ### Direct HTTP URI/method dispatch 33 | 34 | When you need to dispatch on URI pattern with convenient API: 35 | 36 | ```clojure 37 | (defn handler 38 | [request] 39 | ;; ->uri is a macro that dispatches on URI pattern 40 | (->uri request 41 | "/user/:id*" [id] (->uri request 42 | "/profile/:type/" [type] (->method request 43 | :get {:status 200 44 | :headers {"Content-Type" "text/plain"} 45 | :body (format "ID: %s, Type: %s" id type)} 46 | :put {:status 200 47 | :headers {"Content-Type" "text/plain"} 48 | :body "Updated"}) 49 | "/permissions/" [] (->method request 50 | :get {:status 200 51 | :headers {"Content-Type" "text/plain"} 52 | :body (str "ID: " id)} 53 | :put {:status 200 54 | :headers {"Content-Type" "text/plain"} 55 | :body (str "Updated ID: " id)})) 56 | "/company/:cid/dept/:did/" [cid did] (->put request 57 | {:status 200 58 | :headers {"Content-Type" "text/plain"} 59 | :body "Data"}) 60 | "/this/is/a/static/route" [] (->put request 61 | {:status 200 62 | :headers {"Content-Type" "text/plain"} 63 | :body "output"}))) 64 | ``` 65 | 66 | ### Data-driven routes 67 | 68 | Calfpath supports data-driven _routes_ where every route is a map of certain keys. Routes are easy to 69 | extend and re-purpose. See an example below (where route-handler has the same arity as a Ring handler): 70 | 71 | ```clojure 72 | ;; a route-handler is arity-1 (or arity-3 for async) fn, like a ring-handler 73 | (defn list-user-jobs 74 | [{{:keys [user-id] :path-params} :as request}] 75 | ...) 76 | 77 | (defn app-routes 78 | "Return a vector of routes." 79 | [] 80 | [;; first route has a partial URI match,implied by a trailing '*' 81 | {"/users/:user-id*" [{"/jobs/" [{:get list-user-jobs} 82 | {:post assign-job}]} 83 | {["/permissions/" :get] permissions-hanler}]} 84 | {["/orders/:order-id/confirm/" :post] confirm-order} 85 | {"/health/" health-status} 86 | {"/static/*" (-> (fn [_] {:status 400 :body "No such file"}) ; static files serving example 87 | ;; the following requires [ring/ring-core "version"] dependency in your project 88 | (ring.middleware.resource/wrap-resource "public") ; render files from classpath 89 | (ring.middleware.file/wrap-file "/var/www/public") ; render files from filesystem 90 | (ring.middleware.content-type/wrap-content-type) 91 | (ring.middleware.not-modified/wrap-not-modified))}]) 92 | 93 | ;; create a Ring handler from given routes 94 | (def ring-handler 95 | (-> (app-routes) ; return routes vector 96 | r/compile-routes ; turn every map into a route by populating matchers in them 97 | r/make-dispatcher)) 98 | ``` 99 | 100 | 101 | ## Documentation 102 | 103 | See [documentation page](doc/intro.md) for concepts, examples and more features. 104 | 105 | 106 | ## Development 107 | 108 | Running tests: 109 | 110 | ```shell 111 | $ lein do clean, test 112 | $ lein with-profile c08 test 113 | ``` 114 | 115 | Running performance benchmarks: 116 | 117 | ```shell 118 | $ lein do clean, perf-test 119 | $ lein with-profile c08,perf test # on specified Clojure version 120 | ``` 121 | 122 | 123 | ## License 124 | 125 | Copyright © 2015-2021 Shantanu Kumar 126 | 127 | Distributed under the Eclipse Public License either version 1.0 or (at 128 | your option) any later version. 129 | -------------------------------------------------------------------------------- /src/calfpath/core.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.core 11 | "Routing macros in Calfpath." 12 | #?(:cljs (:require-macros calfpath.core)) 13 | (:require 14 | [clojure.string :as str] 15 | [calfpath.internal :as i])) 16 | 17 | 18 | (defmacro ->uri 19 | "Given a ring request map and pairs of URI-templates (e.g. '/user/:id/profile/:type/') and expression clauses, 20 | evaluate matching expression after adding URI params as a map to the :params key in the request map. Odd numbered 21 | clauses imply the last argument is the default expression invoked on no-match. Even numbered clauses return HTTP 400 22 | by default on no-match. The dispatch happens in linear time based on URI length and number of clauses." 23 | [request & clauses] 24 | (i/expected symbol? "a symbol bound to ring request map" request) 25 | (when-not (#{0 1} (rem (count clauses) 3)) 26 | (i/expected "clauses in sets of 3 with an optional default expression" clauses)) 27 | (doseq [[uri-template dav _] (partition 3 clauses)] 28 | (i/expected string? "a uri-template string" #?(:cljs uri-template 29 | :clj (eval uri-template))) 30 | (when-not (and (vector? dav) (every? symbol? dav)) 31 | (i/expected "destructuring argument vector with symbols" dav))) 32 | (let [response-400 {:status 400 33 | :headers {"Content-Type" "text/plain"} 34 | :body "400 Bad request. URI does not match any available uri-template."}] 35 | (if (seq clauses) 36 | (let [params (gensym "params__")] 37 | (if (= 1 (count clauses)) 38 | (first clauses) 39 | (let [[uri-pattern dav expr] clauses 40 | [uri-template partial?] (i/parse-uri-template #?(:cljs uri-pattern 41 | :clj (eval uri-pattern)))] 42 | `(if-some [^"[Ljava.lang.Object;" 43 | match-result# (i/match-uri (:uri ~request) 44 | (int (i/get-uri-match-end-index ~request)) 45 | ~uri-template ~partial?)] 46 | (let [{:keys ~dav :as ~params} (aget match-result# 0) 47 | ~request (i/assoc-uri-match-end-index ~request (aget match-result# 1))] 48 | ~expr) 49 | (->uri ~request ~@(drop 3 clauses)))))) 50 | response-400))) 51 | 52 | 53 | (defmacro ->method 54 | "Like clojure.core/case except that the first argument must be a request map. Odd numbered clauses imply the last 55 | argument is the default expression invoked on no-match. Even numbered clauses return HTTP 405 (method not supported) 56 | by default on no-match. The dispatch happens in constant time." 57 | [request & clauses] 58 | (doseq [[method-key _] (partition 2 clauses)] 59 | (i/expected i/valid-method-keys (str "method-key to be either of " i/valid-method-keys) method-key)) 60 | (let [valid-method-keys-str (->> (partition 2 clauses) 61 | (map first) 62 | (map name) 63 | (map str/upper-case) 64 | (str/join ", ")) 65 | response-405 {:status 405 66 | :headers {"Allow" valid-method-keys-str 67 | "Content-Type" "text/plain"} 68 | :body (str "405 Method not supported. Supported methods are: " valid-method-keys-str)}] 69 | (if (seq clauses) 70 | (if (odd? (count clauses)) ; if odd count, then the user is handling no-match - so no need to provide default 71 | `(case (:request-method ~request) 72 | ~@clauses) 73 | `(case (:request-method ~request) 74 | ~@clauses 75 | ~response-405)) 76 | response-405))) 77 | 78 | 79 | (defmacro ->get 80 | ([request expr] 81 | `(i/method-dispatch :get ~request ~expr)) 82 | ([request expr default-expr] 83 | `(i/method-dispatch :get ~request ~expr ~default-expr))) 84 | 85 | 86 | (defmacro ->head 87 | ([request expr] 88 | `(i/method-dispatch :head ~request ~expr)) 89 | ([request expr default-expr] 90 | `(i/method-dispatch :head ~request ~expr ~default-expr))) 91 | 92 | 93 | (defmacro ->options 94 | ([request expr] 95 | `(i/method-dispatch :options ~request ~expr)) 96 | ([request expr default-expr] 97 | `(i/method-dispatch :options ~request ~expr ~default-expr))) 98 | 99 | 100 | (defmacro ->patch 101 | ([request expr] 102 | `(i/method-dispatch :patch ~request ~expr)) 103 | ([request expr default-expr] 104 | `(i/method-dispatch :patch ~request ~expr ~default-expr))) 105 | 106 | 107 | (defmacro ->put 108 | ([request expr] 109 | `(i/method-dispatch :put ~request ~expr)) 110 | ([request expr default-expr] 111 | `(i/method-dispatch :put ~request ~expr ~default-expr))) 112 | 113 | 114 | (defmacro ->post 115 | ([request expr] 116 | `(i/method-dispatch :post ~request ~expr)) 117 | ([request expr default-expr] 118 | `(i/method-dispatch :post ~request ~expr ~default-expr))) 119 | 120 | 121 | (defmacro ->delete 122 | ([request expr] 123 | `(i/method-dispatch :delete ~request ~expr)) 124 | ([request expr default-expr] 125 | `(i/method-dispatch :delete ~request ~expr ~default-expr))) 126 | -------------------------------------------------------------------------------- /java-src/calfpath/route/UriIndexContext.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shantanu Kumar. All rights reserved. 2 | // The use and distribution terms for this software are covered by the 3 | // Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | // which can be found in the file LICENSE at the root of this distribution. 5 | // By using this software in any fashion, you are agreeing to be bound by 6 | // the terms of this license. 7 | // You must not remove this notice, or any other, from this software. 8 | 9 | 10 | package calfpath.route; 11 | 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | /** 17 | * Internal, URI index based URI matching utility class. Instance of this class may be stored as a context object in 18 | * a Ring request. This class has unsynchronized mutable fields, accessed in a single thread. 19 | * 20 | */ 21 | public class UriIndexContext { 22 | 23 | final StringBuilder BUFFER = new StringBuilder(); 24 | 25 | public final String uri; 26 | public final int uriLength; 27 | public /***/ int uriBeginIndex; 28 | public final Map paramsMap; 29 | 30 | public void setUriBeginIndex(int uriIndex) { 31 | this.uriBeginIndex = uriIndex; 32 | } 33 | 34 | public UriIndexContext(String uri, Map paramsMap) { 35 | this.uri = uri; 36 | this.uriLength = uri.length(); 37 | this.uriBeginIndex = 0; 38 | this.paramsMap = paramsMap; 39 | } 40 | 41 | public UriIndexContext(String uri, int uriLength, int uriBeginIndex, Map paramsMap) { 42 | this.uri = uri; 43 | this.uriLength = uriLength; 44 | this.uriBeginIndex = uriBeginIndex; 45 | this.paramsMap = paramsMap; 46 | } 47 | 48 | // ----- match constants ----- 49 | 50 | public static final int FULL_URI_MATCH_INDEX = -1; 51 | public static final int NO_URI_MATCH_INDEX = -2; 52 | 53 | private int partialUriMatch(Map pathParams, int uriIndex) { 54 | this.paramsMap.putAll(pathParams); 55 | this.uriBeginIndex = uriIndex; 56 | return uriIndex; 57 | } 58 | 59 | private int partialUriMatch(int uriIndex) { 60 | this.uriBeginIndex = uriIndex; 61 | return uriIndex; 62 | } 63 | 64 | private int fullUriMatch() { 65 | this.uriBeginIndex = uriLength; 66 | return FULL_URI_MATCH_INDEX; 67 | } 68 | 69 | private int fullUriMatch(Map pathParams) { 70 | this.paramsMap.putAll(pathParams); 71 | this.uriBeginIndex = uriLength; 72 | return FULL_URI_MATCH_INDEX; 73 | } 74 | 75 | // ----- match methods ----- 76 | 77 | public int dynamicUriMatch(List patternTokens, boolean attemptPartialMatch) { 78 | if (uriBeginIndex >= uriLength) { 79 | return NO_URI_MATCH_INDEX; 80 | } 81 | final Map pathParams = new HashMap(); 82 | int uriIndex = uriBeginIndex; 83 | OUTER: 84 | for (final Object token: patternTokens) { 85 | if (uriIndex >= uriLength) { 86 | return attemptPartialMatch? partialUriMatch(pathParams, uriLength): NO_URI_MATCH_INDEX; 87 | } 88 | if (token instanceof String) { 89 | final String tokenStr = (String) token; 90 | if (uri.startsWith(tokenStr, uriIndex)) { 91 | uriIndex += tokenStr.length(); 92 | // at this point, uriIndex == uriLength if last string token 93 | } else { // 'string token mismatch' implies no match 94 | return NO_URI_MATCH_INDEX; 95 | } 96 | } else { 97 | BUFFER.setLength(0); // reset buffer before use 98 | for (int j = uriIndex; j < uriLength; j++) { 99 | final char ch = uri.charAt(j); 100 | if (ch == '/') { 101 | pathParams.put(token, BUFFER.toString()); 102 | uriIndex = j; 103 | continue OUTER; 104 | } else { 105 | BUFFER.append(ch); 106 | } 107 | } 108 | pathParams.put(token, BUFFER.toString()); 109 | uriIndex = uriLength; // control reaching this point means URI template ending in ":param*" 110 | } 111 | } 112 | if (uriIndex < uriLength) { // 'tokens finished but URI still in progress' implies partial or no match 113 | return attemptPartialMatch? partialUriMatch(pathParams, uriIndex): NO_URI_MATCH_INDEX; 114 | } 115 | return fullUriMatch(pathParams); 116 | } 117 | 118 | public int dynamicUriPartialMatch(List patternTokens) { 119 | return dynamicUriMatch(patternTokens, true); 120 | } 121 | 122 | public int dynamicUriFullMatch(List patternTokens) { 123 | return dynamicUriMatch(patternTokens, false); 124 | } 125 | 126 | public int staticUriPartialMatch(String token) { 127 | if (uri.startsWith(token, uriBeginIndex)) { 128 | final int tokenLength = token.length(); 129 | return (uriLength - uriBeginIndex) == tokenLength? 130 | fullUriMatch(): partialUriMatch(uriBeginIndex + tokenLength); 131 | } 132 | return NO_URI_MATCH_INDEX; 133 | } 134 | 135 | public int staticUriFullMatch(String token) { 136 | if (uriBeginIndex == 0) { 137 | return uri.equals(token)? FULL_URI_MATCH_INDEX: NO_URI_MATCH_INDEX; 138 | } 139 | return (uri.startsWith(token, uriBeginIndex) && (uriLength - uriBeginIndex == token.length()))? 140 | FULL_URI_MATCH_INDEX: NO_URI_MATCH_INDEX; 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/calfpath/route/uri_index_match.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route.uri-index-match 11 | "Internal namespace to implement URI-index based URI match." 12 | (:require 13 | [clojure.string :as string] 14 | [calfpath.type :as t] 15 | #?(:cljs [calfpath.route.uri-index-match-impl :as cljs])) 16 | #?(:clj (:import 17 | [java.util HashMap] 18 | [clojure.lang Associative] 19 | [calfpath.route UriIndexContext]))) 20 | 21 | 22 | (defn parse-uri-template 23 | "Given a URI pattern string, e.g. '/user/:id/profile/:descriptor/' parse it and return a vector [tokens partial?] of 24 | alternating string and keyword tokens, e.g. [['/user/' :id '/profile/' :descriptor '/'] false]. Marker char is ':'." 25 | [uri-pattern] 26 | (let [pattern-length (count uri-pattern) 27 | [path partial?] (if (and (> pattern-length 1) 28 | (string/ends-with? uri-pattern "*")) 29 | [(subs uri-pattern 0 (dec pattern-length)) true] ; chop off last char 30 | [uri-pattern false]) 31 | n (count path) 32 | separator \/] 33 | (loop [i (int 0) ; current index in the URI string 34 | j (int 0) ; start index of the current token (string or keyword) 35 | s? true ; string in progress? (false implies keyword in progress) 36 | r []] 37 | (if (>= i n) 38 | [(conj r (let [t (subs path j i)] 39 | (if s? 40 | t 41 | (keyword t)))) 42 | partial?] 43 | (let [^char ch (get path i) 44 | [jn s? r] (if s? 45 | (if (= \: ch) 46 | [(unchecked-inc i) false (conj r (subs path j i))] 47 | [j true r]) 48 | (if (= separator ch) 49 | [i true (conj r (keyword (subs path j i)))] 50 | [j false r]))] 51 | (recur (unchecked-inc i) (int jn) s? r)))))) 52 | 53 | 54 | ;; ----- request state management ----- 55 | 56 | 57 | (def ^:const calfpath-context-key "Request key for token context" :calfpath/uri-index-context) 58 | 59 | 60 | (defn get-calfpath-context 61 | [request] 62 | (get request calfpath-context-key)) 63 | 64 | 65 | (defn prepare-request 66 | [request path-params-key] 67 | (if (contains? request calfpath-context-key) 68 | request 69 | #?(:cljs (assoc request calfpath-context-key (cljs/make-context (:uri request))) 70 | :clj (let [path-params (HashMap.)] 71 | (-> ^clojure.lang.Associative request 72 | (.assoc calfpath-context-key (UriIndexContext. (:uri request) path-params)) 73 | (.assoc path-params-key path-params)))))) 74 | 75 | 76 | (defn update-path-params 77 | "Update request with path params after a successful match." 78 | [request path-params-key] 79 | #?(:cljs (->> (get-calfpath-context request) 80 | cljs/get-path-params 81 | (assoc request path-params-key)) 82 | :clj request)) 83 | 84 | 85 | ;; ----- matcher/matchex support ----- 86 | 87 | 88 | (def ^:const FULL-URI-MATCH-INDEX -1) 89 | (def ^:const NO-URI-MATCH-INDEX -2) 90 | 91 | 92 | (defn match-static-uri-partial [request static-tokens params-key] 93 | (when (not= NO-URI-MATCH-INDEX 94 | #?(:cljs (cljs/static-uri-partial-match (get-calfpath-context request) static-tokens) 95 | :clj (.staticUriPartialMatch ^UriIndexContext (get-calfpath-context request) static-tokens))) 96 | #?(:cljs (update-path-params request params-key) 97 | :clj request))) 98 | 99 | 100 | (defn match-static-uri-full [request static-tokens params-key] 101 | (when (not= NO-URI-MATCH-INDEX 102 | #?(:cljs (cljs/static-uri-full-match (get-calfpath-context request) static-tokens) 103 | :clj (.staticUriFullMatch ^UriIndexContext (get-calfpath-context request) static-tokens))) 104 | #?(:cljs (update-path-params request params-key) 105 | :clj request))) 106 | 107 | 108 | (defn match-dynamic-uri-partial [request uri-template params-key] 109 | (when (not= NO-URI-MATCH-INDEX 110 | #?(:cljs (cljs/dynamic-uri-partial-match (get-calfpath-context request) uri-template) 111 | :clj (.dynamicUriPartialMatch ^UriIndexContext (get-calfpath-context request) uri-template))) 112 | #?(:cljs (update-path-params request params-key) 113 | :clj request))) 114 | 115 | 116 | (defn match-dynamic-uri-full [request uri-template params-key] 117 | (when (not= NO-URI-MATCH-INDEX 118 | #?(:cljs (cljs/dynamic-uri-full-match (get-calfpath-context request) uri-template) 119 | :clj (.dynamicUriFullMatch ^UriIndexContext (get-calfpath-context request) uri-template))) 120 | #?(:cljs (update-path-params request params-key) 121 | :clj request))) 122 | 123 | 124 | ;; ----- IRouteMatcher ----- 125 | 126 | 127 | (def route-matcher 128 | (reify t/IRouteMatcher 129 | (-parse-uri-template [_ uri-pattern] (parse-uri-template uri-pattern)) 130 | (-get-static-uri-template [_ uri-pattern-tokens] (when (and (= 1 (count uri-pattern-tokens)) 131 | (string? (first uri-pattern-tokens))) 132 | (first uri-pattern-tokens))) 133 | (-initialize-request [_ request params-key] (prepare-request request params-key)) 134 | (-static-uri-partial-match [_ req static-token params-key] (match-static-uri-partial req static-token params-key)) 135 | (-static-uri-full-match [_ req static-token params-key] (match-static-uri-full req static-token params-key)) 136 | (-dynamic-uri-partial-match [_ req uri-template params-key] (match-dynamic-uri-partial req uri-template params-key)) 137 | (-dynamic-uri-full-match [_ req uri-template params-key] (match-dynamic-uri-full req uri-template params-key)))) 138 | -------------------------------------------------------------------------------- /src/calfpath/route/uri_token_match_impl.cljs: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route.uri-token-match-impl 11 | "CLJS implementation for URI-tokens based URI matching.") 12 | 13 | 14 | (defrecord TokenContext [^:mutable uri-tokens 15 | ^:mutable ^long uri-token-count 16 | ^:mutable path-params]) 17 | 18 | 19 | (defn make-context 20 | ^TokenContext [uri-tokens] 21 | (TokenContext. uri-tokens (count uri-tokens) {})) 22 | 23 | 24 | (defn get-path-params 25 | [^IndexContext context] 26 | (.-path-params context)) 27 | 28 | 29 | (defn update-uri-tokens 30 | [^IndexContext context uri-tokens] 31 | (set! (.-uri-tokens context) uri-tokens) 32 | (set! (.-uri-token-count context) (count uri-tokens))) 33 | 34 | 35 | ;; ----- URI matching ----- 36 | 37 | 38 | (def FULL-URI-MATCH-TOKENS []) 39 | 40 | 41 | (defn partial-match 42 | ([^IndexContext context uri-tokens] 43 | (set! (.-uri-tokens context) uri-tokens) 44 | (set! (.-uri-token-count context) (count uri-tokens)) 45 | uri-tokens) 46 | ([^IndexContext context uri-tokens path-params] 47 | (set! (.-path-params context) (conj (.-path-params context) path-params)) 48 | (set! (.-uri-tokens context) uri-tokens) 49 | (set! (.-uri-token-count context) (count uri-tokens)) 50 | uri-tokens)) 51 | 52 | 53 | (defn full-match 54 | ([^IndexContext context] (partial-match context FULL-URI-MATCH-TOKENS)) 55 | ([^IndexContext context path-params] (partial-match context FULL-URI-MATCH-TOKENS path-params))) 56 | 57 | 58 | ;; ~~~ match fns ~~~ 59 | 60 | 61 | (defn dynamic-uri-partial-match 62 | "(Partial) Match URI tokens (vector) against URI-pattern tokens. Optimized for dynamic routes. 63 | Return (vector of) remaining URI tokens on match, interpreted as follows: 64 | 65 | | Condition | Meaning | 66 | |-----------|---------------| 67 | | `nil` | no match | 68 | | empty | full match | 69 | | non-empty | partial match |" 70 | [^TokenContext context pattern-tokens] 71 | (let [pattern-token-count (count pattern-tokens)] 72 | (when (>= (.-uri-token-count context) pattern-token-count) 73 | (loop [path-params (transient {}) 74 | token-index 0] 75 | (let [each-uri-token (get (.-uri-tokens context) token-index) 76 | each-pattern-token (get pattern-tokens token-index)] 77 | (if (< token-index pattern-token-count) 78 | (when-not (and (string? each-pattern-token) 79 | (not= each-uri-token each-pattern-token)) 80 | (recur 81 | (if (string? each-pattern-token) 82 | path-params 83 | (assoc! path-params each-pattern-token each-uri-token)) 84 | (unchecked-inc token-index))) 85 | (if (> (.-uri-token-count context) pattern-token-count) 86 | (partial-match context 87 | (subvec (.-uri-tokens context) pattern-token-count (.-uri-token-count context)) 88 | (persistent! path-params)) 89 | (full-match context 90 | (persistent! path-params))))))))) 91 | 92 | 93 | (defn dynamic-uri-full-match 94 | "(Full) Match URI tokens (vector) against URI-pattern tokens. Optimized for dynamic routes. 95 | Return (vector of) remaining URI tokens on match, interpreted as follows: 96 | 97 | | Condition | Meaning | 98 | |-----------|---------------| 99 | | `nil` | no match | 100 | | empty | full match |" 101 | [^TokenContext context pattern-tokens] 102 | (let [pattern-token-count (count pattern-tokens)] 103 | (when (= (.-uri-token-count context) pattern-token-count) 104 | (loop [path-params (transient {}) 105 | token-index 0] 106 | (let [each-uri-token (get (.-uri-tokens context) token-index) 107 | each-pattern-token (get pattern-tokens token-index)] 108 | (if (< token-index pattern-token-count) 109 | (when-not (and (string? each-pattern-token) 110 | (not= each-uri-token each-pattern-token)) 111 | (recur 112 | (if (string? each-pattern-token) 113 | path-params 114 | (assoc! path-params each-pattern-token each-uri-token)) 115 | (unchecked-inc token-index))) 116 | (full-match context (persistent! path-params)))))))) 117 | 118 | 119 | (defn static-uri-partial-match 120 | "(Full) Match URI tokens against URI-pattern string tokens. Only for static routes. 121 | Return (vector) remaining-uri-tokens not yet matched - interpret as follows: 122 | 123 | | Condition | Meaning | 124 | |-----------|---------------| 125 | | `nil` | no match | 126 | | empty | full match | 127 | | non-empty | partial match |" 128 | [^TokenContext context static-tokens] 129 | (let [static-token-count (count static-tokens)] 130 | (when (>= (.-uri-token-count context) static-token-count) 131 | (loop [i 0] 132 | (if (< i static-token-count) 133 | (when (= (get (.-uri-tokens context) i) 134 | (get static-tokens i)) 135 | (recur (unchecked-inc i))) 136 | (if (> (.-uri-token-count context) static-token-count) 137 | (partial-match context (subvec (.-uri-tokens context) static-token-count (.-uri-token-count context))) 138 | (full-match context))))))) 139 | 140 | 141 | (defn static-uri-full-match 142 | "(Full) Match URI tokens against URI-pattern string tokens. Only for static routes. 143 | Return (vector) remaining-uri-tokens not yet matched - interpret as follows: 144 | 145 | | Condition | Meaning | 146 | |-----------|---------------| 147 | | `nil` | no match | 148 | | empty | full match |" 149 | [^TokenContext context static-tokens] 150 | (let [static-token-count (count static-tokens)] 151 | (when (= (.-uri-token-count context) static-token-count) 152 | (loop [i 0] 153 | (if (< i static-token-count) 154 | (when (= (get (.-uri-tokens context) i) 155 | (get static-tokens i)) 156 | (recur (unchecked-inc i))) 157 | FULL-URI-MATCH-TOKENS))))) 158 | -------------------------------------------------------------------------------- /test/calfpath/route_handler_test.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route-handler-test 11 | (:require 12 | #?(:cljs [cljs.test :refer-macros [deftest is testing]] 13 | :clj [clojure.test :refer [deftest is testing]]) 14 | #?(:cljs [calfpath.route :as r :include-macros true] 15 | :clj [calfpath.route :as r]))) 16 | 17 | 18 | (defn handler 19 | [ks] 20 | (fn [request] 21 | (select-keys request (conj ks :request-method)))) 22 | 23 | 24 | (def all-routes 25 | [{:uri "/info/:token/" :method :get :handler (handler [:path-params]) :name "info"} 26 | {:uri "/album/:lid/artist/:rid/" :method :get :handler (handler [:path-params])} 27 | {:uri "/user/:id/profile/:type/" 28 | :nested [{:method :get :handler (handler [:path-params]) :name "get.user.profile"} 29 | {:method :patch :handler (handler [:path-params]) :name "update.user.profile"} 30 | {:method :delete :handler (handler [:path-params]) :name "delete.user.profile"}]} 31 | {:uri "/user/:id/permissions/" 32 | :nested [{:method :get :handler (handler [:path-params]) :name "get.user.permissions"} 33 | {:method :post :handler (handler [:path-params]) :name "create.user.permission"} 34 | {:method :put :handler (handler [:path-params]) :name "replace.user.permissions"}]} 35 | {:uri "/hello/1234/" :handler (handler [])}]) 36 | 37 | 38 | (def all-partial-routes 39 | [{:uri "/info/:token/" :method :get :handler (handler [:path-params]) :name "info"} 40 | {:uri "/foo*" :nested [{:method :get :uri "" :handler (handler [])}]} 41 | {:uri "/bar/:bar-id*" :nested [{:method :get :uri "" :handler (handler [:path-params])}]} 42 | {:uri "/v1*" :nested [{:uri "/orgs*" :nested [{:uri "/:org-id/topics" :handler (handler [:path-params])}]}]} 43 | {:uri "/album/:lid*" 44 | :nested [{:uri "/artist/:rid/" 45 | :method :get :handler (handler [:path-params])}]} 46 | {:uri "/user/:id*" 47 | :nested [{:uri "/profile/:type/" 48 | :nested [{:method :get :handler (handler [:path-params]) :name "get.user.profile"} 49 | {:method :patch :handler (handler [:path-params]) :name "update.user.profile"} 50 | {:method :delete :handler (handler [:path-params]) :name "delete.user.profile"}]} 51 | {:uri "/permissions/" 52 | :nested [{:method :get :handler (handler [:path-params]) :name "get.user.permissions"} 53 | {:method :post :handler (handler [:path-params]) :name "create.user.permission"} 54 | {:method :put :handler (handler [:path-params]) :name "replace.user.permissions"}]}]} 55 | {:uri "/hello/1234/" :handler (handler [])}]) 56 | 57 | 58 | (def final-routes (r/compile-routes all-routes {:params-key :path-params})) 59 | 60 | 61 | (def final-partial-routes (r/compile-routes all-partial-routes {:params-key :path-params})) 62 | 63 | 64 | (def flat-400 "400 Bad request. URI does not match any available uri-template. 65 | 66 | Available URI templates: 67 | /album/:lid/artist/:rid/ 68 | /hello/1234/ 69 | /info/:token/ 70 | /user/:id*") 71 | 72 | 73 | (def partial-400 "400 Bad request. URI does not match any available uri-template. 74 | 75 | Available URI templates: 76 | /album/:lid* 77 | /bar/:bar-id* 78 | /foo* 79 | /hello/1234/ 80 | /info/:token/ 81 | /user/:id* 82 | /v1*") 83 | 84 | 85 | (defn routes-helper 86 | [handler body-400] 87 | (is (= {:request-method :get 88 | :path-params {:token "status"}} 89 | (handler {:uri "/info/status/" :request-method :get}))) 90 | (is (= {:status 405 91 | :headers {"Allow" "GET" "Content-Type" "text/plain"} 92 | :body "405 Method not supported. Allowed methods are: GET"} 93 | (handler {:uri "/info/status/" :request-method :post}))) 94 | (is (= {:request-method :get 95 | :path-params {:lid "10" 96 | :rid "20"}} 97 | (handler {:uri "/album/10/artist/20/" :request-method :get}))) 98 | (is (= {:request-method :get 99 | :path-params {:id "id-1" 100 | :type "type-2"}} 101 | (handler {:uri "/user/id-1/profile/type-2/" :request-method :get}))) 102 | (is (= {:request-method :post 103 | :path-params {:id "id-2"}} 104 | (handler {:uri "/user/id-2/permissions/" :request-method :post}))) 105 | (is (= {:request-method :get} 106 | (handler {:uri "/hello/1234/" :request-method :get}))) 107 | (is (= {:status 400 108 | :headers {"Content-Type" "text/plain"} 109 | :body body-400} 110 | (handler {:uri "/bad/uri" :request-method :get}))) 111 | (is (= {:status 405 112 | :headers {"Allow" "GET, POST, PUT", "Content-Type" "text/plain"} 113 | :body "405 Method not supported. Allowed methods are: GET, POST, PUT"} 114 | (handler {:uri "/user/123/permissions/" :request-method :bad})))) 115 | 116 | 117 | (defn partial-routes-helper 118 | [handler body-400] 119 | (is (= {:request-method :get} 120 | (handler {:uri "/foo" :request-method :get})) "partial termination - foo") 121 | (is (= {:request-method :get 122 | :path-params {:bar-id "98"}} 123 | (handler {:uri "/bar/98" :request-method :get})) "partial termination - bar") 124 | (is (= {:request-method :get 125 | :path-params {:org-id "87"}} 126 | (handler {:uri "/v1/orgs/87/topics" :request-method :get})) "partial termination - v1/org")) 127 | 128 | 129 | (deftest test-walker 130 | (testing "walker (path params)" 131 | (routes-helper (partial r/dispatch final-routes) flat-400)) 132 | (testing "walker partial (path params)" 133 | (routes-helper (partial r/dispatch final-partial-routes) partial-400) 134 | (partial-routes-helper (partial r/dispatch final-partial-routes) partial-400))) 135 | 136 | 137 | #?(:clj (deftest test-unrolled 138 | (testing "unrolled (path params)" 139 | (routes-helper (r/make-dispatcher final-routes) flat-400)) 140 | (testing "unrolled partial (path params)" 141 | (routes-helper (r/make-dispatcher final-partial-routes) partial-400) 142 | (partial-routes-helper (r/make-dispatcher final-partial-routes) partial-400)))) 143 | -------------------------------------------------------------------------------- /src/calfpath/route/uri_token_match.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route.uri-token-match 11 | "Internal namespace to implement URI-tokens based URI match." 12 | (:require 13 | [clojure.string :as string] 14 | [calfpath.type :as t] 15 | #?(:cljs [calfpath.route.uri-token-match-impl :as cljs])) 16 | #?(:clj (:import 17 | [java.util HashMap] 18 | [clojure.lang Associative] 19 | [calfpath.route UriTokenContext]))) 20 | 21 | 22 | ;; ----- URI and URI-template parsing ----- 23 | 24 | 25 | (defn parse-uri-tokens* 26 | [uri] 27 | (let [uri-len (count uri)] 28 | (loop [tcoll (transient []) 29 | token "" 30 | index 1] 31 | (if (< index uri-len) 32 | (let [ch (get uri index)] 33 | (if (= ch \/) 34 | (recur (conj! tcoll token) "" (unchecked-inc index)) 35 | (recur tcoll (str token ch) (unchecked-inc index)))) 36 | (persistent! (conj! tcoll token)))))) 37 | 38 | 39 | (defn parse-uri-tokens 40 | [uri] 41 | #?(:cljs (parse-uri-tokens* uri) 42 | :clj (UriTokenContext/parseUriTokens uri))) 43 | 44 | 45 | (defn parse-uri-template 46 | "Given a URI pattern string, e.g. '/user/:id/profile/:descriptor/' parse it and return a vector [tokens partial?] of 47 | string and keyword tokens, e.g. [['user' :id 'profile' :descriptor ''] false]. The marker char is ':'." 48 | [uri-pattern] 49 | (let [pattern-length (count uri-pattern) 50 | [path partial?] (if (and (> pattern-length 1) 51 | (string/ends-with? uri-pattern "*")) 52 | [(subs uri-pattern 0 (dec pattern-length)) true] ; chop off last wildcard char 53 | [uri-pattern false]) 54 | tokens (cond 55 | (= "" path) [] 56 | (string/starts-with? 57 | path "/") (parse-uri-tokens path) 58 | :otherwise (throw (ex-info (str "Expected URI pattern to start with '/', but found " 59 | (pr-str path)) {:path path})))] 60 | (as-> tokens <> 61 | (mapv (fn [t] 62 | (if (string/starts-with? t ":") 63 | (keyword (subs t 1)) 64 | t)) <>) 65 | (vector <> partial?)))) 66 | 67 | 68 | ;; ----- request state management ----- 69 | 70 | 71 | (def ^:const calfpath-context-key "Request key for token context" :calfpath/token-context) 72 | 73 | 74 | (defn get-calfpath-context 75 | [request] 76 | (get request calfpath-context-key)) 77 | 78 | 79 | (defn prepare-request 80 | [request path-params-key] 81 | (if (contains? request calfpath-context-key) 82 | request 83 | (let [uri-tokens (parse-uri-tokens (:uri request))] 84 | #?(:cljs (assoc request calfpath-context-key (cljs/make-context uri-tokens)) 85 | :clj (let [path-params (HashMap.)] 86 | (-> ^clojure.lang.Associative request 87 | (.assoc calfpath-context-key (UriTokenContext. uri-tokens path-params)) 88 | (.assoc path-params-key path-params))))))) 89 | 90 | 91 | (defn update-path-params 92 | [request path-params-key] 93 | #?(:cljs (let [context (get-calfpath-context request)] 94 | (assoc request path-params-key (cljs/get-path-params context))) 95 | :clj request)) 96 | 97 | 98 | ;; ----- matcher/matchex support ----- 99 | 100 | 101 | (defn match-static-uri-partial [request static-tokens params-key] 102 | (when-some [rem-tokens #?(:cljs (-> (get-calfpath-context request) 103 | (cljs/static-uri-partial-match static-tokens)) 104 | :clj (-> ^UriTokenContext (get-calfpath-context request) 105 | (.staticUriPartialMatch static-tokens)))] 106 | #?(:cljs (update-path-params request params-key) 107 | :clj request))) 108 | 109 | 110 | (defn match-static-uri-full [request static-tokens params-key] 111 | (when-some [rem-tokens #?(:cljs (-> (get-calfpath-context request) 112 | (cljs/static-uri-full-match static-tokens)) 113 | :clj (-> ^UriTokenContext (get-calfpath-context request) 114 | (.staticUriFullMatch static-tokens)))] 115 | #?(:cljs (update-path-params request params-key) 116 | :clj request))) 117 | 118 | 119 | (defn match-dynamic-uri-partial [request uri-template params-key] 120 | (when-some [rem-tokens #?(:cljs (-> (get-calfpath-context request) 121 | (cljs/dynamic-uri-partial-match uri-template)) 122 | :clj (-> ^UriTokenContext (get-calfpath-context request) 123 | (.dynamicUriPartialMatch uri-template)))] 124 | #?(:cljs (update-path-params request params-key) 125 | :clj request))) 126 | 127 | 128 | (defn match-dynamic-uri-full [request uri-template params-key] 129 | (when-some [rem-tokens #?(:cljs (-> (get-calfpath-context request) 130 | (cljs/dynamic-uri-full-match uri-template)) 131 | :clj (-> ^UriTokenContext (get-calfpath-context request) 132 | (.dynamicUriFullMatch uri-template)))] 133 | #?(:cljs (update-path-params request params-key) 134 | :clj request))) 135 | 136 | 137 | ;; ----- IRouteMatcher ----- 138 | 139 | 140 | (def route-matcher 141 | (reify t/IRouteMatcher 142 | (-parse-uri-template [_ uri-pattern] (parse-uri-template uri-pattern)) 143 | (-get-static-uri-template [_ uri-pattern-tokens] (when (every? string? uri-pattern-tokens) 144 | uri-pattern-tokens)) 145 | (-initialize-request [_ request params-key] (prepare-request request params-key)) 146 | (-static-uri-partial-match [_ req static-tokens params-key] (match-static-uri-partial req static-tokens params-key)) 147 | (-static-uri-full-match [_ req static-tokens params-key] (match-static-uri-full req static-tokens params-key)) 148 | (-dynamic-uri-partial-match [_ req uri-template params-key] (match-dynamic-uri-partial req uri-template params-key)) 149 | (-dynamic-uri-full-match [_ req uri-template params-key] (match-dynamic-uri-full req uri-template params-key)))) 150 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # calfpath Changes and TODO 2 | 3 | 4 | ## TODO 5 | 6 | * [TODO - BREAKING CHANGE] Rename the abstraction uri-template to path 7 | * [TODO - BREAKING CHANGE] Consider `clojure.walk` as routes navigation tool 8 | * [TODO] Include [quickstart] complete server examples using Ring and Machhiato 9 | * [TODO] Return numeric direction (lower/higher) indicator in URI matching 10 | * [Todo] Do not finalize FULL-MATCH (in request) in a partial-match request 11 | - because we don't know if it's a full match; it's determined by a future token 12 | 13 | 14 | ## 0.8.1 / 2021-February-03 15 | 16 | - Bugfix 17 | - Expect only index-key (e.g. `:id`) in indexable route in `calfpath.route/make-index` 18 | - Bug: Expects `:handler` to be present in identifiable route 19 | - Include URI pattern wildcard suffix in `calfpath.route/make-index` 20 | - Bug: URI wildcard suffix is stripped when indexing routes 21 | - Group common URI pattern into sub-routes 22 | - Bug: Common URI pattern in routes should be grouped together 23 | - Enhancement 24 | - Support path params notation `:param` and `{param}`, e.g. `/foo/:param/bar` and `/foo/{param}/bar` 25 | 26 | 27 | ## 0.8.0 / 2020-December-17 28 | 29 | * [BREAKING CHANGE] Drop support 30 | - Java 6 (JDK 1.6) - for compatibility with Java 15 compiler (cannot emit 1.6 class files) 31 | - Clojure 1.7 - to use string utility fns introduced in Clojure 1.8 for CLJS 32 | * [BREAKING CHANGE] Rename `calfpath.route/assoc-spec-to-request` to `assoc-route-to-request` 33 | * ClojureScript compatibility 34 | - Excluding `calfpath.route/make-dispatcher` and matchex optimization (JVM only) 35 | * Data-driven Routes 36 | - [BREAKING CHANGE] Put URI params under `:path-params` key in request 37 | - Support for easy route syntax 38 | - Accept `options` argument in function `calfpath.route/make-dispatcher` 39 | - Show URI patterns in sorted order on no URI match (HTTP 400) 40 | - Fix issue where (tidy) wildcard did not prefix path-param token with `/` 41 | * Bidirectional routing - ID based Ring request generation (ns `calfpath.route`) 42 | - `make-index` 43 | - `realize-uri` 44 | - `template->request` 45 | * Performance tweaks 46 | - Automatic prefix-segregation using wildcard nested routing 47 | - See options `:tidy?` and `:tidy-threshold` in `calfpath.route/compile-routes` 48 | - Add large routes (OpenSensors) to performance benchmarks 49 | - Drop `calfpath.MatchResult` in favour of 2-element array 50 | - Faster match for static URI string (full/partial) 51 | - Use mutable URI end-index for tracking URI match 52 | - Use passthrough params-map from request to add new URI params 53 | - Matchex 54 | - Direct handler invocation for identity matcher 55 | - Use `if-some`/`when-some` instead of `if-let`/`when-let` everywhere 56 | - Use bulk methods matcher when all routes match methods 57 | * Documentation 58 | - Dispatch macros 59 | - Data driven routes 60 | - Concepts 61 | - Easy routes notation 62 | - Applying middleware 63 | - Bidirectional routing 64 | 65 | 66 | ## 0.7.2 / 2019-January-15 67 | 68 | * Add a middleware to add route to the request map 69 | - `calfpath.route/assoc-spec-to-request-middleware` 70 | 71 | 72 | ## 0.7.1 / 2019-January-04 73 | 74 | * Routes 75 | - Add utility fn `calfpath.route/prewalk-routes` 76 | - Fix reporting "URI templates" in routes fallback-400 handler 77 | - Introduce `:full-uri` kwarg in `calfpath.route/compile-routes` as reference key 78 | 79 | 80 | ## 0.7.0 / 2019-January-03 81 | 82 | * Routes 83 | * [BREAKING CHANGE] Allow argument `params-key` instead of looking up route spec 84 | * Accept `params-key` in `calfpath.route/make-uri-matcher` - no route spec lookup 85 | * Changes to `calfpath.route/compile-routes` 86 | * Drop support for kwargs `:split-params?` and `:uri-params-key` 87 | * Accept optional kwarg `:params-key` 88 | * Performance 89 | * Include [Reitit](https://github.com/metosin/reitit) among performance benchmarks 90 | * Avoid allocating MatchResult object on route full-match with no params 91 | * [IMPL CHANGE] Drop `MatchResult.fullMatch()` in favour of `MatchResult.FULL_MATCH_NO_PARAMS` 92 | * Allocate param map (in `Util.matchURI()`) to hold only as many params as likely 93 | 94 | 95 | ## 0.6.0 / 2018-April-30 96 | 97 | * [BREAKING CHANGE] Drop support for Clojure versions 1.5 and 1.6 98 | * Supported Clojure versions: 1.7, 1.8, 1.9 99 | * [BREAKING CHANGE] Rename `calfpath.route/make-routes` to `calfpath.route/compile-routes` 100 | * Routes: Put URI params under an optional key in request map (by adding pair `:uri-params ` to route) 101 | * [BREAKING CHANGE] Update `calfpath.route/make-uri-matcher` arity - accept an extra argument `uri-params-key` 102 | * [BREAKING CHANGE] In middleware `lift-key-middleware` accept `lift-keys` collection instead of single `lift-key` 103 | * Refactor `calfpath.route/compile-routes` 104 | * Add option kwargs 105 | * `:uri-params-key` to find out where to place URI params in the request map 106 | * `:uri-params-val` to specify where to place URI params in the request map 107 | * `:split-params?` to determine whether to split URI params under a separate key in request map 108 | * `:trailing-slash` to specify action to perform with trailing slash (`:add` or `:remove`) to URI patterns 109 | * Workaround for `conj` bug in Aleph (0.4.4) and Immutant (2.1.10) requests 110 | * https://github.com/ztellman/aleph/issues/374 111 | * https://issues.jboss.org/browse/IMMUTANT-640 112 | * Support for asynchronous Ring handlers in routes API 113 | * Performance optimization 114 | * Make fallback matches faster with matchex optimization 115 | * Make keyword method matches faster using `identical?` instead of `=` 116 | * Route middleware 117 | * `calfpath.route/assoc-kv-middleware` - associate key/value pairs corresponding to a main key in a route 118 | * `calfpath.route/trailing-slash-middleware` - drop or add trailing slash to non-partial URI matchers 119 | * Overhaul performance benchmarks 120 | * Use external handler fns in routing code 121 | * Fix parameter extraction with Clout 122 | * Add benchmarks for other routing libraries 123 | * Ataraxy 124 | * Bidi 125 | 126 | 127 | ## 0.5.0 / 2017-December-10 128 | 129 | * Routes 130 | * [BREAKING CHANGE] Matcher now returns potentially-updated request, or `nil` 131 | * [BREAKING CHANGE] Route handler now has the same arity as Ring handler 132 | * Path-params associated in request map under respective keys 133 | * This allows Ring middleware to be applied to route handlers 134 | * [BREAKING CHANGE] Drop `ring-handler-middleware` 135 | * Documentation 136 | * Fetching/rendering static files or classpath resources using Ring middleware 137 | * Documentation for routes (keys other than essential ones) 138 | 139 | 140 | ## 0.4.0 / 2016-October-13 141 | 142 | * Make URI-match work for partial matches and URI prefixes 143 | * A partial URI pattern may be expressed with a `*` suffix 144 | * Zero or more partial URI patterns may exist in a route tree 145 | * This may impact how URIs in HTTP-400 responses are generated 146 | * Middleware 147 | * A helper fn `calfpath.route/update-in-each-route` to apply route attribute wrapper to specs 148 | * A lift-key middleware `calfpath.route/lift-key-middleware` to split routes with mixed specs 149 | * A ring-route middleware `calfpath.route/ring-handler-middleware` to wrap Ring handlers into route handlers 150 | * Helper fn `calfpath.route/make-routes` to build routes from given route specs 151 | * Allow non-literal string URI-patterns in `calfpath.core/->uri` 152 | * Fix `calfpath.route/update-fallback-400` to add fallback 400 route on one or more URI entry, instead of all 153 | * BREAKING CHANGE: Drop `calfpath.core/make-uri-handler` in favor of Calfpath routes 154 | 155 | 156 | ## 0.3.0 / 2016-May-27 157 | 158 | * Support for extensible routes as a first-class abstraction 159 | * A dispatcher fn that walks given routes to match request and invoke corresponding handler 160 | * An optimized (through loop unrolling) way to create a dispatcher fn from given routes 161 | * Helper fns to manipulate routes at shallow and deep levels 162 | 163 | 164 | ## 0.2.1 / 2016-February-17 165 | 166 | * Add support for `PATCH` HTTP method 167 | 168 | 169 | ## 0.2.0 / 2015-June-06 170 | 171 | * Dispatch (fn) on URI template by calling fns, that returns a Ring handler fn 172 | 173 | 174 | ## 0.1.0 / 2015-June-04 175 | 176 | * Dispatch (macro) on URI template by evaluating expression in lexical scope 177 | * Dispatch (macro) on HTTP method by evaluating expression 178 | -------------------------------------------------------------------------------- /java-src/calfpath/Util.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shantanu Kumar. All rights reserved. 2 | // The use and distribution terms for this software are covered by the 3 | // Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | // which can be found in the file LICENSE at the root of this distribution. 5 | // By using this software in any fashion, you are agreeing to be bound by 6 | // the terms of this license. 7 | // You must not remove this notice, or any other, from this software. 8 | 9 | 10 | package calfpath; 11 | 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | public class Util { 18 | 19 | public static final Object[] NO_URI_MATCH = null; 20 | 21 | @SuppressWarnings("unchecked") 22 | public static final Map NO_PARAMS = Collections.EMPTY_MAP; 23 | 24 | public static final int FULL_URI_MATCH_INDEX = -1; 25 | 26 | public static final Object[] FULL_URI_MATCH_NO_PARAMS = new Object[] {NO_PARAMS, FULL_URI_MATCH_INDEX}; 27 | 28 | public static Object[] partialURIMatch(Map params, int endIndex) { 29 | return new Object[] {params, endIndex}; 30 | } 31 | 32 | public static Object[] partialURIMatch(int endIndex) { 33 | return new Object[] {NO_PARAMS, endIndex}; 34 | } 35 | 36 | public static Object[] fullURIMatch(Map params) { 37 | return new Object[] {params, FULL_URI_MATCH_INDEX}; 38 | } 39 | 40 | /** 41 | * Match a URI against URI-pattern tokens and return match result on successful match, {@code null} otherwise. 42 | * When argument {@code attemptPartialMatch} is {@code true}, both full and partial match are attempted without any 43 | * performance penalty. When argument {@code attemptPartialMatch} is {@code false}, only a full match is attempted. 44 | * @param uri the URI string to match 45 | * @param beginIndex beginning index in the URI string to match 46 | * @param patternTokens URI pattern tokens to match the URI against 47 | * @param attemptPartialMatch whether attempt partial match when full match is not possible 48 | * @return a match result on successful match, {@literal null} otherwise 49 | */ 50 | public static Object[] matchURI(String uri, int beginIndex, List patternTokens, boolean attemptPartialMatch, 51 | Map paramsMap) { 52 | final int tokenCount = patternTokens.size(); 53 | final Object firstToken = patternTokens.get(0); 54 | if (beginIndex == FULL_URI_MATCH_INDEX) { // if already a full-match then no need to match further 55 | if (tokenCount == 1 && "".equals(firstToken)) return FULL_URI_MATCH_NO_PARAMS; 56 | return NO_URI_MATCH; 57 | } 58 | // if length==1 and token is string, then it's a static URI 59 | if (tokenCount == 1 && firstToken instanceof String) { 60 | final String staticPath = (String) firstToken; 61 | if (uri.startsWith(staticPath, beginIndex)) { // URI begins with the path, so at least partial match exists 62 | if ((uri.length() - beginIndex) == staticPath.length()) { // if full match exists, then return as such 63 | return FULL_URI_MATCH_NO_PARAMS; 64 | } 65 | return attemptPartialMatch? partialURIMatch(beginIndex + staticPath.length()): NO_URI_MATCH; 66 | } else { 67 | return NO_URI_MATCH; 68 | } 69 | } 70 | final int uriLength = uri.length(); 71 | final Map pathParams = (paramsMap == null || paramsMap.isEmpty())? 72 | new HashMap(tokenCount): paramsMap; 73 | int uriIndex = beginIndex; 74 | OUTER: 75 | for (final Object token: patternTokens) { 76 | if (uriIndex >= uriLength) { 77 | return attemptPartialMatch? partialURIMatch(pathParams, uriIndex): NO_URI_MATCH; 78 | } 79 | if (token instanceof String) { 80 | final String tokenStr = (String) token; 81 | if (uri.startsWith(tokenStr, uriIndex)) { 82 | uriIndex += tokenStr.length(); // now i==n if last string token 83 | } else { // 'string token mismatch' implies no match 84 | return NO_URI_MATCH; 85 | } 86 | } else { 87 | final StringBuilder sb = new StringBuilder(); 88 | for (int j = uriIndex; j < uriLength; j++) { // capture param chars in one pass 89 | final char ch = uri.charAt(j); 90 | if (ch == '/') { // separator implies we got param value, now continue 91 | pathParams.put(token, sb.toString()); 92 | uriIndex = j; 93 | continue OUTER; 94 | } else { 95 | sb.append(ch); 96 | } 97 | } 98 | // 'separator not found' implies URI has ended 99 | pathParams.put(token, sb.toString()); 100 | uriIndex = uriLength; 101 | } 102 | } 103 | if (uriIndex < uriLength) { // 'tokens finished but URI still in progress' implies partial or no match 104 | return attemptPartialMatch? partialURIMatch(pathParams, uriIndex): NO_URI_MATCH; 105 | } 106 | return fullURIMatch(pathParams); 107 | } 108 | 109 | public static final int NO_URI_MATCH_INDEX = -2; 110 | 111 | /** 112 | * Match given URI against string token and return {@code beginIndex} as follows: 113 | * incremented : partial match 114 | * {@code -1} : full match 115 | * {@code -2} : no match 116 | * Attempt partial match when full match is not possible. 117 | * @param uri the URI string to match 118 | * @param beginIndex beginning index in the URI string to match 119 | * @param token URI token string to match against 120 | * @return incremented (partial match), -1 (full match) or -2 (no match) 121 | */ 122 | public static int partialMatchURIString(String uri, int beginIndex, String token) { 123 | if (beginIndex == 0) { 124 | if (uri.startsWith(token)) { 125 | final int tokenLength = token.length(); 126 | return (uri.length() == tokenLength)? FULL_URI_MATCH_INDEX: tokenLength; 127 | } else { 128 | return NO_URI_MATCH_INDEX; 129 | } 130 | } 131 | if (beginIndex > 0) { 132 | if (uri.startsWith(token, beginIndex)) { 133 | if ((uri.length() - beginIndex) == token.length()) { 134 | return FULL_URI_MATCH_INDEX; // full URI match 135 | } 136 | return beginIndex + token.length(); // partial URI match 137 | } else { 138 | return NO_URI_MATCH_INDEX; 139 | } 140 | } 141 | if (beginIndex == FULL_URI_MATCH_INDEX) { 142 | if ("".equals(token)) return FULL_URI_MATCH_INDEX; 143 | return NO_URI_MATCH_INDEX; 144 | } 145 | return NO_URI_MATCH_INDEX; // no URI match 146 | } 147 | 148 | /** 149 | * Match given URI against string token and return {@code beginIndex} as follows: 150 | * {@code -1} : full match 151 | * {@code -2} : no match 152 | * Do not attempt partial match - only attempt full match. 153 | * @param uri the URI string to match 154 | * @param beginIndex beginning index in the URI string to match 155 | * @param token URI token string to match against 156 | * @return -1 (full match) or -2 (no match) 157 | */ 158 | public static int fullMatchURIString(String uri, int beginIndex, String token) { 159 | if (beginIndex == 0) { 160 | return uri.equals(token)? FULL_URI_MATCH_INDEX: NO_URI_MATCH_INDEX; 161 | } 162 | if (beginIndex > 0) { 163 | return (uri.startsWith(token, beginIndex) && (uri.length() - beginIndex) == token.length())? 164 | FULL_URI_MATCH_INDEX: NO_URI_MATCH_INDEX; 165 | } 166 | if (beginIndex == FULL_URI_MATCH_INDEX) { 167 | return ("".equals(token))? FULL_URI_MATCH_INDEX: NO_URI_MATCH_INDEX; 168 | } 169 | return NO_URI_MATCH_INDEX; // no URI match 170 | } 171 | 172 | public static Object[] array(Object obj1, Object obj2) { 173 | return new Object[] {obj1, obj2}; 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /java-src/calfpath/route/UriTokenContext.java: -------------------------------------------------------------------------------- 1 | // Copyright (c) Shantanu Kumar. All rights reserved. 2 | // The use and distribution terms for this software are covered by the 3 | // Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | // which can be found in the file LICENSE at the root of this distribution. 5 | // By using this software in any fashion, you are agreeing to be bound by 6 | // the terms of this license. 7 | // You must not remove this notice, or any other, from this software. 8 | 9 | 10 | package calfpath.route; 11 | 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | /** 19 | * Internal, URI tokens based URI matching utility class. Instance of this class may be stored as a context object in 20 | * a Ring request. This class has unsynchronized mutable fields, accessed in a single thread. 21 | * 22 | */ 23 | public class UriTokenContext { 24 | 25 | public List uriTokens; 26 | public int uriTokenCount; 27 | public Map paramsMap; 28 | 29 | public UriTokenContext(List uriTokens, Map paramsMap) { 30 | this.uriTokens = uriTokens; 31 | this.uriTokenCount = uriTokens.size(); 32 | this.paramsMap = paramsMap; 33 | } 34 | 35 | // ----- static utility ----- 36 | 37 | public static List parseUriTokens(String uri) { 38 | // if (uri == null || uri.isBlank()) { 39 | // throw new IllegalArgumentException("URI cannot be empty or blank"); 40 | // } 41 | // char first = uri.charAt(0); 42 | // if (first != '/') { 43 | // throw new IllegalArgumentException("URI must begin with '/'"); 44 | // } 45 | final int len = uri.length(); 46 | final List tokens = new ArrayList(); 47 | final StringBuilder sb = new StringBuilder(); 48 | for (int i = 1; // start with second character, because first character is '/' 49 | i < len; i++) { 50 | final char ch = uri.charAt(i); 51 | if (ch == '/') { 52 | tokens.add(sb.toString()); 53 | sb.setLength(0); 54 | } else { 55 | sb.append(ch); 56 | } 57 | } 58 | tokens.add(sb.toString()); 59 | return tokens; 60 | } 61 | 62 | // ----- match constants ----- 63 | 64 | public static final List NO_MATCH_TOKENS = null; 65 | 66 | @SuppressWarnings("unchecked") 67 | public static final List FULL_MATCH_TOKENS = Collections.EMPTY_LIST; 68 | 69 | private List partialMatch(List resultTokens) { 70 | this.uriTokens = resultTokens; 71 | this.uriTokenCount = resultTokens.size(); 72 | return resultTokens; 73 | } 74 | 75 | private List partialMatch(List resultTokens, Map pathParams) { 76 | this.uriTokens = resultTokens; 77 | this.uriTokenCount = resultTokens.size(); 78 | this.paramsMap.putAll(pathParams); 79 | return resultTokens; 80 | } 81 | 82 | private List fullMatch(Map pathParams) { 83 | this.paramsMap.putAll(pathParams); 84 | this.uriTokens = FULL_MATCH_TOKENS; 85 | this.uriTokenCount = 0; 86 | return FULL_MATCH_TOKENS; 87 | } 88 | 89 | private List fullMatch() { 90 | this.uriTokens = FULL_MATCH_TOKENS; 91 | this.uriTokenCount = 0; 92 | return FULL_MATCH_TOKENS; 93 | } 94 | 95 | // ----- match methods ----- 96 | 97 | /** 98 | * (Partial) Match given URI tokens against URI pattern tokens. Meant for dynamic URI routes with path params. 99 | * Return a sub-list of URI tokens yet to be matched, to be interpreted as follows: 100 | * 101 | * | Condition | Meaning | 102 | * |-----------|---------------| 103 | * | `null` | no match | 104 | * | empty | full match | 105 | * | non-empty | partial match | 106 | * 107 | * @param patternTokens 108 | * @return 109 | */ 110 | public List dynamicUriPartialMatch(List patternTokens) { 111 | final int patternTokenCount = patternTokens.size(); 112 | if ((uriTokenCount < patternTokenCount)) { 113 | return NO_MATCH_TOKENS; 114 | } 115 | final Map pathParams = new HashMap(patternTokenCount); 116 | for (int i = 0; i < patternTokenCount; i++) { 117 | final Object eachPatternToken = patternTokens.get(i); 118 | final String eachURIToken = uriTokens.get(i); 119 | if (eachPatternToken instanceof String) { 120 | if (!eachURIToken.equals(eachPatternToken)) { 121 | return NO_MATCH_TOKENS; 122 | } 123 | } else { 124 | pathParams.put(eachPatternToken, eachURIToken); 125 | } 126 | } 127 | if (uriTokenCount > patternTokenCount) { 128 | return partialMatch(uriTokens.subList(patternTokenCount, uriTokenCount), pathParams); // partial match 129 | } else { 130 | return fullMatch(pathParams); // full match 131 | } 132 | } 133 | 134 | /** 135 | * (Full) Match given URI tokens against URI pattern tokens. Meant for dynamic URI routes with path params. 136 | * Return a sub-list of URI tokens yet to be matched, to be interpreted as follows: 137 | * 138 | * | Condition | Meaning | 139 | * |-----------|---------------| 140 | * | `null` | no match | 141 | * | empty | full match | 142 | * 143 | * @param patternTokens 144 | * @return 145 | */ 146 | public List dynamicUriFullMatch(List patternTokens) { 147 | final int patternTokenCount = patternTokens.size(); 148 | if (uriTokenCount != patternTokenCount) { 149 | return NO_MATCH_TOKENS; 150 | } 151 | final Map pathParams = new HashMap(patternTokenCount); 152 | for (int i = 0; i < patternTokenCount; i++) { 153 | final Object eachPatternToken = patternTokens.get(i); 154 | final String eachURIToken = uriTokens.get(i); 155 | if (eachPatternToken instanceof String) { 156 | if (!eachURIToken.equals(eachPatternToken)) { 157 | return NO_MATCH_TOKENS; 158 | } 159 | } else { 160 | pathParams.put(eachPatternToken, eachURIToken); 161 | } 162 | } 163 | paramsMap.putAll(pathParams); // mutate pathParams only on a successful match 164 | return FULL_MATCH_TOKENS; // full match 165 | } 166 | 167 | /** 168 | * (Partial) Match given URI tokens against URI pattern string tokens. Meant for static URI routes. Return the 169 | * tokens yet to be matched, interpreted as follows: 170 | * 171 | * | Condition | Meaning | 172 | * |-----------|---------------| 173 | * | `null` | no match | 174 | * | empty | full match | 175 | * | non-empty | partial match | 176 | * 177 | * @param patternTokens 178 | * @return 179 | */ 180 | public List staticUriPartialMatch(List patternTokens) { 181 | final int patternTokenCount = patternTokens.size(); 182 | if (uriTokenCount < patternTokenCount) { 183 | return NO_MATCH_TOKENS; 184 | } 185 | for (int i = 0; i < patternTokenCount; i++) { 186 | if (!patternTokens.get(i).equals(uriTokens.get(i))) { 187 | return NO_MATCH_TOKENS; 188 | } 189 | } 190 | if (uriTokenCount > patternTokenCount) { 191 | return partialMatch(uriTokens.subList(patternTokenCount, uriTokenCount)); // partial match 192 | } else { 193 | return fullMatch(); // full match 194 | } 195 | } 196 | 197 | /** 198 | * (Full) Match given URI tokens against URI pattern string tokens. Meant for static URI routes. Return the 199 | * tokens yet to be matched, interpreted as follows: 200 | * 201 | * | Condition | Meaning | 202 | * |-----------|---------------| 203 | * | `null` | no match | 204 | * | empty | full match | 205 | * 206 | * @param patternTokens 207 | * @return 208 | */ 209 | public List staticUriFullMatch(List patternTokens) { 210 | final int patternTokenCount = patternTokens.size(); 211 | if (uriTokenCount != patternTokenCount) { 212 | return NO_MATCH_TOKENS; 213 | } 214 | for (int i = 0; i < patternTokenCount; i++) { 215 | if (!patternTokens.get(i).equals(uriTokens.get(i))) { 216 | return NO_MATCH_TOKENS; 217 | } 218 | } 219 | return FULL_MATCH_TOKENS; // full match 220 | } 221 | 222 | } 223 | -------------------------------------------------------------------------------- /src/calfpath/route/uri_match.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.route.uri-match 11 | "Internal namespace for data-driven route URI match." 12 | (:require 13 | [clojure.string :as string]) 14 | #?(:clj (:import 15 | [java.util HashMap] 16 | [clojure.lang Associative] 17 | [calfpath.route UriMatch]))) 18 | 19 | 20 | (def ^:const NO-URI-MATCH-INDEX "URI does not match" -2) 21 | 22 | 23 | ;; ~~~ match fns ~~~ 24 | 25 | 26 | (defn dynamic-uri-match 27 | [uri begin-index params-map pattern-tokens partial?] 28 | (let [uri-length (count uri)] 29 | (if (>= begin-index uri-length) 30 | NO-URI-MATCH-INDEX 31 | (loop [uri-index begin-index 32 | path-params (transient {}) 33 | next-tokens (seq pattern-tokens)] 34 | (if next-tokens 35 | (if (>= uri-index uri-length) 36 | (if partial? 37 | (do 38 | (vswap! params-map conj (persistent! path-params)) 39 | #_full-match uri-length) 40 | NO-URI-MATCH-INDEX) 41 | (let [token (first next-tokens)] 42 | (if (string? token) 43 | ;; string token 44 | (if (string/starts-with? (subs uri uri-index) token) 45 | (recur (unchecked-add uri-index (count token)) path-params (next next-tokens)) 46 | NO-URI-MATCH-INDEX) 47 | ;; must be a keyword 48 | (let [[pp ui] (loop [sb "" ; string buffer 49 | j uri-index] 50 | (if (< j uri-length) 51 | (let [ch (get uri j)] 52 | (if (= \/ ch) 53 | [(assoc! path-params token sb) 54 | j] 55 | (recur (str sb ch) (unchecked-inc j)))) 56 | [(assoc! path-params token sb) 57 | uri-length]))] 58 | (recur ui pp (next next-tokens)))))) 59 | (if (< uri-index uri-length) 60 | (if partial? 61 | (do 62 | (vswap! params-map conj (persistent! path-params)) 63 | #_partial-match uri-index) 64 | NO-URI-MATCH-INDEX) 65 | (do 66 | (vswap! params-map conj (persistent! path-params)) 67 | #_full-match uri-length))))))) 68 | 69 | 70 | (defn dynamic-uri-partial-match* 71 | [uri ^long begin-index params-map pattern-tokens] 72 | (dynamic-uri-match uri begin-index params-map pattern-tokens true)) 73 | 74 | 75 | (defn dynamic-uri-full-match* 76 | [uri ^long begin-index params-map pattern-tokens] 77 | (dynamic-uri-match uri begin-index params-map pattern-tokens false)) 78 | 79 | 80 | (defn static-uri-partial-match* 81 | [uri ^long begin-index static-token] 82 | (let [rem-uri (subs uri begin-index) 83 | rem-len (count rem-uri)] 84 | (if (string/starts-with? rem-uri static-token) 85 | (let [token-length (count static-token)] 86 | (if (= rem-len token-length) 87 | #_full-match (unchecked-add begin-index rem-len) 88 | #_partial-match (unchecked-add begin-index token-length))) 89 | NO-URI-MATCH-INDEX))) 90 | 91 | 92 | (defn static-uri-full-match* 93 | [uri ^long begin-index static-token] 94 | (if (= 0 begin-index) 95 | (if (= static-token uri) 96 | (count uri) ; full match 97 | NO-URI-MATCH-INDEX) 98 | (let [rem-uri (subs uri begin-index) 99 | rem-len (count rem-uri)] 100 | (if (= rem-uri static-token) 101 | (unchecked-add begin-index rem-len) ; full match 102 | NO-URI-MATCH-INDEX)))) 103 | 104 | 105 | ;; ----- matcher / matchex support ----- 106 | 107 | 108 | (defn parse-uri-template 109 | "Given a URI pattern string, e.g. '/user/:id/profile/:descriptor/' parse it and return a vector [tokens partial?] of 110 | alternating string and keyword tokens, e.g. [['/user/' :id '/profile/' :descriptor '/'] false]. Marker char is ':'." 111 | [uri-pattern] 112 | (let [pattern-length (count uri-pattern) 113 | [path partial?] (if (and (> pattern-length 1) 114 | (string/ends-with? uri-pattern "*")) 115 | [(subs uri-pattern 0 (dec pattern-length)) true] ; chop off last char 116 | [uri-pattern false]) 117 | n (count path) 118 | separator \/] 119 | (loop [i (int 0) ; current index in the URI string 120 | j (int 0) ; start index of the current token (string or keyword) 121 | s? true ; string in progress? (false implies keyword in progress) 122 | r []] 123 | (if (>= i n) 124 | [(conj r (let [t (subs path j i)] 125 | (if s? 126 | t 127 | (keyword t)))) 128 | partial?] 129 | (let [^char ch (get path i) 130 | [jn s? r] (if s? 131 | (if (= \: ch) 132 | [(unchecked-inc i) false (conj r (subs path j i))] 133 | [j true r]) 134 | (if (= separator ch) 135 | [i true (conj r (keyword (subs path j i)))] 136 | [j false r]))] 137 | (recur (unchecked-inc i) (int jn) s? r)))))) 138 | 139 | 140 | (defn get-static-uri-template 141 | [uri-pattern-tokens] 142 | (when (and (= 1 (count uri-pattern-tokens)) 143 | (string? (first uri-pattern-tokens))) 144 | (first uri-pattern-tokens))) 145 | 146 | 147 | (def ^:const uri-begin-index-key :calfpath/uri-begin-index) 148 | 149 | 150 | (defn static-uri-partial-match [request static-token params-key] 151 | (let [^String uri (:uri request) 152 | begin-index (uri-begin-index-key request 0) 153 | final-index #?(:cljs (static-uri-partial-match* uri begin-index static-token) 154 | :clj (UriMatch/staticUriPartialMatch uri begin-index static-token))] 155 | (when #?(:cljs (pos? final-index) 156 | :clj (UriMatch/isPos final-index)) 157 | #?(:cljs (assoc request uri-begin-index-key final-index) 158 | :clj (.assoc ^Associative request uri-begin-index-key final-index))))) 159 | 160 | 161 | (defn static-uri-full-match [request static-token params-key] 162 | (let [uri (:uri request) 163 | begin-index (uri-begin-index-key request 0) 164 | final-index #?(:cljs (static-uri-full-match* uri begin-index static-token) 165 | :clj (UriMatch/staticUriFullMatch uri begin-index static-token))] 166 | (when #?(:cljs (pos? final-index) 167 | :clj (UriMatch/isPos final-index)) 168 | request))) 169 | 170 | 171 | (defn dynamic-uri-partial-match [request uri-template params-key] 172 | (let [^String uri (:uri request) 173 | begin-index (uri-begin-index-key request 0) 174 | has-params? (contains? request params-key) 175 | path-params #?(:cljs (volatile! (if has-params? (get request params-key) {})) 176 | :clj (if has-params? (get request params-key) (HashMap.))) 177 | final-index #?(:cljs (dynamic-uri-partial-match* uri begin-index path-params uri-template) 178 | :clj (UriMatch/dynamicUriPartialMatch uri begin-index path-params uri-template))] 179 | (when #?(:cljs (pos? final-index) 180 | :clj (UriMatch/isPos final-index)) 181 | #?(:cljs (-> request 182 | (assoc uri-begin-index-key final-index) 183 | (assoc params-key @path-params)) 184 | :clj (if has-params? 185 | (-> ^Associative request 186 | (.assoc uri-begin-index-key final-index)) 187 | (-> ^Associative request 188 | (.assoc uri-begin-index-key final-index) 189 | (.assoc params-key path-params))))))) 190 | 191 | 192 | (defn dynamic-uri-full-match [request uri-template params-key] 193 | (let [uri (:uri request) 194 | begin-index (uri-begin-index-key request 0) 195 | has-params? (contains? request params-key) 196 | path-params #?(:cljs (volatile! (if has-params? (get request params-key) {})) 197 | :clj (if has-params? (get request params-key) (HashMap.))) 198 | final-index #?(:cljs (dynamic-uri-full-match* uri begin-index path-params uri-template) 199 | :clj (UriMatch/dynamicUriFullMatch uri begin-index path-params uri-template))] 200 | (when #?(:cljs (pos? final-index) 201 | :clj (UriMatch/isPos final-index)) 202 | #?(:cljs (assoc request params-key @path-params) 203 | :clj (if has-params? 204 | request 205 | (.assoc ^Associative request params-key path-params)))))) 206 | -------------------------------------------------------------------------------- /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to calfpath 2 | 3 | ## Requiring namespace 4 | 5 | ```clojure 6 | (:require 7 | [calfpath.core :refer [->uri ->method 8 | ->get ->head ->options ->patch ->put ->post ->delete]] 9 | [calfpath.route :as r]) 10 | ``` 11 | 12 | 13 | ## Routing with convenience macros 14 | 15 | Calfpath provides some convenience macros to match routes against request and invokes the 16 | corresponding handler code. For data driven routing (recommended), see the next section. 17 | 18 | ```clojure 19 | (defn handler 20 | [request] 21 | ;; ->uri is a macro that dispatches on URI pattern 22 | (->uri request 23 | "/user/:id*" [id] (->uri request 24 | "/profile/:type/" [type] (->method request 25 | :get {:status 200 26 | :headers {"Content-Type" "text/plain"} 27 | :body (format "ID: %s, Type: %s" id type)} 28 | :put {:status 200 29 | :headers {"Content-Type" "text/plain"} 30 | :body "Updated"}) 31 | "/permissions/" [] (->method request 32 | :get {:status 200 33 | :headers {"Content-Type" "text/plain"} 34 | :body (str "ID: " id)} 35 | :put {:status 200 36 | :headers {"Content-Type" "text/plain"} 37 | :body (str "Updated ID: " id)})) 38 | "/company/:cid/dept/:did/" [cid did] (->put request 39 | {:status 200 40 | :headers {"Content-Type" "text/plain"} 41 | :body "Data"}) 42 | "/this/is/a/static/route" [] (->put request 43 | {:status 200 44 | :headers {"Content-Type" "text/plain"} 45 | :body "output"}))) 46 | ``` 47 | 48 | In this example, we first check the URI followed by the nested method checks. The first URI group 49 | uses a wildcard to match the common URI prefix `/user/:id`, then the remaining segment. 50 | 51 | 52 | ## Data driven routing 53 | 54 | ### Concept 55 | 56 | Calfpath provides data driven routing via the API in `calfpath.route` namespace. It is based on the 57 | concept of a route, which is a map of two required keys - a matcher (`:matcher`) that matches an 58 | incoming request against the route, and a dispatch point (either `:nested` or `:handler`) for a 59 | successful match. A route must contain the following keys: 60 | 61 | - `:matcher` and `:nested`, or 62 | - `:matcher` and `:handler` 63 | 64 | Example: 65 | 66 | ```edn 67 | [{:matcher m1 :nested [{:matcher m1-1 :handler h1-1} 68 | {:matcher m1-2 :handler h1-2}]} 69 | {:matcher m2 :handler h2}] 70 | ``` 71 | 72 | Synopsis: 73 | 74 | | Route key| Description | 75 | |----------|-------------| 76 | |`:matcher`|`(fn [request])` returning request on success, `nil` otherwise | 77 | |`:nested` |vector of one or more sub routes | 78 | |`:handler`|route handler, same arity as ring handler fn (regular or async)| 79 | 80 | 81 | ### Quickstart example 82 | 83 | Calfpath provides matchers for common use cases, which we would see in an example below: 84 | 85 | #### Route handler 86 | 87 | A route handler is a function with same arity and semantics as a Ring handler. 88 | 89 | ```clojure 90 | (defn list-user-jobs 91 | "Route handler for listing user jobs." 92 | [{{:keys [user-id] :path-params} :as request}] 93 | [:job-id 1 94 | :job-id 2]) 95 | ``` 96 | 97 | #### Routes definition 98 | 99 | A routes definition is a vector of route maps. 100 | 101 | ```clojure 102 | (def easy-routes 103 | "Routes defined using a short, easy notation." 104 | [; partial URI match, implied by trailing '*' 105 | {"/users/:user-id*" [{["/jobs/" :get] list-user-jobs} 106 | {["/permissions/" :get] permissions-handler}]} 107 | {["/orders/:order-id/confirm/" :post] confirm-order} 108 | {"/health/" health-status}]) 109 | ``` 110 | 111 | The easy routes definition above is translated as the longer notation below during route compilation: 112 | 113 | ```clojure 114 | (def app-routes 115 | "Vector of application routes. To be processed by calfpath.route/compile-routes to generate matchers." 116 | [; partial URI match, implied by trailing '*' 117 | {:uri "/users/:user-id*" :nested [{:uri "/jobs/" :nested [{:method :get :handler list-user-jobs}]} 118 | {:uri "/permissions/" :method :get permissions-handler}]} 119 | {:uri "/orders/:order-id/confirm/" :method :post :handler confirm-order} ; :uri is lifted over :method 120 | {:uri "/health/" :handler health-status}]) 121 | ``` 122 | 123 | Here, we do not specify any matcher directly but put relevant attributes to generate matchers 124 | from, e.g. `:uri`, `:method` etc. 125 | 126 | 127 | #### Serving static resources 128 | 129 | Calfpath routes may be used to serve static resources using wrapped handlers. 130 | 131 | ```clojure 132 | (def static-routes 133 | "Vector of static web resources" 134 | [{["/static/*" :get] 135 | (-> (fn [_] {:status 400 :body "No such file"}) ; fallback 136 | ;; the following requires [ring/ring-core "version"] dependency in your project 137 | (ring.middleware.resource/wrap-resource "public") ; serve files from classpath 138 | (ring.middleware.file/wrap-file "/var/www/public") ; serve files from filesystem 139 | (ring.middleware.content-type/wrap-content-type) ; detect and put content type 140 | (ring.middleware.not-modified/wrap-not-modified))}]) 141 | ``` 142 | 143 | 144 | #### Making Ring handler 145 | 146 | The routes vector must be turned into a Ring handler before it can be used. 147 | 148 | ```clojure 149 | (def ring-handler 150 | (-> app-routes ; the routes vector 151 | r/compile-routes ; turn maps into routes by putting matchers in them 152 | r/make-dispatcher)) 153 | ``` 154 | 155 | 156 | ### Applying route middleware 157 | 158 | Let us say you want to measure and log the total time taken by a route handler. How would you do that without 159 | modifying every handler function or the routes vector? Using route middleware, as shown below: 160 | 161 | ```clojure 162 | (defn tme-tracking-middleware 163 | [handler] 164 | (fn [request] 165 | (let [start (System/currentTimeMillis) 166 | taken (fn [] (unchecked-subtract (System/currentTimeMillis) start))] 167 | (try 168 | (let [result (handler request)] 169 | (println "Time taken" (taken) "ms") 170 | result) 171 | (catch Exception e 172 | (println "Time taken" (taken) "ms, exception thrown:" e) 173 | (throw e)))))) 174 | ``` 175 | 176 | This middleware needs to be applied to only the `:handler` value in all routes, which can be done as follows: 177 | 178 | ```clojure 179 | (calfpath.route/update-in-each-route 180 | routes :handler time-tracking-middleware) 181 | ``` 182 | 183 | Should you need to inspect the entire route before updating anything, consider `calfpath.route/update-each-route`. 184 | Note that you need to apply all middleware before making a Ring handler out of the routes. 185 | 186 | 187 | ### From route to request (bi-directional routing) 188 | 189 | Bi-directional routing is when you can not only find a matching route for a given request, but you can generate one 190 | given a route and the template parameters. To make routes bi-directiional you need to add unique identifier in every 191 | route. Consider the following (easy notation) routes example: 192 | 193 | ```clojure 194 | (def indexable-routes 195 | [{["/info/:token" :get] identity :id :info} 196 | {["/album/:lid/artist/:rid/" :get] identity :id :album} 197 | {"/user/:id*" [{"/auth" identity :id :auth-user} 198 | {"/permissions/" [{:get identity :id :read-perms} 199 | {:post identity :id :save-perms} 200 | {:put identity :id :update-perms}]} 201 | {"/profile/:type/" [{:get identity :id :read-profile} 202 | {:patch identity :id :patch-profile} 203 | {:delete identity :id :remove-profile}]} 204 | {:uri "" identity}]}]) 205 | ``` 206 | 207 | Every route (except the last one) having a handler function also has a unique ID that we can refer the route with. 208 | Now we can build a reverse index: 209 | 210 | ```clojure 211 | (calfpath.route/make-index indexable-routes) 212 | ``` 213 | 214 | It returns a reverse index looking like as follows: 215 | 216 | ```clojure 217 | {:info {:uri ["/info/" :token] :request-method :get} 218 | :album {:uri ["/album/" :lid "/artist/" :rid "/"] :request-method :get} 219 | :auth-user {:uri ["/user/" :id "/auth"] :request-method :get} 220 | :read-perms {:uri ["/user/" :id "/permissions/"] :request-method :get} 221 | :save-perms {:uri ["/user/" :id "/permissions/"] :request-method :post} 222 | :update-perms {:uri ["/user/" :id "/permissions/"] :request-method :put} 223 | :read-profile {:uri ["/user/" :id "/profile/" :type "/"] :request-method :get} 224 | :patch-profile {:uri ["/user/" :id "/profile/" :type "/"] :request-method :patch} 225 | :remove-profile {:uri ["/user/" :id "/profile/" :type "/"] :request-method :delete}} 226 | ``` 227 | 228 | Now we can create a request based on any of the indexed routes: 229 | 230 | ```clojure 231 | (-> (:album routes-index) 232 | (calfpath.route/template->request {:uri-params {:lid 10 :rid 20}})) 233 | ``` 234 | 235 | It returns a request map looking like the one below: 236 | 237 | ```clojure 238 | {:uri "/album/10/artist/20/" 239 | :request-method :get} 240 | ``` 241 | 242 | This structure matches the Ring request SPEC attributes. 243 | -------------------------------------------------------------------------------- /test/calfpath/core_test.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.core-test 11 | (:require 12 | #?(:cljs [goog.string :as gstring]) 13 | #?(:cljs [goog.string.format]) 14 | #?(:cljs [cljs.test :refer-macros [deftest is testing]] 15 | :clj [clojure.test :refer [deftest is testing]]) 16 | #?(:cljs [calfpath.core :as c :include-macros true] 17 | :clj [calfpath.core :as c]))) 18 | 19 | 20 | #?(:cljs (def format gstring/format)) 21 | 22 | 23 | (deftest test-->uri 24 | (testing "No clause" 25 | (let [request {:uri "/user/1234/profile/compact/"}] 26 | (is (= 400 27 | (:status (c/->uri request)))))) 28 | (testing "One clause (no match)" 29 | (let [request {:uri "/hello/1234/"}] 30 | (is (= 400 31 | (:status (c/->uri request 32 | "/user/:id/profile/:type/" [id type] (do {:status 200 33 | :body (format "ID: %s, Type: %s" id type)}))))))) 34 | (testing "One clause (with match)" 35 | (let [request {:uri "/user/1234/profile/compact/"}] 36 | (is (= "ID: 1234, Type: compact" 37 | (:body (c/->uri request 38 | "/user/:id/profile/:type/" [id type] {:status 200 39 | :body (format "ID: %s, Type: %s" id type)})))) 40 | (is (= "ID: 1234, Type: compact" 41 | (:body (c/->uri request 42 | "/user/:id/profile/:type/" [id type] {:status 200 43 | :body (format "ID: %s, Type: %s" id type)})))))) 44 | (testing "Two clauses (no match)" 45 | (let [request {:uri "/hello/1234/"}] 46 | (is (= 400 47 | (:status (c/->uri request 48 | "/user/:id/profile/:type/" [id type] {:status 200 49 | :body (format "ID: %s, Type: %s" id type)} 50 | "/user/:id/permissions/" [id] {:status 200 51 | :body (format "ID: %s" id)})))))) 52 | (testing "Two clauses (no match, custom default)" 53 | (let [request {:uri "/hello/1234/"}] 54 | (is (= 404 55 | (:status (c/->uri request 56 | "/user/:id/profile/:type/" [id type] {:status 200 57 | :body (format "ID: %s, Type: %s" id type)} 58 | "/user/:id/permissions/" [id] {:status 200 59 | :body (format "ID: %s" id)} 60 | {:status 404 61 | :body "Not found"})))))) 62 | (testing "Two clause (with match)" 63 | (let [request {:uri "/user/1234/permissions/"}] 64 | (is (= "ID: 1234" 65 | (:body (c/->uri request 66 | "/user/:id/profile/:type/" [id type] {:status 200 67 | :body (format "ID: %s, Type: %s" id type)} 68 | "/user/:id/permissions/" [id] {:status 200 69 | :body (format "ID: %s" id)}))))))) 70 | 71 | 72 | (deftest test-->method 73 | (testing "No clause" 74 | (let [request {:request-method :get}] 75 | (is (= 405 76 | (:status (c/->method request)))))) 77 | (testing "One clause (no match)" 78 | (let [request {:request-method :get}] 79 | (is (= 405 80 | (:status (c/->method request 81 | :put {:status 200 82 | :body "Updated"})))))) 83 | (testing "One clause (with match)" 84 | (let [request {:request-method :get}] 85 | (is (= 200 86 | (:status (c/->method request 87 | :get {:status 200 88 | :body "Data"})))))) 89 | (testing "Two clauses (no match)" 90 | (let [request {:request-method :delete}] 91 | (is (= 405 92 | (:status (c/->method request 93 | :get {:status 200 94 | :body "Data"} 95 | :put {:status 200 96 | :body "Updated"})))))) 97 | (testing "Two clauses (no match, custom default)" 98 | (let [request {:request-method :delete}] 99 | (is (= 404 100 | (:status (c/->method request 101 | :get {:status 200 102 | :body "Data"} 103 | :put {:status 200 104 | :body "Updated"} 105 | {:status 404 106 | :body "Not found"})))))) 107 | (testing "Two clauses (with match)" 108 | (let [request {:request-method :put}] 109 | (is (= "Updated" 110 | (:body (c/->method request 111 | :get {:status 200 112 | :body "Data"} 113 | :put {:status 200 114 | :body "Updated"}))))))) 115 | 116 | 117 | (deftest test-shortcuts 118 | (let [request-get {:request-method :get} 119 | request-head {:request-method :head} 120 | request-options {:request-method :options} 121 | request-put {:request-method :put} 122 | request-post {:request-method :post} 123 | request-delete {:request-method :delete} 124 | ok {:status 200 125 | :body "Data"} 126 | not-found {:status 404 127 | :body "Not found"}] 128 | (testing "->get" 129 | (is (= 405 (:status (c/->get request-put ok)))) 130 | (is (= 404 (:status (c/->get request-put ok not-found)))) 131 | (is (= 200 (:status (c/->get request-get ok)))) 132 | (is (= 200 (:status (c/->get request-get ok not-found))))) 133 | (testing "->head" 134 | (is (= 405 (:status (c/->head request-put ok)))) 135 | (is (= 404 (:status (c/->head request-put ok not-found)))) 136 | (is (= 200 (:status (c/->head request-head ok)))) 137 | (is (= 200 (:status (c/->head request-head ok not-found))))) 138 | (testing "->options" 139 | (is (= 405 (:status (c/->options request-put ok)))) 140 | (is (= 404 (:status (c/->options request-put ok not-found)))) 141 | (is (= 200 (:status (c/->options request-options ok)))) 142 | (is (= 200 (:status (c/->options request-options ok not-found))))) 143 | (testing "->put" 144 | (is (= 405 (:status (c/->put request-get ok)))) 145 | (is (= 404 (:status (c/->put request-get ok not-found)))) 146 | (is (= 200 (:status (c/->put request-put ok)))) 147 | (is (= 200 (:status (c/->put request-put ok not-found))))) 148 | (testing "->post" 149 | (is (= 405 (:status (c/->post request-put ok)))) 150 | (is (= 404 (:status (c/->post request-put ok not-found)))) 151 | (is (= 200 (:status (c/->post request-post ok)))) 152 | (is (= 200 (:status (c/->post request-post ok not-found))))) 153 | (testing "->delete" 154 | (is (= 405 (:status (c/->delete request-put ok)))) 155 | (is (= 404 (:status (c/->delete request-put ok not-found)))) 156 | (is (= 200 (:status (c/->delete request-delete ok)))) 157 | (is (= 200 (:status (c/->delete request-delete ok not-found))))))) 158 | 159 | 160 | (defn composite 161 | [request] 162 | (c/->uri request 163 | "/never/hit/" [] {:status 200 :body "Never hit"} 164 | "/user/:id/profile/:type/" [id type] (c/->method request 165 | :get {:status 200 166 | :body (format "Compact profile for ID: %s, Type: %s" id type)} 167 | :put {:status 200 168 | :body (format "Updated ID: %s, Type: %s" id type)}) 169 | "/user/:id/permissions/" [id] (c/->post request {:status 201 170 | :body (format "Profile ID: %s, Created new permission" id)}))) 171 | 172 | 173 | (defn composite-partial 174 | [request] 175 | (c/->uri request 176 | "/never/hit/" [] {:status 200 :body "Never hit"} 177 | "/user/:id*" [id] (c/->uri request 178 | "/profile/:type/" [type] (c/->method request 179 | :get {:status 200 180 | :body (format "Compact profile for ID: %s, Type: %s" id type)} 181 | :put {:status 200 182 | :body (format "Updated ID: %s, Type: %s" id type)}) 183 | "/permissions/" [] (c/->post request {:status 201 184 | :body (format "Profile ID: %s, Created new permission" id)})))) 185 | 186 | 187 | (deftest test-composite 188 | (testing "No route match" 189 | (let [request {:request-method :get 190 | :uri "/hello/1234/"}] 191 | (is (= 400 (:status (composite request)))) 192 | (is (= 400 (:status (composite-partial request)))))) 193 | (testing "Matching route and method" 194 | (let [request {:request-method :get 195 | :uri "/user/1234/profile/compact/"} 196 | expected "Compact profile for ID: 1234, Type: compact"] 197 | (is (= expected (:body (composite request)))) 198 | (is (= expected (:body (composite-partial request)))))) 199 | (testing "Matching route and method" 200 | (let [request {:request-method :post 201 | :uri "/user/1234/permissions/"} 202 | expected "Profile ID: 1234, Created new permission"] 203 | (is (= expected (:body (composite request)))) 204 | (is (= expected (:body (composite-partial request)))))) 205 | (testing "Matching route, but no matching method" 206 | (let [request {:request-method :delete 207 | :uri "/user/1234/profile/compact/"}] 208 | (is (= 405 (:status (composite request)))) 209 | (is (= 405 (:status (composite-partial request))))))) 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /perf/calfpath/perf_test.clj: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.perf-test 11 | (:require 12 | [clojure.test :refer [deftest is testing use-fixtures]] 13 | [ataraxy.core :as ataraxy] 14 | [bidi.ring :as bidi] 15 | [compojure.core :refer [defroutes rfn routes context GET POST PUT ANY]] 16 | [clout.core :as l] 17 | [reitit.ring :as reitit] 18 | [calfpath.core :refer [->uri ->method ->get ->head ->options ->put ->post ->delete]] 19 | [calfpath.internal :as i] 20 | [calfpath.route :as r] 21 | [citius.core :as c])) 22 | 23 | 24 | (defn h11 [id type] {:status 200 25 | :headers {"Content-Type" "text/plain"} 26 | :body (str id ".11." type)}) 27 | (defn h12 [id type] {:status 200 28 | :headers {"Content-Type" "text/plain"} 29 | :body (str id ".12." type)}) 30 | (defn h1x [] {:status 405 31 | :headers {"Allow" "GET, PUT" 32 | "Content-Type" "text/plain"} 33 | :body "405 Method not supported. Supported methods are: GET, PUT"}) 34 | 35 | 36 | (defn h21 [id] {:status 200 37 | :headers {"Content-Type" "text/plain"} 38 | :body (str id ".21")}) 39 | (defn h22 [id] {:status 200 40 | :headers {"Content-Type" "text/plain"} 41 | :body (str id ".22")}) 42 | (defn h2x [] {:status 405 43 | :headers {"Allow" "GET, PUT" 44 | "Content-Type" "text/plain"} 45 | :body "405 Method not supported. Supported methods are: GET, PUT"}) 46 | (defn h30 [cid did] {:status 200 47 | :headers {"Content-Type" "text/plain"} 48 | :body (str cid ".3." did)}) 49 | (defn h3x [] {:status 405 50 | :headers {"Allow" "PUT" 51 | "Content-Type" "text/plain"} 52 | :body "405 Method not supported. Only PUT is supported."}) 53 | (defn h40 [] {:status 200 54 | :headers {"Content-Type" "text/plain"} 55 | :body "4"}) 56 | (defn h4x [] {:status 405 57 | :headers {"Allow" "PUT" 58 | "Content-Type" "text/plain"} 59 | :body "405 Method not supported. Only PUT is supported."}) 60 | (defn hxx [] {:status 400 61 | :headers {"Content-Type" "text/plain"} 62 | :body "400 Bad request. URI does not match any available uri-template."}) 63 | 64 | (def handler-ataraxy 65 | (ataraxy/handler 66 | {:routes '{["/user/" id "/profile/" type "/"] {:get [:h11 id type] :put [:h12 id type] "" [:h1x]} 67 | ["/user/" id "/permissions/"] {:get [:h21 id] :put [:h22 id] "" [:h2x]} 68 | ["/company/" cid "/dept/" did "/"] {:put [:h30] "" [:h3x]} 69 | "/this/is/a/static/route" {:put [:h40] "" [:h4x]} 70 | ^{:re #".*"} path [:hxx]} 71 | :handlers {:h11 (fn [{{:keys [id type]} :route-params}] (h11 id type)) 72 | :h12 (fn [{{:keys [id type]} :route-params}] (h12 id type)) 73 | :h1x (fn [_] (h1x)) 74 | :h21 (fn [{{:keys [id]} :route-params}] (h21 id)) 75 | :h22 (fn [{{:keys [id]} :route-params}] (h22 id)) 76 | :h2x (fn [_] (h2x)) 77 | :h30 (fn [{{:keys [cid did]} :route-params}] (h30 cid did)) 78 | :h3x (fn [_] (h3x)) 79 | :h40 (fn [_] (h40)) 80 | :h4x (fn [_] (h4x)) 81 | :hxx (fn [_] (hxx))}})) 82 | 83 | 84 | (def handler-bidi 85 | ;; path params are in (:route-params request) 86 | (bidi/make-handler 87 | ["/" {"user/" {[:id "/profile/" :type "/"] {:get (fn [{{:keys [id type]} :route-params}] (h11 id type)) 88 | :put (fn [{{:keys [id type]} :route-params}] (h12 id type)) 89 | true (fn [_] (h1x))} 90 | [:id "/permissions/"] {:get (fn [{{:keys [id]} :route-params}] (h21 id)) 91 | :put (fn [{{:keys [id]} :route-params}] (h22 id)) 92 | true (fn [_] (h2x))}} 93 | ["company/" :cid "/dept/" :did "/"] {:put (fn [{{:keys [cid did]} :route-params}] (h30 cid did)) 94 | true (fn [_] (h3x))} 95 | "this/is/a/static/route" {:put (fn [_] (h40)) 96 | true (fn [_] (h4x))} 97 | true (fn [_] (hxx))}])) 98 | 99 | 100 | (defroutes handler-compojure 101 | (context "/user/:id/profile/:type/" [id type] 102 | (GET "/" request (h11 id type)) 103 | (PUT "/" request (h12 id type)) 104 | (ANY "/" request (h1x))) 105 | (context "/user/:id/permissions" [id] 106 | (GET "/" request (h21 id)) 107 | (PUT "/" request (h22 id)) 108 | (ANY "/" request (h2x))) 109 | (context "/company/:cid/dept/:did" [cid did] 110 | (PUT "/" request (h30 cid did)) 111 | (ANY "/" request (h3x))) 112 | (context "/this/is/a/static/route" [] 113 | (PUT "/" request (h40)) 114 | (ANY "/" request (h4x))) 115 | (rfn request (hxx))) 116 | 117 | 118 | (def handler-reitit 119 | (reitit/ring-handler 120 | (reitit/router 121 | [["/user/:id/profile/:type/" {:get (fn [{{:keys [id type]} :path-params}] (h11 id type)) 122 | :put (fn [{{:keys [id type]} :path-params}] (h12 id type)) 123 | :handler (constantly (h1x))}] 124 | ["/user/:id/permissions/" {:get (fn [{{:keys [id]} :path-params}] (h21 id)) 125 | :put (fn [{{:keys [id]} :path-params}] (h22 id)) 126 | :handler (constantly (h2x))}] 127 | ["/company/:cid/dept/:did/" {:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did)) 128 | :handler (constantly (h3x))}] 129 | ["/this/is/a/static/route" {:put (fn [_] (h40)) 130 | :handler (constantly (h4x))}]]) 131 | (constantly (hxx)) 132 | ;; as per https://github.com/kumarshantanu/calfpath/pull/12 comments 133 | {:inject-match? false 134 | :inject-router? false 135 | :reitit.trie/parameters reitit.trie/record-parameters})) 136 | 137 | 138 | (defmacro cond-let 139 | [& clauses] 140 | (i/expected (comp odd? count) "odd number of clauses" clauses) 141 | (if (= 1 (count clauses)) 142 | (first clauses) 143 | `(if-let ~(first clauses) 144 | ~(second clauses) 145 | (cond-let ~@(drop 2 clauses))))) 146 | 147 | 148 | (let [uri-1 (l/route-compile "/user/:id/profile/:type/") 149 | uri-2 (l/route-compile "/user/:id/permissions/") 150 | uri-3 (l/route-compile "/company/:cid/dept/:did/") 151 | uri-4 (l/route-compile "/this/is/a/static/route")] 152 | (defn handler-clout 153 | [request] 154 | (cond-let 155 | [{:keys [id type]} (l/route-matches uri-1 request)] (case (:request-method request) 156 | :get (h11 id type) 157 | :put (h12 id type) 158 | (h1x)) 159 | [{:keys [id]} (l/route-matches uri-2 request)] (case (:request-method request) 160 | :get (h21 id) 161 | :put (h22 id) 162 | (h2x)) 163 | [{:keys [cid did]} (l/route-matches uri-3 request)] (if (identical? :put (:request-method request)) 164 | (h30 cid did) 165 | (h3x)) 166 | [_ (l/route-matches uri-4 request)] (if (identical? :put (:request-method request)) 167 | (h40) 168 | (h4x)) 169 | (hxx)))) 170 | 171 | 172 | (defn handler-calfpath 173 | [request] 174 | (->uri request 175 | "/user/:id/profile/:type/" [id type] (->method request 176 | :get (h11 id type) 177 | :put (h12 id type) 178 | (h1x)) 179 | "/user/:id/permissions/" [id] (->method request 180 | :get (h21 id) 181 | :put (h22 id) 182 | (h2x)) 183 | "/company/:cid/dept/:did/" [cid did] (->put request (h30 cid did) (h3x)) 184 | "/this/is/a/static/route" [] (->put request (h40) (h4x)) 185 | (hxx))) 186 | 187 | 188 | (def calfpath-routes 189 | [{"/user/:id/profile/:type/" [{:get (fn [{{:keys [id type]} :path-params}] (h11 id type))} 190 | {:put (fn [{{:keys [id type]} :path-params}] (h12 id type))} 191 | {:matcher identity :handler (fn [_] (h1x))}]} 192 | {:uri "/user/:id/permissions/" :nested [{:method :get :handler (fn [{{:keys [id]} :path-params}] (h21 id))} 193 | {:method :put :handler (fn [{{:keys [id]} :path-params}] (h22 id))} 194 | {:matcher identity :handler (fn [_] (h2x))}]} 195 | {"/company/:cid/dept/:did/" [{:put (fn [{{:keys [cid did]} :path-params}] (h30 cid did))} 196 | {:matcher identity :handler (fn [_] (h3x))}]} 197 | {"/this/is/a/static/route" [{:put (fn [request] (h40))} 198 | {:matcher identity :handler (fn [_] (h4x))}]} 199 | {:matcher identity :handler (fn [_] (hxx))}]) 200 | 201 | 202 | (def compiled-calfpath-routes (r/compile-routes calfpath-routes {:show-uris-400? false})) 203 | 204 | 205 | (def handler-calfpath-route-walker 206 | (partial r/dispatch compiled-calfpath-routes)) 207 | 208 | 209 | (def handler-calfpath-route-unroll 210 | (r/make-dispatcher compiled-calfpath-routes)) 211 | 212 | 213 | (use-fixtures :once 214 | (c/make-bench-wrapper 215 | ["Ataraxy" "Bidi" "Compojure" "Clout" "Reitit" "CalfPath-macros" "CalfPath-route-walker" "CalfPath-route-unroll"] 216 | {:chart-title "Ataraxy/Bidi/Compojure/Clout/Reitit/CalfPath" 217 | :chart-filename (format "bench-small-routing-table-clj-%s.png" c/clojure-version-str)})) 218 | 219 | 220 | (defmacro test-compare-perf 221 | [bench-name & exprs] 222 | `(do 223 | (is (= ~@exprs) ~bench-name) 224 | (when-not (System/getenv "BENCH_DISABLE") 225 | (c/compare-perf ~bench-name ~@exprs)))) 226 | 227 | 228 | (deftest test-no-match 229 | (testing "no URI match" 230 | (let [request {:request-method :get 231 | :uri "/hello/joe/"}] 232 | (test-compare-perf (str "no URI match: " (pr-str request)) 233 | (handler-ataraxy request) (handler-bidi request) (handler-compojure request) (handler-clout request) 234 | (handler-reitit request) 235 | (handler-calfpath request) (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request)))) 236 | (testing "no method match" 237 | (let [request {:request-method :post 238 | :uri "/user/1234/profile/compact/"}] 239 | (test-compare-perf (str "no method match: " (pr-str request)) 240 | (handler-ataraxy request) (handler-bidi request) (handler-compojure request) (handler-clout request) 241 | (handler-reitit request) 242 | (handler-calfpath request) (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request))))) 243 | 244 | 245 | (deftest test-match 246 | (testing "static route match" 247 | (let [request {:request-method :put 248 | :uri "/this/is/a/static/route"}] 249 | (test-compare-perf (str "static URI match, 1 method: " (pr-str request)) 250 | (handler-ataraxy request) (handler-bidi request) (handler-compojure request) (handler-clout request) 251 | (handler-reitit request) 252 | (handler-calfpath request) (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request)))) 253 | (testing "pattern route match" 254 | (let [request {:request-method :get 255 | :uri "/user/1234/profile/compact/"}] 256 | (test-compare-perf (str "pattern URI match, 2 methods: " (pr-str request)) 257 | (handler-ataraxy request) (handler-bidi request) (handler-compojure request) (handler-clout request) 258 | (handler-reitit request) 259 | (handler-calfpath request) (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request))) 260 | (let [request {:request-method :get 261 | :uri "/company/1234/dept/5678/"}] 262 | (test-compare-perf (str "pattern URI match, 1 method: " (pr-str request)) 263 | (handler-ataraxy request) (handler-bidi request) (handler-compojure request) (handler-clout request) 264 | (handler-reitit request) 265 | (handler-calfpath request) (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request))))) 266 | -------------------------------------------------------------------------------- /perf/calfpath/long_perf_test.clj: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.long-perf-test 11 | "Performance benchmarks for long (OpenSensors) routing table." 12 | (:require 13 | [clojure.pprint :as pp] 14 | [clojure.test :refer [deftest is testing use-fixtures]] 15 | [ataraxy.core :as ataraxy] 16 | [bidi.ring :as bidi] 17 | [compojure.core :refer [defroutes rfn routes context GET POST PUT ANY]] 18 | [clout.core :as l] 19 | [reitit.ring :as reitit] 20 | [calfpath.core :as cp :refer [->uri ->method ->get ->head ->options ->put ->post ->delete]] 21 | [calfpath.internal :as i] 22 | [calfpath.route :as r] 23 | [citius.core :as c])) 24 | 25 | 26 | (use-fixtures :once 27 | (c/make-bench-wrapper 28 | ["Reitit" "CalfPath-core-macros" 29 | "CalfPath-route-walker" "CalfPath-route-unroll"] 30 | {:chart-title "Reitit/CalfPath" 31 | :chart-filename (format "bench-large-routing-table-clj-%s.png" c/clojure-version-str)})) 32 | 33 | 34 | (defn handler 35 | [request] 36 | {:status 200 37 | :headers {"Content-Type" "text/plain"} 38 | :body "OK"}) 39 | 40 | 41 | (defn p-handler 42 | [params] 43 | {:status 200 44 | :headers {"Content-Type" "text/plain"} 45 | :body (apply str params)}) 46 | 47 | 48 | (defmacro fnpp 49 | [params] 50 | (i/expected vector? "params vector" params) 51 | `(fn [{{:keys ~params} :path-params}] 52 | (p-handler ~params))) 53 | 54 | 55 | (def handler-reitit 56 | (reitit/ring-handler 57 | (reitit/router 58 | [["/v2/whoami" {:get handler}] 59 | ["/v2/users/:user-id/datasets" {:get (fnpp [user-id])}] 60 | ["/v2/public/projects/:project-id/datasets" {:get (fnpp [project-id])}] 61 | ["/v1/public/topics/:topic" {:get (fnpp [topic])}] 62 | ["/v1/users/:user-id/orgs/:org-id" {:get (fnpp [user-id org-id])}] 63 | ["/v1/search/topics/:term" {:get (fnpp [term])}] 64 | ["/v1/users/:user-id/invitations" {:get (fnpp [user-id])}] 65 | ["/v1/orgs/:org-id/devices/:batch/:type" {:get (fnpp [org-id batch type])}] 66 | ["/v1/users/:user-id/topics" {:get (fnpp [user-id])}] 67 | ["/v1/users/:user-id/bookmarks/followers" {:get (fnpp [user-id])}] 68 | ["/v2/datasets/:dataset-id" {:get (fnpp [dataset-id])}] 69 | ["/v1/orgs/:org-id/usage-stats" {:get (fnpp [org-id])}] 70 | ["/v1/orgs/:org-id/devices/:client-id" {:get (fnpp [org-id client-id])}] 71 | ["/v1/messages/user/:user-id" {:get (fnpp [user-id])}] 72 | ["/v1/users/:user-id/devices" {:get (fnpp [user-id])}] 73 | ["/v1/public/users/:user-id" {:get (fnpp [user-id])}] 74 | ["/v1/orgs/:org-id/errors" {:get (fnpp [org-id])}] 75 | ["/v1/public/orgs/:org-id" {:get (fnpp [org-id])}] 76 | ["/v1/orgs/:org-id/invitations" {:get (fnpp [org-id])}] 77 | ;;["/v2/public/messages/dataset/bulk" {:get handler}] 78 | ;;["/v1/users/:user-id/devices/bulk" {:get (fnpp [user-id])}] 79 | ["/v1/users/:user-id/device-errors" {:get (fnpp [user-id])}] 80 | ["/v2/login" {:get handler}] 81 | ["/v1/users/:user-id/usage-stats" {:get (fnpp [user-id])}] 82 | ["/v2/users/:user-id/devices" {:get (fnpp [user-id])}] 83 | ["/v1/users/:user-id/claim-device/:client-id" {:get (fnpp [user-id client-id])}] 84 | ["/v2/public/projects/:project-id" {:get (fnpp [project-id])}] 85 | ["/v2/public/datasets/:dataset-id" {:get (fnpp [dataset-id])}] 86 | ;;["/v2/users/:user-id/topics/bulk" {:get (fnpp [user-id])}] 87 | ["/v1/messages/device/:client-id" {:get (fnpp [client-id])}] 88 | ["/v1/users/:user-id/owned-orgs" {:get (fnpp [user-id])}] 89 | ["/v1/topics/:topic" {:get (fnpp [topic])}] 90 | ["/v1/users/:user-id/bookmark/:topic" {:get (fnpp [user-id topic])}] 91 | ["/v1/orgs/:org-id/members/:user-id" {:get (fnpp [org-id user-id])}] 92 | ["/v1/users/:user-id/devices/:client-id" {:get (fnpp [user-id client-id])}] 93 | ["/v1/users/:user-id" {:get (fnpp [user-id])}] 94 | ["/v1/orgs/:org-id/devices" {:get (fnpp [org-id])}] 95 | ["/v1/orgs/:org-id/members" {:get (fnpp [org-id])}] 96 | ["/v1/orgs/:org-id/members/invitation-data/:user-id" {:get (fnpp [org-id user-id])}] 97 | ["/v2/orgs/:org-id/topics" {:get (fnpp [org-id])}] 98 | ["/v1/whoami" {:get handler}] 99 | ["/v1/orgs/:org-id" {:get (fnpp [org-id])}] 100 | ["/v1/users/:user-id/api-key" {:get (fnpp [user-id])}] 101 | ["/v2/schemas" {:get handler}] 102 | ["/v2/users/:user-id/topics" {:get (fnpp [user-id])}] 103 | ["/v1/orgs/:org-id/confirm-membership/:token" {:get (fnpp [org-id token])}] 104 | ["/v2/topics/:topic" {:get (fnpp [topic])}] 105 | ["/v1/messages/topic/:topic" {:get (fnpp [topic])}] 106 | ["/v1/users/:user-id/devices/:client-id/reset-password" {:get (fnpp [user-id client-id])}] 107 | ["/v2/topics" {:get handler}] 108 | ["/v1/login" {:get handler}] 109 | ["/v1/users/:user-id/orgs" {:get (fnpp [user-id])}] 110 | ["/v2/public/messages/dataset/:dataset-id" {:get (fnpp [dataset-id])}] 111 | ["/v1/topics" {:get handler}] 112 | ["/v1/orgs" {:get handler}] 113 | ["/v1/users/:user-id/bookmarks" {:get (fnpp [user-id])}] 114 | ["/v1/orgs/:org-id/topics" {:get (fnpp [org-id])}]]) 115 | nil 116 | ;; as per https://github.com/kumarshantanu/calfpath/pull/12 comments 117 | {:inject-match? false 118 | :inject-router? false 119 | :reitit.trie/parameters reitit.trie/record-parameters})) 120 | 121 | 122 | (defn handler-calfpath [request] 123 | (cp/->uri request 124 | "/v2/whoami" [] (cp/->get request (handler request) nil) 125 | "/v2/users/:user-id/datasets" [user-id] (cp/->get request (p-handler [user-id]) nil) 126 | "/v2/public/projects/:project-id/datasets" [project-id] (cp/->get request (p-handler [project-id]) nil) 127 | "/v1/public/topics/:topic" [topic] (cp/->get request (p-handler [topic]) nil) 128 | "/v1/users/:user-id/orgs/:org-id" [user-id org-id] (cp/->get request (p-handler [user-id org-id]) nil) 129 | "/v1/search/topics/:term" [term] (cp/->get request (p-handler [term]) nil) 130 | "/v1/users/:user-id/invitations" [user-id] (cp/->get request (p-handler [user-id]) nil) 131 | "/v1/orgs/:org-id/devices/:batch/:type" [org-id batch type] (cp/->get request (p-handler [org-id batch type]) nil) 132 | "/v1/users/:user-id/topics" [user-id] (cp/->get request (p-handler [user-id]) nil) 133 | "/v1/users/:user-id/bookmarks/followers" [user-id] (cp/->get request (p-handler [user-id]) nil) 134 | "/v2/datasets/:dataset-id" [dataset-id] (cp/->get request (p-handler [dataset-id]) nil) 135 | "/v1/orgs/:org-id/usage-stats" [org-id] (cp/->get request (p-handler [org-id]) nil) 136 | "/v1/orgs/:org-id/devices/:client-id" [org-id client-id] (cp/->get request (p-handler [org-id client-id]) nil) 137 | "/v1/messages/user/:user-id" [user-id] (cp/->get request (p-handler [user-id]) nil) 138 | "/v1/users/:user-id/devices" [user-id] (cp/->get request (p-handler [user-id]) nil) 139 | "/v1/public/users/:user-id" [user-id] (cp/->get request (p-handler [user-id]) nil) 140 | "/v1/orgs/:org-id/errors" [org-id] (cp/->get request (p-handler [org-id]) nil) 141 | "/v1/public/orgs/:org-id" [org-id] (cp/->get request (p-handler [org-id]) nil) 142 | "/v1/orgs/:org-id/invitations" [org-id] (cp/->get request (p-handler [org-id]) nil) 143 | ;;"/v2/public/messages/dataset/bulk" [] (cp/->get request (handler request) nil) 144 | ;;"/v1/users/:user-id/devices/bulk" [user-id] (cp/->get request (handler request) nil) 145 | "/v1/users/:user-id/device-errors" [user-id] (cp/->get request (p-handler [user-id]) nil) 146 | "/v2/login" [] (cp/->get request (handler request) nil) 147 | "/v1/users/:user-id/usage-stats" [user-id] (cp/->get request (p-handler [user-id]) nil) 148 | "/v2/users/:user-id/devices" [user-id] (cp/->get request (p-handler [user-id]) nil) 149 | "/v1/users/:user-id/claim-device/:client-id" [user-id client-id] (cp/->get request (p-handler [user-id client-id]) nil) 150 | "/v2/public/projects/:project-id" [project-id] (cp/->get request (p-handler [project-id]) nil) 151 | "/v2/public/datasets/:dataset-id" [dataset-id] (cp/->get request (p-handler [dataset-id]) nil) 152 | ;;"/v2/users/:user-id/topics/bulk" [user-id] (cp/->get request (handler request) nil) 153 | "/v1/messages/device/:client-id" [client-id] (cp/->get request (p-handler [client-id]) nil) 154 | "/v1/users/:user-id/owned-orgs" [user-id] (cp/->get request (p-handler [user-id]) nil) 155 | "/v1/topics/:topic" [topic] (cp/->get request (p-handler [topic]) nil) 156 | "/v1/users/:user-id/bookmark/:topic" [user-id topic] (cp/->get request (p-handler [user-id topic]) nil) 157 | "/v1/orgs/:org-id/members/:user-id" [org-id user-id] (cp/->get request (p-handler [org-id user-id]) nil) 158 | "/v1/users/:user-id/devices/:client-id" [user-id client-id] (cp/->get request (p-handler [user-id client-id]) nil) 159 | "/v1/users/:user-id" [user-id] (cp/->get request (p-handler [user-id]) nil) 160 | "/v1/orgs/:org-id/devices" [org-id] (cp/->get request (p-handler [org-id]) nil) 161 | "/v1/orgs/:org-id/members" [org-id] (cp/->get request (p-handler [org-id]) nil) 162 | "/v1/orgs/:org-id/members/invitation-data/:user-id" [org-id user-id] (cp/->get request (p-handler [org-id user-id]) nil) 163 | "/v2/orgs/:org-id/topics" [org-id] (cp/->get request (p-handler [org-id]) nil) 164 | "/v1/whoami" [] (cp/->get request (handler request) nil) 165 | "/v1/orgs/:org-id" [org-id] (cp/->get request (p-handler [org-id]) nil) 166 | "/v1/users/:user-id/api-key" [user-id] (cp/->get request (p-handler [user-id]) nil) 167 | "/v2/schemas" [] (cp/->get request (handler request) nil) 168 | "/v2/users/:user-id/topics" [user-id] (cp/->get request (p-handler [user-id]) nil) 169 | "/v1/orgs/:org-id/confirm-membership/:token" [org-id token] (cp/->get request (p-handler [org-id token]) nil) 170 | "/v2/topics/:topic" [topic] (cp/->get request (p-handler [topic]) nil) 171 | "/v1/messages/topic/:topic" [topic] (cp/->get request (p-handler [topic]) nil) 172 | "/v1/users/:user-id/devices/:client-id/reset-password" [user-id client-id] (cp/->get request (p-handler [user-id client-id]) nil) 173 | "/v2/topics" [] (cp/->get request (handler request) nil) 174 | "/v1/login" [] (cp/->get request (handler request) nil) 175 | "/v1/users/:user-id/orgs" [user-id] (cp/->get request (p-handler [user-id]) nil) 176 | "/v2/public/messages/dataset/:dataset-id" [dataset-id] (cp/->get request (p-handler [dataset-id]) nil) 177 | "/v1/topics" [] (cp/->get request (handler request) nil) 178 | "/v1/orgs" [] (cp/->get request (handler request) nil) 179 | "/v1/users/:user-id/bookmarks" [user-id] (cp/->get request (p-handler [user-id]) nil) 180 | "/v1/orgs/:org-id/topics" [org-id] (cp/->get request (p-handler [org-id]) nil) 181 | nil)) 182 | 183 | 184 | (defmacro fnp 185 | [params] 186 | (i/expected vector? "params vector" params) 187 | `(fn [{:keys ~params}] 188 | (p-handler ~params))) 189 | 190 | 191 | (def opensensors-calfpath-routes 192 | [{["/v2/whoami" :get] handler} 193 | {["/v2/users/:user-id/datasets" :get] (fnpp [user-id])} 194 | {["/v2/public/projects/:project-id/datasets" :get] (fnpp [project-id])} 195 | {["/v1/public/topics/:topic" :get] (fnpp [topic])} 196 | {["/v1/users/:user-id/orgs/:org-id" :get] (fnpp [user-id org-id])} 197 | {["/v1/search/topics/:term" :get] (fnpp [term])} 198 | {["/v1/users/:user-id/invitations" :get] (fnpp [user-id])} 199 | {["/v1/orgs/:org-id/devices/:batch/:type" :get] (fnpp [org-id batch type])} 200 | {["/v1/users/:user-id/topics" :get] (fnpp [user-id])} 201 | {["/v1/users/:user-id/bookmarks/followers" :get] (fnpp [user-id])} 202 | {["/v2/datasets/:dataset-id" :get] (fnpp [dataset-id])} 203 | {["/v1/orgs/:org-id/usage-stats" :get] (fnpp [org-id])} 204 | {["/v1/orgs/:org-id/devices/:client-id" :get] (fnpp [org-id client-id])} 205 | {["/v1/messages/user/:user-id" :get] (fnpp [user-id])} 206 | {["/v1/users/:user-id/devices" :get] (fnpp [user-id])} 207 | {["/v1/public/users/:user-id" :get] (fnpp [user-id])} 208 | {["/v1/orgs/:org-id/errors" :get] (fnpp [org-id])} 209 | {["/v1/public/orgs/:org-id" :get] (fnpp [org-id])} 210 | {["/v1/orgs/:org-id/invitations" :get] (fnpp [org-id])} 211 | ;;{["/v2/public/messages/dataset/bulk" :get] handler} 212 | ;;{["/v1/users/:user-id/devices/bulk" :get] (fnpp [user-id])} 213 | {["/v1/users/:user-id/device-errors" :get] (fnpp [user-id])} 214 | {["/v2/login" :get] handler} 215 | {["/v1/users/:user-id/usage-stats" :get] (fnpp [user-id])} 216 | {["/v2/users/:user-id/devices" :get] (fnpp [user-id])} 217 | {["/v1/users/:user-id/claim-device/:client-id" :get] (fnpp [user-id client-id])} 218 | {["/v2/public/projects/:project-id" :get] (fnpp [project-id])} 219 | {["/v2/public/datasets/:dataset-id" :get] (fnpp [dataset-id])} 220 | ;;{["/v2/users/:user-id/topics/bulk" :get] (fnpp [user-id])} 221 | {["/v1/messages/device/:client-id" :get] (fnpp [client-id])} 222 | {["/v1/users/:user-id/owned-orgs" :get] (fnpp [user-id])} 223 | {["/v1/topics/:topic" :get] (fnpp [topic])} 224 | {["/v1/users/:user-id/bookmark/:topic" :get] (fnpp [user-id topic])} 225 | {["/v1/orgs/:org-id/members/:user-id" :get] (fnpp [org-id user-id])} 226 | {["/v1/users/:user-id/devices/:client-id" :get] (fnpp [user-id client-id])} 227 | {["/v1/users/:user-id" :get] (fnpp [user-id])} 228 | {["/v1/orgs/:org-id/devices" :get] (fnpp [org-id])} 229 | {["/v1/orgs/:org-id/members" :get] (fnpp [org-id])} 230 | {["/v1/orgs/:org-id/members/invitation-data/:user-id" :get] (fnpp [org-id user-id])} 231 | {["/v2/orgs/:org-id/topics" :get] (fnpp [org-id])} 232 | {["/v1/whoami" :get] handler} 233 | {["/v1/orgs/:org-id" :get] (fnpp [org-id])} 234 | {["/v1/users/:user-id/api-key" :get] (fnpp [user-id])} 235 | {["/v2/schemas" :get] handler} 236 | {["/v2/users/:user-id/topics" :get] (fnpp [user-id])} 237 | {["/v1/orgs/:org-id/confirm-membership/:token" :get] (fnpp [org-id token])} 238 | {["/v2/topics/:topic" :get] (fnpp [topic])} 239 | {["/v1/messages/topic/:topic" :get] (fnpp [topic])} 240 | {["/v1/users/:user-id/devices/:client-id/reset-password" :get] (fnpp [user-id client-id])} 241 | {["/v2/topics" :get] handler} 242 | {["/v1/login" :get] handler} 243 | {["/v1/users/:user-id/orgs" :get] (fnpp [user-id])} 244 | {["/v2/public/messages/dataset/:dataset-id" :get] (fnpp [dataset-id])} 245 | {["/v1/topics" :get] handler} 246 | {["/v1/orgs" :get] handler} 247 | {["/v1/users/:user-id/bookmarks" :get] (fnpp [user-id])} 248 | {["/v1/orgs/:org-id/topics" :get] (fnpp [org-id])}]) 249 | 250 | 251 | (def compiled-calfpath-routes (r/compile-routes opensensors-calfpath-routes {:show-uris-400? true})) 252 | 253 | 254 | ;; print trie-routes for debugging 255 | ;; 256 | ;(->> compiled-calfpath-routes 257 | ; (mapv (let [update-when (fn [m k & args] 258 | ; (if (contains? m k) 259 | ; (apply update m k args) 260 | ; m))] 261 | ; (fn cleanup [route] 262 | ; (-> route 263 | ; (dissoc :handler :matcher :matchex :full-uri) 264 | ; (update-when :nested #(when (seq %) (mapv cleanup %))))))) 265 | ; pp/pprint) 266 | 267 | 268 | (def handler-calfpath-route-walker 269 | (partial r/dispatch compiled-calfpath-routes)) 270 | 271 | 272 | (def handler-calfpath-route-unroll 273 | (r/make-dispatcher compiled-calfpath-routes)) 274 | 275 | 276 | (defmacro test-compare-perf 277 | [bench-name & exprs] 278 | `(do 279 | (is (= ~@exprs) ~bench-name) 280 | (when-not (System/getenv "BENCH_DISABLE") 281 | (c/compare-perf ~bench-name ~@exprs)))) 282 | 283 | 284 | (deftest test-static-path 285 | (testing "early" 286 | (let [request {:request-method :get 287 | :uri "/v2/whoami"}] 288 | (test-compare-perf (str "(early) static URI: " (pr-str request)) 289 | (handler-reitit request) (handler-calfpath request) 290 | (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request)))) 291 | (testing "mid" 292 | (let [request {:request-method :get 293 | :uri "/v2/login"}] 294 | (test-compare-perf (str "(mid) static URI: " (pr-str request)) 295 | (handler-reitit request) (handler-calfpath request) 296 | (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request)))) 297 | (testing "late" 298 | (let [request {:request-method :get 299 | :uri "/v1/orgs"}] 300 | (test-compare-perf (str "(late) static URI: " (pr-str request)) 301 | (handler-reitit request) (handler-calfpath request) 302 | (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request))))) 303 | 304 | 305 | (deftest test-dynamic-path 306 | (testing "early" 307 | (let [request {:request-method :get 308 | :uri "/v2/users/1234/datasets"}] 309 | (test-compare-perf (str "(early) dynamic URI: " (pr-str request)) 310 | (handler-reitit request) (handler-calfpath request) 311 | (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request))) 312 | (testing "mid" 313 | (let [request {:request-method :get 314 | :uri "/v2/public/projects/4567"}] 315 | (test-compare-perf (str "(mid) dynamic URI: " (pr-str request)) 316 | (handler-reitit request) (handler-calfpath request) 317 | (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request)))) 318 | (testing "late" 319 | (let [request {:request-method :get 320 | :uri "/v1/orgs/6789/topics"}] 321 | (test-compare-perf (str "(late) dynamic URI: " (pr-str request)) 322 | (handler-reitit request) (handler-calfpath request) 323 | (handler-calfpath-route-walker request) (handler-calfpath-route-unroll request)))))) 324 | -------------------------------------------------------------------------------- /src/calfpath/internal.cljc: -------------------------------------------------------------------------------- 1 | ; Copyright (c) Shantanu Kumar. All rights reserved. 2 | ; The use and distribution terms for this software are covered by the 3 | ; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) 4 | ; which can be found in the file LICENSE at the root of this distribution. 5 | ; By using this software in any fashion, you are agreeing to be bound by 6 | ; the terms of this license. 7 | ; You must not remove this notice, or any other, from this software. 8 | 9 | 10 | (ns calfpath.internal 11 | "Internal implementation details." 12 | #?(:cljs (:require-macros calfpath.internal)) 13 | (:require 14 | [clojure.set :as set] 15 | [clojure.string :as string]) 16 | #?(:clj (:import 17 | [java.util Map Map$Entry] 18 | [clojure.lang Associative] 19 | [calfpath Util VolatileInt]))) 20 | 21 | 22 | (defn expected 23 | ([expectation found] 24 | (throw (ex-info 25 | (str "Expected " expectation ", but found (" (type found) ") " (pr-str found)) 26 | {:found found}))) 27 | ([pred expectation found] 28 | (when-not (pred found) 29 | (expected expectation found)))) 30 | 31 | 32 | (defn dassoc 33 | "Direct assoc" 34 | [a k v] 35 | #?(:cljs (assoc a k v) 36 | :clj (.assoc ^clojure.lang.Associative a k v))) 37 | 38 | 39 | (defn parse-uri-template 40 | "Given a URI pattern string, 41 | e.g. \"/user/:id/profile/:descriptor/\" 42 | or \"/user/{id}/profile/{descriptor}/\" 43 | parse it as a vector of alternating string/keyword tokens, e.g. `[\"/user/\" :id \"/profile/\" :descriptor \"/\"]`. 44 | Last character `*` in the pattern string is considered partial URI pattern. Final return value is 45 | `[pattern-tokens partial?]`" 46 | [^String pattern] 47 | (let [[^String path partial?] (if (string/ends-with? pattern "*") 48 | [(subs pattern 0 (dec (count pattern))) true] ; chop off last char 49 | [pattern false]) 50 | n (count path)] 51 | (if (= "" path) 52 | [[""] partial?] 53 | (loop [i (int 0) ; current index in the URI string 54 | j (int 0) ; start index of the current token (string or keyword) 55 | s? true ; string in progress? (false implies keyword in progress) 56 | r []] 57 | (if (>= i n) 58 | [(if (>= j n) 59 | r 60 | (conj r (let [t (subs path j i)] 61 | (if s? 62 | t 63 | (keyword t))))) 64 | partial?] 65 | (let [^char ch (get path i) 66 | [jn s? r] (if s? 67 | (if (or (= \: ch) 68 | (= \{ ch)) 69 | [(unchecked-inc i) false (conj r (subs path j i))] 70 | [j true r]) 71 | (cond 72 | (= \/ ch) [i true (conj r (keyword (subs path j i)))] 73 | (= \} ch) [(unchecked-inc i) true (conj r (keyword (subs path j i)))] 74 | :else [j false r]))] 75 | (recur (unchecked-inc i) (int jn) s? r))))))) 76 | 77 | 78 | (defn as-uri-template 79 | [uri-pattern-or-template] 80 | (cond 81 | (string? uri-pattern-or-template) (parse-uri-template uri-pattern-or-template) 82 | (and (vector? uri-pattern-or-template) 83 | (every? (some-fn string? keyword?) 84 | uri-pattern-or-template)) uri-pattern-or-template 85 | :otherwise (expected "a string URI pattern or a parsed URI template" 86 | uri-pattern-or-template))) 87 | 88 | 89 | (def ^:const uri-match-end-index :calfpath/uri-match-end-index) 90 | 91 | 92 | (defn get-uri-match-end-index 93 | ^long [request] 94 | (if-some [vol (get request uri-match-end-index)] 95 | #?(:cljs (deref vol) 96 | :clj (VolatileInt/deref vol)) 97 | 0)) 98 | 99 | 100 | (defn assoc-uri-match-end-index 101 | [request ^long end-index] 102 | (if-let [vol (get request uri-match-end-index)] 103 | (do 104 | #?(:cljs (vreset! vol end-index) 105 | :clj (VolatileInt/reset vol end-index)) 106 | request) 107 | (dassoc request uri-match-end-index #?(:cljs (volatile! end-index) 108 | :clj (VolatileInt/create end-index))))) 109 | 110 | 111 | (def valid-method-keys #{:get :head :options :patch :put :post :delete}) 112 | 113 | 114 | (defmacro method-dispatch 115 | ([method-keyword request expr] 116 | (when-not (valid-method-keys method-keyword) 117 | (expected (str "a method key (" valid-method-keys ")") method-keyword)) 118 | (let [method-string (->> (name method-keyword) 119 | string/upper-case) 120 | default-expr {:status 405 121 | :headers {"Allow" method-string 122 | "Content-Type" "text/plain"} 123 | :body (str "405 Method not supported. Only " method-string " is supported.")}] 124 | ;; In CLJS `defmacro` is called by ClojureJVM, hence reader conditionals always choose :clj - 125 | ;; so we discover the environment using a hack (:ns &env), which returns truthy for CLJS. 126 | ;; Reference: https://groups.google.com/forum/#!topic/clojure/DvIxYnO1QLQ 127 | ;; Reference: https://dev.clojure.org/jira/browse/CLJ-1750 128 | `(if (~(if (:ns &env) `= `identical?) 129 | ~method-keyword (:request-method ~request)) 130 | ~expr 131 | ~default-expr))) 132 | ([method-keyword request expr default-expr] 133 | (when-not (valid-method-keys method-keyword) 134 | (expected (str "a method key (" valid-method-keys ")") method-keyword)) 135 | ;; In CLJS `defmacro` is called by ClojureJVM, hence reader conditionals always choose :clj - 136 | ;; so we discover the environment using a hack (:ns &env), which returns truthy for CLJS. 137 | ;; Reference: https://groups.google.com/forum/#!topic/clojure/DvIxYnO1QLQ 138 | ;; Reference: https://dev.clojure.org/jira/browse/CLJ-1750 139 | `(if (~(if (:ns &env) `= `identical?) 140 | ~method-keyword (:request-method ~request)) 141 | ~expr 142 | ~default-expr))) 143 | 144 | 145 | (defn dispatch-expr-methods 146 | "Bulk methods match" 147 | [routes 148 | matcher-syms 149 | handler-syms 150 | request-sym 151 | invoke-sym 152 | {:keys [method-key] 153 | :as options}] 154 | (let [case-rows (->> routes 155 | (map vector handler-syms) 156 | (reduce 157 | (fn [expr [each-handler-sym each-route]] 158 | (let [method (get each-route method-key) ; FIXME no hardcoding, get method-key 159 | matcher (:matcher each-route) 160 | invoker `(~invoke-sym ~each-handler-sym ~request-sym)] 161 | (cond 162 | (valid-method-keys 163 | method) (concat expr `[~method ~invoker]) 164 | (and (set? method) 165 | (set/subset? method 166 | valid-method-keys)) (concat expr `[~(list* method) ~invoker]) 167 | (= identity matcher) (reduced (concat expr `[~invoker])) 168 | :otherwise (throw (ex-info "Neither method, nor identity handler" 169 | {:route each-route}))))) 170 | []))] 171 | `(case (:request-method ~request-sym) 172 | ~@case-rows))) 173 | 174 | 175 | (defn dispatch-expr-generic 176 | "Generic (fallback) match" 177 | [routes 178 | matcher-syms 179 | handler-syms 180 | request-sym 181 | invoke-sym] 182 | (->> (count routes) 183 | range 184 | reverse 185 | (reduce (fn 186 | ([expr] 187 | expr) 188 | ([expr idx] 189 | (let [matcher-sym (get matcher-syms idx) 190 | matcher-val (:matcher (get routes idx)) 191 | matcher-exp (if-some [matchex (:matchex (get routes idx))] 192 | (matchex request-sym) 193 | `(~matcher-sym ~request-sym)) 194 | handler-sym (get handler-syms idx)] 195 | (if (= identity matcher-val) ; identity matcher would always match 196 | `(~invoke-sym ~handler-sym ~matcher-exp) ; so optimize 197 | `(if-some [request# ~matcher-exp] 198 | (~invoke-sym ~handler-sym request#) 199 | ~expr))))) 200 | `nil))) 201 | 202 | 203 | (defn make-dispatcher-expr 204 | "Emit code that matches route and invokes handler" 205 | [routes 206 | matcher-syms 207 | handler-syms 208 | request-sym 209 | invoke-sym 210 | options] 211 | (if (every? (some-fn :method #(= identity (:matcher %))) routes) 212 | (dispatch-expr-methods routes matcher-syms handler-syms request-sym invoke-sym options) 213 | (dispatch-expr-generic routes matcher-syms handler-syms request-sym invoke-sym))) 214 | 215 | 216 | (defn invoke 217 | "Invoke first arg as a function on remaing args." 218 | ([f] (f)) 219 | ([f x] (f x)) 220 | ([f x y] (f x y)) 221 | ([f x y & args] (apply f x y args))) 222 | 223 | 224 | (defn strip-partial-marker 225 | [x] 226 | (when (string? x) 227 | (if (string/ends-with? x "*") 228 | (subs x 0 (dec (count x))) 229 | x))) 230 | 231 | 232 | ;; helpers for `routes -> wildcard tidy` 233 | 234 | 235 | (defn deduplicate-paths 236 | [routes uri-key] 237 | (let [[with-path without-path] (reduce (fn [[with without] each-route] 238 | (if (contains? each-route uri-key) 239 | [(conj with each-route) without] 240 | [with (conj without each-route)])) 241 | [[] []] 242 | routes)] 243 | (concat (->> with-path ; de-duplicate common paths 244 | (sort-by uri-key) 245 | (partition-by uri-key) 246 | (mapv (fn [coll] 247 | (if (= 1 (count coll)) 248 | (first coll) 249 | {uri-key (get (first coll) uri-key) 250 | :nested (mapv #(dissoc % uri-key) coll)})))) 251 | without-path))) 252 | 253 | 254 | (defn split-routes-having-uri 255 | "Given mixed routes (vector), split into those having distinct routes and those that don't." 256 | [routes uri-key] 257 | (->> (deduplicate-paths routes uri-key) 258 | (reduce (fn [[with-uri no-uri] each-route] 259 | (if (and (contains? each-route uri-key) 260 | ;; wildcard already? then exclude 261 | (not (string/ends-with? ^String (get each-route uri-key) "*"))) 262 | [(conj with-uri each-route) no-uri] 263 | [with-uri (conj no-uri each-route)])) 264 | [[] []]))) 265 | 266 | 267 | (defn tokenize-routes-uris 268 | "Given routes with URI patterns, tokenize them as vectors." 269 | [routes-with-uri uri-key] 270 | (expected vector? "a vector of routes" routes-with-uri) 271 | (expected #(every? 272 | map? %) "a vector of routes" routes-with-uri) 273 | (expected some? "a non-nil URI key" uri-key) 274 | (mapv (fn [route] 275 | (expected map? "a route map" route) 276 | (let [^String uri-template (get route uri-key)] 277 | (as-> uri-template $ 278 | (string/split $ #"/") 279 | (mapv #(if (string/starts-with? ^String % ":") 280 | (keyword (subs % 1)) 281 | %) 282 | $) 283 | (if (string/ends-with? uri-template "/") 284 | (conj $ "") 285 | $)))) 286 | routes-with-uri)) 287 | 288 | 289 | (defn find-prefix-tokens 290 | "Given routes with URI-patterns, find the common (non empty) prefix URI pattern tokens." 291 | [routes-uri-tokens] 292 | (reduce (fn [tokens-a tokens-b] 293 | (->> (map vector tokens-a tokens-b) 294 | (take-while (fn [[a b]] (= a b))) 295 | (mapv first))) 296 | routes-uri-tokens)) 297 | 298 | 299 | (defn dropper 300 | [n] 301 | (fn [items] (->> items 302 | (drop n) 303 | vec))) 304 | 305 | 306 | (defn find-prefix-tokens-pair 307 | "Given routes with URI-patterns, find the common (non empty) prefix URI pattern tokens and balance tokens." 308 | [routes-uri-tokens] 309 | (let [prefix-tokens (find-prefix-tokens routes-uri-tokens)] 310 | (when-not (= [""] prefix-tokens) 311 | [prefix-tokens (-> (count prefix-tokens) 312 | dropper 313 | (mapv routes-uri-tokens))]))) 314 | 315 | 316 | (defn find-discriminator-tokens 317 | [routes-uri-tokens] 318 | (loop [token-count 1 319 | token-vectors routes-uri-tokens] 320 | (cond 321 | (every? empty? 322 | token-vectors) [nil 0] 323 | (->> token-vectors 324 | (map first) 325 | (apply =)) (recur (inc token-count) (mapv next token-vectors)) 326 | :else [(->> routes-uri-tokens 327 | (mapv #(vec (take token-count %)))) 328 | token-count]))) 329 | 330 | 331 | (declare tidyfy-all) 332 | 333 | 334 | (defn tidyfy [routes-with-uri ^long tidy-threshold uri-key] ; return vector of routes 335 | (expected vector? "vector of routes" routes-with-uri) 336 | (expected #(every? map? %) "vector of route-maps" routes-with-uri) 337 | (expected (every-pred 338 | integer? pos?) "a positive integer" tidy-threshold) 339 | (expected some? "a non-nil uri-key" uri-key) 340 | (let [routes-uri-tokens (tokenize-routes-uris routes-with-uri uri-key) ; [ [t1 t2 ..] [t1 t2 ..] ...] 341 | [prefix-tokens 342 | token-vectors] (find-prefix-tokens-pair routes-uri-tokens)] 343 | (if (seq prefix-tokens) 344 | ;; we found a common URI-prefix for all routes 345 | [{uri-key (-> "/" 346 | (string/join prefix-tokens) 347 | (str "*")) 348 | :nested (as-> token-vectors $ 349 | (mapv (fn [route tokens] 350 | (->> (string/join "/" tokens) 351 | (str (when (keyword? (first tokens)) "/")) 352 | (assoc route uri-key))) 353 | routes-with-uri $) 354 | (tidyfy-all $ tidy-threshold uri-key))}] 355 | ;; we need to find URI-prefix groups now 356 | (let [[first-tokens 357 | first-count] (find-discriminator-tokens routes-uri-tokens) 358 | token-counts (->> routes-uri-tokens 359 | (group-by #(take first-count %)) 360 | (reduce-kv #(assoc %1 %2 (count %3)) {}))] 361 | (if (->> [routes-with-uri routes-uri-tokens first-tokens] 362 | (map count) 363 | (apply =)) 364 | (->> [routes-with-uri routes-uri-tokens first-tokens] 365 | (apply map vector) 366 | (sort-by (comp str last)) ; turn keywords into string for comparison 367 | (partition-by last) 368 | (reduce (fn [result-routes batch] 369 | (if (> (count batch) tidy-threshold) 370 | (let [[sub-prefix-tokens _] (-> (mapv first batch) 371 | (tokenize-routes-uris uri-key) ; [ [t1 t2 ..] [t1 t2 ..] ...] 372 | (find-prefix-tokens-pair)) ; look-ahead prefix tokens 373 | prefix-tokens-count (count sub-prefix-tokens)] 374 | (conj result-routes 375 | {uri-key (as-> sub-prefix-tokens $ 376 | (string/join "/" $) 377 | (str $ "*")) 378 | :nested (as-> batch $ 379 | (mapv (fn [[r ts ft]] 380 | (assoc r uri-key (if-some [toks (seq (drop prefix-tokens-count ts))] 381 | (->> toks 382 | (string/join "/") 383 | (str "/")) 384 | ""))) $) 385 | (tidyfy-all $ tidy-threshold uri-key))})) 386 | (->> batch 387 | (mapv first) 388 | (into result-routes)))) 389 | [])) 390 | routes-with-uri))))) 391 | 392 | 393 | (defn tidyfy-all 394 | [routes ^long tidy-threshold uri-key] 395 | (expected (every-pred 396 | integer? pos?) "value of :tidy-threshold must be positive integer" tidy-threshold) 397 | (let [[with-uri no-uri] (split-routes-having-uri routes uri-key)] 398 | (if (> (count with-uri) tidy-threshold) 399 | (-> (tidyfy with-uri tidy-threshold uri-key) 400 | vec 401 | (into no-uri)) 402 | routes))) 403 | 404 | 405 | ;; ----- URI match ----- 406 | 407 | 408 | (def ^:const NO-PARAMS {}) 409 | 410 | 411 | (def ^:const FULL-MATCH-INDEX -1) 412 | (def ^:const NO-MATCH-INDEX -2) 413 | 414 | 415 | (def ^"[Ljava.lang.Object;" FULL-MATCH-NO-PARAMS (object-array [NO-PARAMS FULL-MATCH-INDEX])) 416 | 417 | 418 | (defn partial-match 419 | (^"[Ljava.lang.Object;" [end-index] #?(:cljs (array NO-PARAMS end-index) 420 | :clj (Util/array NO-PARAMS end-index))) 421 | (^"[Ljava.lang.Object;" [params end-index] #?(:cljs (array params end-index) 422 | :clj (Util/array params end-index)))) 423 | 424 | 425 | (defn full-match 426 | ^"[Ljava.lang.Object;" [params] 427 | #?(:cljs (array params FULL-MATCH-INDEX) 428 | :clj (Util/array params FULL-MATCH-INDEX))) 429 | 430 | 431 | (defn match-uri* 432 | ^"[Ljava.lang.Object;" 433 | [uri begin-index pattern-tokens attempt-partial-match? params-map] 434 | (let [begin-index (int begin-index) 435 | token-count (count pattern-tokens) 436 | static-path (first pattern-tokens)] 437 | (if (= begin-index FULL-MATCH-INDEX) ; if already a full-match then no need to match any further 438 | (when (and (= 1 token-count) (= "" static-path)) 439 | FULL-MATCH-NO-PARAMS) 440 | (let [actual-uri (subs uri begin-index) 441 | actual-len (count actual-uri)] 442 | (if (and (= 1 token-count) (string? static-path)) ; if length==1 and token is string, then it's a static URI 443 | (when (string/starts-with? actual-uri static-path) ; URI begins with path, so at least partial match exists 444 | (let [static-size (count static-path)] 445 | (if (= (count actual-uri) static-size) ; if full match exists, then return as such 446 | FULL-MATCH-NO-PARAMS 447 | (when attempt-partial-match? 448 | (partial-match (unchecked-add begin-index static-size)))))) 449 | (loop [path-params (transient (or params-map {})) 450 | actual-index 0 451 | next-tokens (seq pattern-tokens)] 452 | (if (and next-tokens (< actual-index actual-len)) 453 | (let [token (first next-tokens)] 454 | (if (string? token) 455 | ;; string token 456 | (when (string/starts-with? (subs actual-uri actual-index) token) 457 | (recur path-params (unchecked-add actual-index (count token)) (next next-tokens))) 458 | ;; must be a keyword 459 | (let [[u-path-params 460 | u-actual-index] (loop [sb (transient []) ; string buffer 461 | j actual-index] 462 | (if (>= j actual-len) ; 'separator not found' implies URI has ended 463 | [(assoc! path-params token (apply str (persistent! sb))) 464 | actual-len] 465 | (let [ch (get actual-uri j)] 466 | (if (= \/ ch) 467 | [(assoc! path-params token (apply str (persistent! sb))) 468 | j] 469 | (recur (conj! sb ch) (unchecked-inc j))))))] 470 | (recur u-path-params (long u-actual-index) (next next-tokens))))) 471 | (if (< actual-index actual-len) 472 | (when attempt-partial-match? 473 | (partial-match (persistent! path-params) (unchecked-add begin-index actual-index))) 474 | (full-match (persistent! path-params)))))))))) 475 | 476 | 477 | (defmacro match-uri 478 | "Match given URI string against URI pattern, returning a vector `[params-map ^int end-index]` on success, 479 | and `nil` on no match. 480 | 481 | | Argument | Description | 482 | |------------------------|---------------------------------------------------| 483 | | uri | the URI string to match | 484 | | begin-index | index in the URI string to start matching at | 485 | | pattern-tokens | URI pattern tokens to match against | 486 | | attempt-partial-match? | flag to indicate whether to attempt partial-match |" 487 | ([uri begin-index pattern-tokens attempt-partial-match?] 488 | `(match-uri ~uri ~begin-index ~pattern-tokens ~attempt-partial-match? nil)) 489 | ([uri begin-index pattern-tokens attempt-partial-match? params-map] 490 | ;; In CLJS `defmacro` is called by ClojureJVM, hence reader conditionals always choose :clj - 491 | ;; so we discover the environment using a hack (:ns &env), which returns truthy for CLJS. 492 | ;; Reference: https://groups.google.com/forum/#!topic/clojure/DvIxYnO1QLQ 493 | ;; Reference: https://dev.clojure.org/jira/browse/CLJ-1750 494 | (if (:ns &env) 495 | ;; CLJS 496 | `(match-uri* ~uri ~begin-index ~pattern-tokens ~attempt-partial-match? ~params-map) 497 | ;; CLJ 498 | `(Util/matchURI ~uri ~begin-index ~pattern-tokens ~attempt-partial-match? ~params-map)))) 499 | 500 | 501 | (defn assoc-path-params 502 | [request params-key params-map] 503 | #?(:cljs (dassoc request params-key params-map) 504 | :clj (if (contains? request params-key) 505 | request 506 | (dassoc request params-key params-map)))) 507 | 508 | 509 | (defn partial-match-uri-string 510 | ^long [uri ^long begin-index string-token] 511 | (cond 512 | (zero? begin-index) (if (string/starts-with? uri string-token) 513 | (let [token-length (count string-token)] 514 | (if (= (count uri) token-length) 515 | FULL-MATCH-INDEX 516 | token-length)) 517 | NO-MATCH-INDEX) 518 | (pos? begin-index) (let [sub-uri (subs uri begin-index)] 519 | (if (string/starts-with? sub-uri string-token) 520 | (let [token-length (count string-token)] 521 | (if (= (count sub-uri) token-length) 522 | FULL-MATCH-INDEX 523 | (unchecked-add begin-index token-length))) 524 | NO-MATCH-INDEX)) 525 | (= FULL-MATCH-INDEX 526 | begin-index) (if (= "" string-token) 527 | FULL-MATCH-INDEX 528 | NO-MATCH-INDEX) 529 | :otherwise NO-MATCH-INDEX)) 530 | 531 | 532 | (defn full-match-uri-string* 533 | ^long [uri ^long begin-index string-token] 534 | (cond 535 | (zero? begin-index) (if (= uri string-token) 536 | FULL-MATCH-INDEX 537 | NO-MATCH-INDEX) 538 | (pos? begin-index) (let [sub-uri (subs uri begin-index)] 539 | (if (and (string/starts-with? sub-uri string-token) 540 | (= (count sub-uri) (count string-token))) 541 | FULL-MATCH-INDEX 542 | NO-MATCH-INDEX)) 543 | (= FULL-MATCH-INDEX 544 | begin-index) (if (= "" string-token) 545 | FULL-MATCH-INDEX 546 | NO-MATCH-INDEX) 547 | :otherwise NO-MATCH-INDEX)) 548 | 549 | 550 | (defn full-match-uri-string 551 | ^long [uri ^long begin-index string-token] 552 | #?(:cljs (full-match-uri-string* uri begin-index string-token) 553 | :clj (Util/fullMatchURIString uri begin-index string-token))) 554 | 555 | 556 | ;; ----- routes indexing ----- 557 | 558 | 559 | (defn build-routes-index 560 | "Given a collection of routes, index them returning a map {:id the-route}." 561 | [context routes {:keys [index-key 562 | uri-key 563 | method-key] 564 | :or {index-key :id 565 | uri-key :uri 566 | method-key :method} 567 | :as options}] 568 | (expected map? "a context map" context) 569 | (expected coll? "collection of routes" routes) 570 | (reduce (fn [{:keys [uri-prefix] :as context} each-route] 571 | (expected map? "every route to be a map" each-route) 572 | (let [is-key? (fn [k] (contains? each-route k)) 573 | [partial? 574 | uri-now] (if (contains? each-route uri-key) 575 | (let [uri-string (get each-route uri-key)] 576 | [(string/ends-with? uri-string "*") 577 | (->> uri-string 578 | strip-partial-marker 579 | (str uri-prefix))]) 580 | [false uri-prefix])] 581 | (cond-> context 582 | (is-key? method-key) (update :method (fn [_] (get each-route method-key))) 583 | (is-key? index-key) (as-> $ 584 | (update $ :index-map (fn [imap] (if-some [index-val (get each-route index-key)] 585 | (assoc imap index-val 586 | {:uri (as-> (strip-partial-marker uri-now) $ 587 | (parse-uri-template $) 588 | (first $) 589 | (concat $ (when partial? [:*])) 590 | (vec $)) 591 | :request-method (:method $)}) 592 | imap)))) 593 | (is-key? :nested) (as-> $ 594 | (update $ :index-map (fn [imap] (:index-map 595 | (build-routes-index 596 | (assoc $ :uri-prefix uri-now) 597 | (:nested each-route) options)))))))) 598 | context routes)) 599 | --------------------------------------------------------------------------------