[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
으로 넘어갈 것이다.