1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
(ns ruuter.core
(:require [clojure.string :as string]
[org.httpkit.server :as http])
(:gen-class))
(defn- path->regex-path
"Takes in a raw route `path` and turns it into a regex pattern to
match against the request URI."
[path]
(if (= "/" path)
path
(->> (string/split path #"/")
(map (fn [piece]
(if (string/starts-with? piece ":")
".*"
piece)))
(string/join "/"))))
(defn- path+uri->path-params
"Takes a raw route `path` and the actual request `uri`, which it then
turns into a map of k:v, if any parameters were used in the `path`."
[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
"For a collection of `route`, will attempt to find one that matches
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))
(defn- route+req->response
"Given the current route and the current HTTP request, it will
attempt to return a response, either directly if it's a map or
indirectly if it's a function. In case of a function, it will also
pass along the request map with added-in params that were parsed
from the route path.
If the response is invalid, or does not exist, a error message with
status code 404 will be returned instead."
[{: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
"For a given collection of `routes` and the current HTTP request as
`req`, will attempt to match a route with the HTTP request, which it
will then try to return a response for.
If no route matched for a given HTTP request it will try to find a
route with `:not-found` as its `:path` instead, and return the response
for that."
[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!
"Starts an HTTP server which will then try to find a matching route for
each request from within the given collection of `routes`. Takes an
optional `opts` map, which corresponds directly to http-kit's config,
allowing you to specify things like `{:port 8080}` and so on."
([routes]
(route! routes {:port 9600}))
([routes opts]
(println "Starting HTTP server on port " (:port opts))
(http/run-server #(router routes %) opts)))
|