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

Date: 2022-01-05 Wed 00:00

Author: 남영환

Created: 2024-01-04 Thu 09:13

Emacs 27.2 (Org mode 9.4.4)

Validate