phel-lang/router

基于 symfony 路由组件的 Phel 路由器

v0.5.0 2024-06-22 05:29 UTC

This package is auto-updated.

Last update: 2024-09-16 18:04:52 UTC


README

Phel 的数据驱动路由器。

安装

composer require phel-lang/router

路由语法

路由定义为向量。第一个元素是路由的路径。此元素后面可以跟一个可选的路线数据映射和一个可选的子路由向量。路由的路径可以包含路径参数。

示例

简单路由

["/ping"]

两个路由

[["/ping"]
 ["/pong"]]

含数据的路由

[["/ping" {:name ::ping}]
 ["/pong" {:name ::pong}]]

含路径参数的路由

[["/users/{user-id}"]
 ["/api/{version}/ping]]

含路径参数验证的路由

["/users/{user-id<\d+>}"]

含通配符参数的路由

["/public/{path<.*>}"]

嵌套路由

["/api"
 ["/admin" {:middware [admin-middleware-fn]}
  ["" {:name ::admin}]
  ["/db" {name ::db}]]
 ["/ping" {:name ::ping}]]

扁平化的相同路由

[["/api/admin" {:middleware [admin-middleware-fn] :name ::admin}]
 ["/api/admin/db" {:middleware [admin-middleware-fn] :name ::db}]
 ["/api/ping" {:name ::ping}]]

路由器

给定一个路由向量,可以创建一个路由器。Phel 路由器提供两种创建路由器的方式。第一种是动态路由器,它在每次新请求时都会进行评估

(ns my-app
  (:require phel\router :as r))

(def router
  (r/router
    ["/api"
     ["/ping" {:name ::ping}]
     ["/user/{id}" {:name ::user}]]))

第二种路由器是编译路由器。这个路由器在编译时(宏)进行评估,因此非常快。缺点是路由不能在请求执行期间动态创建。

(ns my-app
  (:require phel\router :as r))

(def router
  (r/compiled-router
    ["/api"
     ["/ping" {:name ::ping}]
     ["/user/{id}" {:name ::user}]]))

基于路径的路由

要匹配给定路径的路由,可以使用 phel\router/match-by-path 函数。它接受一个路由器和路径作为参数,并返回一个映射或 nil

(r/match-by-path router "/api/user/10")
# Evaluates to
# {:template "/api/user/{id}"
#  :data {:name ::user
#  :path "/api/user/10"
#  :path-params {:id "10"}}}

(r/match-by-path router "/hello") # Evaluates to nil

基于名称的路由

可以使用 phel\router/match-by-name 函数通过名称匹配所有具有 :name 路由数据的路由。它接受一个路由器和路由名称作为参数,并返回一个映射或 nil

(r/match-by-name router ::ping)
# Evaluates to
# {:template "/api/ping"
#  :data {:name ::ping}}

(r/match-by-name router ::foo) # Evaluates to nil

生成路径

还可以为给定路由及其路径参数生成路径。函数 phel\router/generate 接受一个路由器、路由名称和路由参数映射。它返回生成路径作为字符串,或者在找不到路由或缺少路径参数时抛出异常。

(r/generate router ::ping {}) # Evaluates to "/api/ping"

(r/generate router ::user {:id 10}) # Evaluates to "/api/user/10"

(r/generate router ::user {:id 10 :foo "bar"}) # Evaluates to "/api/user/10?foo=bar"

(r/generate router ::user {}) # Throws Symfony\Component\Routing\Exception\MissingMandatoryParametersException

(r/generate router ::foo {}) # Throws Symfony\Component\Routing\Exception\RouteNotFoundException

处理器

每个路由都可以有一个处理器函数,当路由与当前路径匹配时执行。处理器函数是一个接受 phel\http/request 作为参数并返回 phel\http/response 的函数。

# request -> response
(fn [request]
  (h/response-from-map {:status 200 :body "ok"}))

处理器可以放置在路由数据的顶层,使用 :handler 关键字,或者放置在特定方法(:get:head:patch:delete:options:post:put:trace)下。如果找不到基于请求方法的处理器,则使用顶层处理器。

[["/all" {:handler handler-fn}]
 ["/ping" {:name ::ping
           :get {:handler handler-fn}
           :post {:handler handler-fn}}]]

为了处理请求,必须在 phel\router/handler 方法中包装路由器。此方法返回一个接受 phel\http/request 并返回 phel\http/response 的函数。

(ns my-app
  (:require phel\router :as r)
  (:require phel\http :as h))

(defn handler [req]
  (h/response-from-map {:status 200 :body "ok"}))

(def app
  (r/handler
    (r/router
      [["/all" {:handler handler}]
       ["/ping" {:name ::ping
                 :get {:handler handler}
                 :post {:handler handler}}]])))

(app (h/request-from-map {:method "DELETE" :uri "/all"}))
# Evaluates to (h/response-from-map  {:status 200 :body "ok"})

(app (h/request-from-map {:method "GET" :uri "/ping"}))
# Evaluates to (h/response-from-map {:status 200 :body "ok"})

(app (h/request-from-map {:method "PUT" :uri "/ping"}))
# Evaluates to (h/response-from-map {:status 404 :body "Not found"})

中间件

每个路由器可以有多个中间件函数。中间件函数是一个接受处理器函数和 phel\http/request 并返回 phel\http/response 的函数。

# handler -> request -> response
(fn [handler request] (handler request))

中间件可以放置在路由数据的顶层,使用 :middleware 关键字,或者放置在特定方法(:get:head:patch:delete:options:post:put:trace)下。

(ns my-app
  (:require phel\router :as r)
  (:require phel\http :as h))

(defn handler [req]
  (h/response-from-map
    {:status 200
     :body (push (get-in req [:attributes :my-middleware]) :handler)}))

(defn my-middleware [name]
  (fn [handler request]
    (handler
      (update-in
        request
        [:attributes :my-middleware]
        (fn [x]
          (if (nil? x)
            [name]
            (push x name)))))))

(def app
  (r/handler
    (r/router
      ["/api" {:middleware [(my-middleware :api)]}
       ["/ping" {:handler handler}]
       ["/admin" {:middleware [(my-middleware :admin)]}
        ["/db" {:middleware [(my-middleware :db)]
                :delete {:middleware [(my-middleware :delete)]
                         :handler handler}}]]])))

(app (h/request-from-map {:method "DELETE" :uri "/api/ping"}))
# Evaluates to (h/response-from-map {:status 200 :body [:api :handler]})

(app (h/request-from-map {:method "DELETE" :uri "/api/admin/db"}))
# Evaluates to (h/response-from-map {:status 200 :body [:api :admin :db :delete :handler]})