[220105] reitit에 대해 알아보자
Table of Contents
1 reitit 소개
Clojure(Script)를 위한 라우터를 제공한다. Reitit에서 특이한 점은 Router를 데이터기반으로 만들어준다는 것이다. 아래처럼 vector자료구조를 받아서 router를 만들어준다.
(require '[reitit.core :as r])
(def router
(r/router
[["/api/ping" ::ping]
["/api/orders/:id" ::order]]))
(r/match-by-path router "/api/ping")
; #Match {:template "/api/ping"
; :data {:name ::ping}
; :result nil
; :path-params {}
; :path "/api/ping"}
(r/match-by-name router ::order {:id 2})
; #Match {:template "/api/orders/:id"
; :data {:name ::order}
; :result nil
; :path-params {:id 2}
; :path "/api/orders/2"}
Router 라이브러리가 바뀌어도 데이터는 그대로 일 것이다. 우리에게 중요한 것은 라우팅을 위한 정보이지, 라우팅정보를 등록하는 함수를 아는 것은 아니라고 하는 것 같다.
2 reitit 코드 실행하기
github에서 받아보면 여러 모듈로 나뉘어져 있음을 알 수 있다. 우리는 맨 위에 프로젝트를 가서 dev 프로파일과 함께 repl을 실행할 것이다.
이맥스에서는 C-u C-c M-j RET with-profile dev 이 형태로 연결하면 dev 프로파일이 추가된다.
3 router 함수
내가 사용한 라우팅정보를 보고 어떤 router 구현체를 사용할지 검증한다. 아래와 같은 router가 보인다.
- single-static-path-router
- quarantine-router
- lookup-router
- trie-router
- mixed-router
3.1 impl/resolve-routes
router를 사용하기 전에 인자로 넣은 route데이터를 resolve 한다.
(impl/resolve-routes raw-routes opts) 이런식으로 수행된다. 여기서 raw-routes 는 아래에 코드로 설명된다.
(defn my-handler [req] "Helo World")
(let [raw-routes ["" ["/ping" ["/pong" my-handler]]]
r (router raw-routes)]
(println r))
;; =>
;; 프린트된 것.
;; {:router [[/ping/pong {:handler #function[my-handler]}]]}
;; 리턴된 것.
;; #object[reitit.core$single_static_path_router$reify__11563 0x000000
;; reitit.core$single_static_path_router$reify__11563@0x00000]
3.2 default-router-options
(defn default-router-options []
{:lookup (fn lookup [[_ {:keys [name]}] _] (if name #{name}))
:expand expand
:coerce (fn coerce [route _] route)
:compile (fn compile [[_ {:key [handler]} _]] handler)
:exception exception/exception
:conflicts (fn throw! [conflicts]
(exception/fail! :path-conflicts conflicts))})
3.2.1 lookup
lookup 함수는 name 이 있으면 #{name} 을 리턴한다.
3.2.2 expnad
expand는 프로토콜로 정의되어 있다.
(defprotocol Expand
(expand [this opts]))
(extend-protocol Expand
clojure.lang.Keyword
(expand [this _] {:name this})
clojure.lang.PersistentArrayMap
(expand [this _] this)
clojure.lang.PersistentHashMap
(expand [this _] this)
clojure.lang.Fn
(expand [this _] {:handler this}))
3.2.3 coerce
아래를 보니 딱히 하는 일은 (아직) 없다.
(fn coerce [route _] route)
3.2.4 exception
reitit에서 따로 사용하는 exception 이 있다.
(defn get-message [e]
(.getMessage ^Exception e))
(defn exception [e]
(let [data (ex-data e)
message (format-exception (:type data)
(get-message e)
(:data data))]
(ex-info message (assoc (or data {}) ::cause e))))
위에 format-exception 이 있다. 각 타입(:type 키 안의 값에 따라) 포맷팅하는 법이 다르다.
(defmulti format-exception (fn [type _ _] type)) (defmethod format-exception :default [_ message data] (str message (if data (str "\n\n" (pr-str data))))) ;;; etc
이렇게 구현별로 나뉠 수 있다.
3.2.5 fail!
실패 했을 때 예외를 던지는 함수다.
(defn fail!
([type] (fail? type nil))
([type data]
(throw (ex-info (str type) {:type type :data data})))
실패하면 {:type ,,, :data ,,,} 형태를 리턴한다.
이렇게 만들어진 default-router-options 을 impl/resolve-routes 는 어떻게 사용하고 을까
3.3 impl/resolve-routes 내부구현
(defn resolve-routes [raw-routes {:keys [coerce] :as opts}]
(cond->> (->> (walk raw-routes opts)
(map-data merge-data))
coerce (into [] (keep #(coerce % opts)))))
사용해보자.
(let [handler (fn hello-world [_] "Hello World")
raw-routes ["" ["/ping" ["/pong" handler]]]
opts (default-router-options)]
(resolve-routes raw-routes opts))
;=>
; [["/ping/pong" {:handler #function[hello-world]}]]
제대로 동작하는 것 같다. "" "/ping" "/pong" 은 하나로 합쳐졌고, handler함수는 {:handler ,,,} 형태로 만들어졌다.
여러개의 엔드포인트를 넣으면 어떻게 될까?
(let [h1 (fn h1 [_] h1)
h2 (fn h2 [_] h2)
raw-routes ["" ["/v1" ["/h1" h1]
["/h2" h2]]]
opts (default-router-opts)]
(impl/resolve-routes raw-routes opts))
;=>
[["/v1/h1" {:handler #fn[h1]}]
["/v1/h2" {:handler #fn[h2]}]]
어떻게 패스들을 합치는 것일까? 내부구현을 구경하자.
3.3.1 walk (path와 함수를 매핑하기 위한 자료구조 생성)
(impl/walk raw-routes opts) => (["/v1/h1" [[:handler #fn/h1]]]] ["/v1/h2" [[:handler #h2/n2]]]])
이 walk 가 중요한 것 같다.
(defn walk [raw-routes {:keys [path data routes expand]
:or {data [], routes []}
:as opts}]
(letfn
[(walk-many [p m r]
(reduce #(into %1 (walk-one p m %2)) [] r))
(walk-one [pacc macc routes]
(if (vector? (first routes))
(walk-many pacc macc routes)
(when (string? (first routes))
(let [[path & [maybe-arg :as args]] routes
[data childs] (if (or (vector? maybe-arg)
(and (sequential? maybe-arg)
(sequential? (first maybe-arg)))
(nil? maybe-arg))
[{} args]
[maybe-arg (rest args)])
macc (into macc (expand data opts))
child-routes (walk-many (str pacc path) macc (keep identity childs))]
(if (seq childs) (seq child-routes) [[(str pacc path) macc]])))))]
(walk-one path (mapv identity data) raw-routes)))
여기서 처음 시작은
(walk-one path (mapv identity data) raw-routes) ;; path nil ;; mapv 리스트를 벡터로 변환 ;; raw-routes ["" ["/v1" [["/h1" h1] ;; ["/h2" h2]]]
벡터의 첫번째 값은 "" 이다. 그러므로 첫 if문을 실패한다.
(if (vector? (first routes)))
이제 when 으로 넘어갈 것이다.