summaryrefslogtreecommitdiff
path: root/src/ruuter/core.clj
blob: b441b1f8a4c00c4742a804b968f9359f8d7c7e98 (plain)
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)))