summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
Diffstat (limited to 'README.md')
-rw-r--r--README.md98
1 files changed, 62 insertions, 36 deletions
diff --git a/README.md b/README.md
index 5bbba5f..4cb6802 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,13 @@ A tiny, zero dependency, system-agnostic router for Clojure, ClojureScript, Baba
## Installation
-[![Clojars Project](https://img.shields.io/clojars/v/org.clojars.askonomm/ruuter.svg)](https://clojars.org/org.clojars.askonomm/ruuter)
+Add Ruuter as a git dependency in your `deps.edn`:
+
+```clojure
+{:deps {askonomm/ruuter {:git/url "https://git.nmm.ee/asko/ruuter.git"
+ :git/tag "v2.0.0"
+ :git/sha "<sha>"}}}
+```
## Usage
@@ -28,7 +34,7 @@ Require the namespace `ruuter.core` and then pass your routes to the `route` fun
; :body "Hi there!"}
```
-This will attempt to match a route with the request map and return the matched route' response. If no route was found, it will attempt to find a route that has a `:path` that is `:not-found`, and return its response instead. But if not even that route was found, it will simply return a built-in 404 response instead.
+This will attempt to match the best route for the request map and return its response. Routes are matched using **best-match semantics** — the most specific route always wins, regardless of the order routes appear in the vector. If no route was found, it will attempt to find a route that has a `:path` that is `:not-found`, and return its response instead. But if not even that route was found, it will simply return a built-in 404 response instead.
Note that the `request-method` doesn't have to be a keyword, it can be anything that your HTTP server returns. But it does have to be called `request-method` for the router to know where to look for. That said, you do not have to provide neither `method` in the route, nor `request-method` in the request if you don't want to. You can skip both of them and let Ruuter route based on the `:uri` alone if you want.
@@ -85,7 +91,9 @@ You can also use Ruuter with [Babashka](https://github.com/babashka/babashka), b
```clojure
#!/usr/bin/env bb
-(deps/add-deps '{:deps {org.clojars.askonomm/ruuter {:mvn/version "1.3.5"}}})
+(deps/add-deps '{:deps {askonomm/ruuter {:git/url "https://git.nmm.ee/asko/ruuter.git"
+ :git/tag "v2.0.0"
+ :git/sha "<sha>"}}})
(require '[org.httpkit.server :as http]
'[babashka.deps :as deps]
@@ -103,7 +111,7 @@ You can also use Ruuter with [Babashka](https://github.com/babashka/babashka), b
### Creating routes
-Like mentioned above, each route is a map inside a vector - the order is important only in that the route matcher will return the first result it finds according to `:path`.
+Like mentioned above, each route is a map inside a vector. Routes are matched using **best-match semantics** — the most specific route wins regardless of order.
Each route consists of three items:
@@ -115,7 +123,7 @@ To create parameters from the path, prepend a colon (:) in front of a path slice
##### Required parameters
-A required parameter with a string such as `/hi/:name`, which would match any string that matches the `\/hi\/.*` regex in the URI, in its own slice. The `:name` itself will then be available with its value from the `request` passed to the response function, like this:
+A required parameter with a string such as `/hi/:name`, which would match any string in its own slice. The `:name` itself will then be available with its value from the `request` passed to the response function, like this:
```clojure
(fn [req]
@@ -126,7 +134,7 @@ A required parameter with a string such as `/hi/:name`, which would match any st
##### Optional parameters
-A optional parameter with a string such as `/hi/:name?`, which would match any string that matches the `\/hi\/?.*?` regex in the URI, in its own slice. If there is a `:name` provided in the URI then it will then be available with its value from the `request` passed to the response function, like this:
+An optional parameter with a string such as `/hi/:name?`, which would match any string in its own slice, but is not required to be present. If there is a `:name` provided in the URI then it will then be available with its value from the `request` passed to the response function, like this:
```clojure
(fn [req]
@@ -137,7 +145,7 @@ A optional parameter with a string such as `/hi/:name?`, which would match any s
##### Wildcard parameters
-The above-mentioned `:name` and `:name?` only match in its own path slice, e.g inside a space surrounded by two forward 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.
+The above-mentioned `:name` and `:name?` only match in their own path slice, e.g inside a space surrounded by two forward 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. A wildcard parameter must be the last segment in a path.
#### `:method`
@@ -172,54 +180,72 @@ Or a function returning a map:
What the actual map can contain that you return depends again on the HTTP server you decided to use Ruuter with. The examples I've noted here are based on [http-kit](https://github.com/http-kit/http-kit) & [ring + jetty](https://github.com/ring-clojure/ring), but feel free to make a PR with additions for other HTTP servers.
-## Changelog
+### How It Works
-### 1.3.5
+Under the hood, Ruuter compiles your route definitions into a **segment trie** (prefix tree). Each segment of a path becomes a node in the tree, with branches for literal strings, parameters, optional parameters, and wildcards. This means route matching runs in O(path-depth) time — proportional to the number of segments in the URI, not the number of routes — so performance stays constant whether you have 5 routes or 5,000.
-- Fixes an issue where the usage of wildcard parameters [did not work quite right](https://github.com/askonomm/ruuter/issues/7).
-- Added test cases to cover the fix
+When a request comes in, the trie is walked depth-first, trying all branches at each node in **specificity order** and tracking the best match found so far:
-### 1.3.4
+| Priority | Segment type | Score | Example |
+|----------|-------------|-------|---------|
+| 1st | Literal | +3 | `users` |
+| 2nd | Required param | +2 | `:id` |
+| 3rd | Optional param | +1 | `:id?` |
+| 4th | Wildcard | +0 | `:path*` |
-- Fixes an issue where if used with middlewares (like Ring), or really anything that passes a `:params` key from outside of Ruuter in the request, Ruuter overwrites the `:params` key with its own parametarization. It now does a deep merge instead, with the outside parameters having priority. This means that Ruuter parameters will remain unless overwritten, and should co-exist with outside parameters nicely. [Issue #6](https://github.com/askonomm/ruuter/issues/6).
+The route with the highest total score wins. This means you can define routes in any order and always get the expected behavior:
-- Added and fixed some tests
-
-### 1.3.3
-
-- Removed ClojureScript from dependencies to make the bundle size smaller in case you want to use Ruuter with nbb.
+```clojure
+(def routes [{:path "/api/:resource" :method :get :response ...}
+ {:path "/api/users" :method :get :response ...} ; wins for /api/users
+ {:path "/api/users/:id" :method :get :response ...}
+ {:path "/api/users/me" :method :get :response ...} ; wins for /api/users/me
+ {:path "/:catch*" :method :get :response ...}])
+```
-### 1.3.2
+No regex is involved — matching is done via direct string comparison of path segments against the trie.
-- When using wildcard parameters, the keyword returned in ´:params´ of a request was ´:name*´, but aiming for consistency with an optional parameter where we remove the question mark ´?´, the asterisk has been removed.
+### Pre-Compiling Routes
-### 1.3.1
+When you pass a routes vector to `ruuter/route`, the trie is compiled automatically on first use and cached via `memoize`. For most applications this is all you need.
-- A small bugfix related to wildcard parameters losing the first character in the result.
+If you want explicit control — for instance, to compile once at startup, to avoid the memoization cache, or to inspect the compiled structure — use `compile-routes`:
-### 1.3.0
+```clojure
+(def compiled (ruuter/compile-routes routes))
-- 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.
+;; Pass the pre-compiled trie to route — no compilation step at request time
+(ruuter/route compiled request)
+```
-### 1.2.2
+The return value of `compile-routes` is a map with `:trie` (the segment trie) and `:not-found` (the fallback route, if any), tagged with metadata so `route` can detect it and skip recompilation.
-- Fixed an issue where CLJS compilation would fail because of the `(:gen-class)` that is JVM-only.
+## Development
-- Tests are now runnable for CLJS as well, via `clojure -Atest`.
+Ruuter uses `deps.edn` (Clojure CLI) and `bb.edn` (Babashka) for all development tasks.
-### 1.2.1
+### Running Tests
-- Fixed an issue with regex parsing. Sorry about that.
+```bash
+# JVM (Clojure)
+clojure -M:test
-### 1.2.0
+# ClojureScript (Node.js)
+clojure -M:cljs-test
-- 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.
+# Babashka
+bb test
+```
-- 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.
+### Running Benchmarks
-- 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.
+```bash
+# JVM
+clojure -M:bench
-### 1.1.0
+# ClojureScript
+clojure -M:cljs-bench && node bench-out/bench.js
-- 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!
+# Babashka
+bb bench
+```