summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAsko Nõmm <asko@bien.ee>2021-10-03 12:50:37 -0300
committerAsko Nõmm <asko@bien.ee>2021-10-03 12:50:37 -0300
commite1b8f0969a4bd5f1e1e390364a55ffc6aa141d65 (patch)
tree625dbd89675daa807c375a5df66449b79a72c855
parentbf91cdfd50daffaa4814e2271a2eba82674fba76 (diff)
Optional path parameters, ClojureScript support and Babashka example in README
-rw-r--r--README.md38
-rw-r--r--project.clj2
-rw-r--r--src/ruuter/core.cljc (renamed from src/ruuter/core.clj)52
-rw-r--r--test/ruuter/core_test.clj24
4 files changed, 87 insertions, 29 deletions
diff --git a/README.md b/README.md
index e23b6dc..a6d4180 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Ruuter
-A tiny HTTP router that operates with a simple data structure where each route is a map inside a vector. Yup, that's it. No magic, no bullshit.
+A tiny, zero dependency HTTP router for Clojure(Script) that operates with a simple data structure where each route is a map inside a vector. Yup, that's it. No magic, no bullshit.
## Installation
@@ -78,6 +78,30 @@ Now, obviously on its own the router is not very useful as it needs an actual HT
(jetty/run-jetty #(ruuter/route routes %) {:port 8080}))
```
+### Setting up with [Babashka](https://github.com/babashka/babashka)
+
+You can also use Ruuter with [Babashka](https://github.com/babashka/babashka), by using the built-in http-kit server, for example. Either add the dependency in your `bb.edn` file or if you want to make the whole thing one-file-rules-them-all, then load it in with `deps/add-deps`, like below:
+
+```clojure
+#!/usr/bin/env bb
+
+(require '[org.httpkit.server :as http]
+ '[babashka.deps :as deps])
+
+(deps/add-deps '{:deps {org.clojars.askonomm/ruuter {:mvn/version "1.1.0"}}})
+
+(require '[ruuter.core :as ruuter])
+
+(def routes [{:path "/"
+ :method :get
+ :response {:status 200
+ :body "Hi there!"}}])
+
+(http/run-server #(ruuter/route routes %) {:port 8082})
+
+@(promise)
+```
+
### Creating routes
Like mentioned above, each route is a map inside of a vector - the order is important only in that the route matcher will return the first result it finds according to `:path`.
@@ -88,7 +112,7 @@ Each route consists of three items:
A string path starting with a forward slash describing the URL path to match.
-To create parameters from the path, prepend a colon (:) in front of a path slice like you would with a Clojure keyword. For example a string such as `/hi/:name` would match any string that matches the `/hi/.*` regex. The `:name` itself will then be available with its value from the `request` passed to the response function, like this:
+To create parameters from the path, prepend a colon (:) in front of a path slice like you would with a Clojure keyword. For example a string such as `/hi/:name` would match any string that matches the `\/hi\/.*` regex. The `:name` itself will then be available with its value from the `request` passed to the response function, like this:
```clojure
(fn [req]
@@ -97,6 +121,8 @@ To create parameters from the path, prepend a colon (:) in front of a path slice
:body (str "Hi, " name)}))
```
+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.
+
#### `: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:
@@ -132,6 +158,14 @@ What the actual map can contain that you return depends again on the HTTP server
## Changelog
+### 1.2.0
+
+- Implemented optional route parameters, so now you can do paths like `/hi/:name?` in your routes, and it would match the route even if the `:name` is not present. All you have to do is add a question mark to the parameter, and that's it.
+
+- Changed Ruuter from a .clj file to a .cljc file, so it would also work with ClojureScript. Although it would probably require a more hands-on set-up than just a drop-in to an HTTP server like http-kit or ring + jetty, there is no reason that the router itself wouldn't work as it does not rely on any platform-specific code.
+
+- Ruuter also works with [Babashka](https://github.com/babashka/babashka), and I've created a "Setting up with Babashka" section in this README to show that.
+
### 1.1.0
- Made Ruuter server-agnostic, which means now it really is just a router and nothing else, and can thus be used with just about any HTTP server you can throw at it. It also means there are now zero dependencies! ZERO! \ No newline at end of file
diff --git a/project.clj b/project.clj
index 21498d6..7674f4e 100644
--- a/project.clj
+++ b/project.clj
@@ -1,4 +1,4 @@
-(defproject org.clojars.askonomm/ruuter "1.1.0"
+(defproject org.clojars.askonomm/ruuter "1.2.0"
:description "A tiny HTTP router"
:url "https://github.com/askonomm/ruuter"
:license {:name "MIT"
diff --git a/src/ruuter/core.clj b/src/ruuter/core.cljc
index f811802..0da46b6 100644
--- a/src/ruuter/core.clj
+++ b/src/ruuter/core.cljc
@@ -8,13 +8,24 @@
match against the request URI."
[path]
(if (= "/" path)
- path
+ "\\/"
(->> (string/split path #"/")
- (map (fn [piece]
- (if (string/starts-with? piece ":")
- ".*"
- piece)))
- (string/join "/"))))
+ (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 "\\/")
+ (re-pattern))))
(defn- path+uri->path-params
@@ -27,8 +38,19 @@
split-uri (string/split uri #"/")]
(into {} (map-indexed
(fn [idx item]
- (when (string/starts-with? item ":")
- {(keyword (subs item 1)) (get split-uri idx)}))
+ (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)))))
@@ -37,12 +59,14 @@
the given `uri` and `request-method`. If none is matched, `nil` will
be returned instead."
[routes uri request-method]
- (->> routes
- (filter #(not (= :not-found (:path %))))
- (map #(merge % {:regex-path (path->regex-path (:path %))}))
- (filter #(and (re-matches (re-pattern (:regex-path %)) uri)
- (= (:method %) request-method)))
- first))
+ (let [route (->> routes
+ (filter #(not (= :not-found (:path %))))
+ (map #(merge % {:regex-path (path->regex-path (:path %))}))
+ (filter #(and (re-matches (:regex-path %) uri)
+ (= (:method %) request-method)))
+ first)]
+ (when route
+ (dissoc route :regex-path))))
(defn- route+req->response
diff --git a/test/ruuter/core_test.clj b/test/ruuter/core_test.clj
index c735f1a..4f60054 100644
--- a/test/ruuter/core_test.clj
+++ b/test/ruuter/core_test.clj
@@ -2,31 +2,31 @@
(:require [clojure.test :refer :all]
[ruuter.core :as ruuter]))
-(deftest path->regex-path-test
- (let [testfn #'ruuter/path->regex-path]
- (testing "Converting a path to a regex path with no params"
- (is (= "/hello/world" (testfn "/hello/world"))))
- (testing "Converting a path to a regex path with params"
- (is (= "/hello/.*" (testfn "/hello/:who")))
- (is (= "/.*/.*/.*" (testfn "/:these/:are/:params"))))))
-
(deftest path+uri->path-params-test
(let [testfn #'ruuter/path+uri->path-params]
(testing "No params returns an empty map"
- (is (= {} (testfn "/hello/world" "/hello/world"))))
+ (is (= {}
+ (testfn "/hello/world" "/hello/world"))))
(testing "Having a param returns a map accordingly"
- (is (= {:who "world"} (testfn "/hello/:who" "/hello/world"))))
+ (is (= {:who "world"}
+ (testfn "/hello/:who" "/hello/world"))))
(testing "Multiple params returns a map accordingly"
(is (= {:who "world"
- :why "because"} (testfn "/hello/:who/:why" "/hello/world/because"))))))
+ :why "because"}
+ (testfn "/hello/:who/:why" "/hello/world/because"))))
+ (testing "Multiple params, but one is optional"
+ (is (= {:who "world"}
+ (testfn "/hello/:who/:why?" "/hello/world")))
+ (is (= {:who "world"
+ :why "because"}
+ (testfn "/hello/:who/:why?" "/hello/world/because"))))))
(deftest match-route-test
(let [testfn #'ruuter/match-route]
(testing "Find a route that exists"
(is (= {:path "/hello"
- :regex-path "/hello"
:method :get
:response {:status 200
:body "Hello."}}