From 9e8abbf26d4422ba133bba7186363bee5b1f38d7 Mon Sep 17 00:00:00 2001 From: Asko Nõmm Date: Wed, 5 Oct 2022 22:29:03 +0300 Subject: 1.3.0 --- README.md | 9 +++++ project.clj | 10 +++-- src/ruuter/core.cljc | 99 ++++++++++++++++++++++++++-------------------- test/ruuter/core_test.cljc | 14 +++++-- 4 files changed, 82 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 21e70ba..5384ea8 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,10 @@ To create parameters from the path, prepend a colon (:) in front of a path slice Additionally, you may want to use an optional parameter, in which case you'd want to add a question mark to the end of it, like `/hi/:name?`, which will match the `\/hi\/?.*?` regex, meaning that the previous forward slash is optional, and what comes after that is also optional. +##### Wildcard matching + +The above-mentioned `:name` and `:name?` only match in its own sequence, e.g inside a space of two slashes. They cannot, by design, match the whole URL path. If you need wildcard matching, instead use `:name*`, which will match everything, including forward slashes. + #### `:method` The HTTP method to listen for when matching the given path. This can be whatever the HTTP server uses. For example, if you're using http-kit for the HTTP server then the accepted values are: @@ -157,6 +161,11 @@ What the actual map can contain that you return depends again on the HTTP server ## Changelog +### 1.3.0 + +- Fixed an issue with optional parameters not matching correctly when there were multiple optional paremeters in use. +- Implemented wildcard parameters in the form of `:name*`, which will match everything including forward slashes. + ### 1.2.2 - Fixed an issue where CLJS compilation would fail because of the `(:gen-class)` that is JVM-only. diff --git a/project.clj b/project.clj index 4d1d18d..29f048e 100644 --- a/project.clj +++ b/project.clj @@ -1,11 +1,13 @@ -(defproject org.clojars.askonomm/ruuter "1.2.2" +(defproject org.clojars.askonomm/ruuter "1.3.0" :description "A tiny HTTP router" :url "https://github.com/askonomm/ruuter" :license {:name "MIT" :url "https://raw.githubusercontent.com/askonomm/ruuter/master/LICENSE.txt"} - :dependencies [[org.clojure/clojure "1.10.3"] - [org.clojure/clojurescript "1.10.879"]] - :plugins [[lein-cloverage "1.2.2"]] + :dependencies [[org.clojure/clojure "1.11.1"] + [org.clojure/clojurescript "1.11.60"]] + :deploy-repositories [["releases" :clojars] + ["snapshots" :clojars]] + :plugins [[lein-cloverage "1.2.3"]] :main ruuter.core :min-lein-version "2.0.0" :aot [ruuter.core] diff --git a/src/ruuter/core.cljc b/src/ruuter/core.cljc index 444788f..6eaa912 100644 --- a/src/ruuter/core.cljc +++ b/src/ruuter/core.cljc @@ -1,57 +1,74 @@ (ns ruuter.core - (:require [clojure.string :as string]) + (:require + [clojure.string :as string]) #?(:clj (:gen-class))) - (defn- path->regex-path "Takes in a raw route `path` and turns it into a regex pattern to match against the request URI." [path] - (if (= "/" path) - "\\/" - (->> (string/split path #"/") - (map #(cond - ; matches anything, and must be present - ; for example `:name` - (and (string/starts-with? % ":") - (not (string/ends-with? % "?"))) - ".*" - ; matches anything, but is optional - ; for example `:name?` - (and (string/starts-with? % ":") - (string/ends-with? % "?")) - "?.*?" - :else - ; what comes around, goes around - %)) - (string/join "\\/")))) + (cond (= "/" path) + "\\/" + + (re-find #"\*" path) + (-> (string/replace path #"\:.*?\*" ".*?") + (string/replace #"/" "\\/")) + :else + (->> (string/split path #"/") + (map #(cond + ; matches anything, and must be present + ; for example `:name` + (and (string/starts-with? % ":") + (not (string/ends-with? % "?"))) + ".*" + ; matches anything, but is optional + ; for example `:name?` + (and (string/starts-with? % ":") + (string/ends-with? % "?")) + "?.*?" + :else + ; what comes around, goes around + %)) + (string/join "\\/")))) (defn- path+uri->path-params "Takes a raw route `path` and the actual request `uri`, which it then turns into a map of k:v, if any parameters were used in the `path`." [path uri] - (if (= "/" path) - {} - (let [split-path (string/split path #"/") - split-uri (string/split uri #"/")] - (into {} (map-indexed - (fn [idx item] - (cond - ; required parameter - (and (string/starts-with? item ":") - (not (string/ends-with? item "?"))) - {(keyword (subs item 1)) (get split-uri idx)} - ; optional parameter - (and (string/starts-with? item ":") - (string/ends-with? item "?") - (get split-uri idx)) - {(keyword (-> item - (subs 0 (- (count item) 1)) - (subs 1))) - (get split-uri idx)})) - split-path))))) + (cond (= "/" path) + {} + + (re-find #"\*" path) + (let [index-of-k-start (string/index-of path ":") + k (-> (subs path (+ 1 index-of-k-start)) + keyword) + v (subs uri (+ 1 index-of-k-start))] + {k v}) + :else + (let [split-path (->> (string/split path #"/") + (remove empty?) + vec) + split-uri (->> (string/split uri #"/") + (remove empty?) + vec)] + (into {} (map-indexed + (fn [idx item] + (cond + ; required parameter + (and (string/starts-with? item ":") + (not (string/ends-with? item "?"))) + {(keyword (subs item 1)) (get split-uri idx)} + ; optional parameter + (and (string/starts-with? item ":") + (string/ends-with? item "?") + (get split-uri idx)) + {(keyword (-> item + (subs 0 (- (count item) 1)) + (subs 1))) + (get split-uri idx)})) + split-path))))) (defn- match-route "For a collection of `route`, will attempt to find one that matches @@ -67,7 +84,6 @@ (when route (dissoc route :regex-path)))) - (defn- route+req->response "Given the current route and the current HTTP request, it will attempt to return a response, either directly if it's a map or @@ -96,7 +112,6 @@ {:status 404 :body "Not found."})) - (defn route "For a given collection of `routes` and the current HTTP request as `req`, will attempt to match a route with the HTTP request, which it diff --git a/test/ruuter/core_test.cljc b/test/ruuter/core_test.cljc index 9f1df23..75a71a0 100644 --- a/test/ruuter/core_test.cljc +++ b/test/ruuter/core_test.cljc @@ -4,7 +4,6 @@ #?(:cljs (:require [cljs.test :refer-macros [deftest testing is]] [ruuter.core :as ruuter]))) - (deftest path+uri->path-params-test (let [testfn #'ruuter/path+uri->path-params] (testing "No params returns an empty map" @@ -22,8 +21,16 @@ (testfn "/hello/:who/:why?" "/hello/world"))) (is (= {:who "world" :why "because"} - (testfn "/hello/:who/:why?" "/hello/world/because")))))) - + (testfn "/hello/:who/:why?" "/hello/world/because")))) + (testing "Multiple params, but all are optional" + (is (= {:who "world" + :why "because"} + (testfn "hello/:who?/:why?" "/hello/world/because"))) + (is (= {:who "world"} + (testfn "hello/:who?/:why?" "/hello/world")))) + (testing "Wildcard param" + (is (= {:everything* "this/means/literally/everything"} + (testfn "hello/:everything*" "/hello/this/means/literally/everything")))))) (deftest match-route-test (let [testfn #'ruuter/match-route] @@ -40,7 +47,6 @@ (is (= nil (testfn [] "/hello" :get)))))) - (deftest route+req->response-test (let [testfn #'ruuter/route+req->response] (testing "Returning a map when the response is a direct map" -- cgit v1.2.3