diff options
| -rw-r--r-- | README.md | 38 | ||||
| -rw-r--r-- | project.clj | 2 | ||||
| -rw-r--r-- | src/ruuter/core.cljc (renamed from src/ruuter/core.clj) | 52 | ||||
| -rw-r--r-- | test/ruuter/core_test.clj | 24 |
4 files changed, 87 insertions, 29 deletions
@@ -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."}} |
