summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAsko Nõmm <asko@bien.ee>2021-10-02 01:26:07 -0300
committerAsko Nõmm <asko@bien.ee>2021-10-02 01:26:07 -0300
commit8a158dd3cff218223afb9dd77c567023795bfc7e (patch)
tree345cfbfdf430bbcfe684d219e20f2f4ef9035912
Initial commit
-rw-r--r--.gitignore15
-rw-r--r--LICENSE.txt21
-rw-r--r--README.md65
-rw-r--r--project.clj10
-rw-r--r--src/ruuter/core.clj97
-rw-r--r--test/ruuter/core_test.clj3
6 files changed, 211 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..68e52fc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+/target
+/classes
+/checkouts
+profiles.clj
+pom.xml
+pom.xml.asc
+*.jar
+*.class
+/.lein-*
+/.nrepl-port
+/.prepl-port
+.hgignore
+.hg/
+.idea/
+ruuter.iml \ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..0ec86b4
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Asko Nõmm
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..703e0bc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+# 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.
+
+## Usage
+
+### Setting up
+
+Require the namespace `ruuter.core` and then pass your routes to the `route!` function, like this:
+
+```clojure
+(ns myapp.core
+ (:require [ruuter.core :as ruuter]))
+
+(defn -main [& opts]
+ (ruuter/route! [{:path "/"
+ :method :get
+ :response {:status 200
+ :body "Hi there!"}}]))
+```
+
+This will start an HTTP server on a default port of 9600 using [http-kit](https://github.com/http-kit/http-kit) under the hood.
+
+The `route!` function also takes a second, optional argument, which is the [options map for http-kit](http://http-kit.github.io/http-kit/org.httpkit.server.html#var-run-server), allowing you to specify the port and so on, like this:
+
+```clojure
+(def routes [{:path "/"
+ :method :get
+ :response {:status 200
+ :body "Hi there!"}}])
+
+(ruuter/route! routes {:port 8080})
+```
+
+### 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`.
+
+Each route consists of three items:
+
+#### `:path`
+
+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:
+
+```clojure
+(fn [req]
+ (let [name (:name (:params req))]
+ {:status 200
+ :body (str "Hi, " name)}))
+```
+
+#### `:method`
+
+The HTTP method to listen for when matching the given path.
+
+Accepted values are:
+
+- `:get`
+- `:post`
+
+#### `:response`
+
+The response can be a direct map, or a function returning a map. In case of a function, you will also get passed to you the `request` object. For better information on what are all the things you could do with a response, check out [the http-kit documentation](https://http-kit.github.io/server.html).
diff --git a/project.clj b/project.clj
new file mode 100644
index 0000000..36356c3
--- /dev/null
+++ b/project.clj
@@ -0,0 +1,10 @@
+(defproject ruuter "1.0.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.1"]
+ [http-kit "2.5.3"]]
+ :main ruuter.core
+ :min-lein-version "2.0.0"
+ :repl-options {:init-ns ruuter.core})
diff --git a/src/ruuter/core.clj b/src/ruuter/core.clj
new file mode 100644
index 0000000..2504b6f
--- /dev/null
+++ b/src/ruuter/core.clj
@@ -0,0 +1,97 @@
+(ns ruuter.core
+ (:require [clojure.string :as string]
+ [org.httpkit.server :as http]))
+
+
+(def routes [{:path "/"
+ :method :get
+ :response {:status 200
+ :body "Hello, World."}}
+ {:path "/some/page/goes/here"
+ :method :get
+ :response {:status 200
+ :body ":)"}}
+ {:path "/hi/:name"
+ :method :get
+ :response (fn [req]
+ {:status 200
+ :body (str "Hi, " (:name (:params req)))})}
+ {:path :not-found
+ :response {:status 404
+ :body "Not found."}}])
+
+
+(defn- path->regex-path
+ [path]
+ (if (= "/" path)
+ path
+ (->> (string/split path #"/")
+ (map (fn [piece]
+ (if (string/starts-with? piece ":")
+ ".*"
+ piece)))
+ (string/join "/"))))
+
+
+(defn- path+uri->path-params
+ [path uri]
+ (if (= "/" path)
+ {}
+ (let [split-path (string/split path #"/")
+ split-uri (string/split uri #"/")]
+ (into {} (map-indexed
+ (fn [idx item]
+ (when (string/starts-with? item ":")
+ {(keyword (subs item 1)) (get split-uri idx)}))
+ split-path)))))
+
+
+(defn- match-route
+ [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))
+
+
+(defn- route+req->response
+ [{:keys [path response]} {:keys [uri] :as req}]
+ (cond
+ ; responses are maps, so there's no reason they can't be
+ ; direct maps.
+ (map? response)
+ response
+ ; responses can also be functions that return maps, and
+ ; when using a function, you get the whole `req` and params
+ ; with it as well.
+ (fn? response)
+ (response (->> {:params (path+uri->path-params path uri)}
+ (merge req)))
+ ; if by whatever reason we make it here it must mean the
+ ; route is invalid, or doesn't exist, in which case we return
+ ; an error message.
+ :else
+ {:status 404
+ :body "Not found."}))
+
+
+(defn- router
+ [routes {:keys [uri request-method] :as req}]
+ (if-let [route (match-route routes uri request-method)]
+ (route+req->response route req)
+ (route+req->response (->> routes
+ (filter #(= :not-found (:path %)))
+ first) req)))
+
+
+(defn route!
+ ([routes]
+ (route! routes {:port 9600}))
+ ([routes opts]
+ (http/run-server #(router routes %) opts)))
+
+
+(defn -main [& opts]
+ (route! routes)) \ No newline at end of file
diff --git a/test/ruuter/core_test.clj b/test/ruuter/core_test.clj
new file mode 100644
index 0000000..f98d41c
--- /dev/null
+++ b/test/ruuter/core_test.clj
@@ -0,0 +1,3 @@
+(ns ruuter.core-test
+ (:require [clojure.test :refer :all]
+ [ruuter.core :refer :all])) \ No newline at end of file