diff options
Diffstat (limited to 'README.md')
| -rw-r--r-- | README.md | 98 |
1 files changed, 62 insertions, 36 deletions
@@ -4,7 +4,13 @@ A tiny, zero dependency, system-agnostic router for Clojure, ClojureScript, Baba ## Installation -[](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 +``` |
