4

我想获得与我的处理程序相对应的当前路线。这是我的服务器的模型,仅供参考:

type ServerAPI = 
         "route01" :> Get '[HTML] Text
    :<|> "route02" :> "subroute" :> Get '[HTML] Text
    :<|> "route03" :> Get '[HTML] Text

这里有一些处理程序:

route1and2Handler :: Handler Text
route1and2Handler = do
    route <- getCurrentRoute
    addVisitCountForRouteToDatabaseOrSomethingOfThatSort...
    return template

route3Handler :: Handler Text
route3Handler = return "Hello, I'm route 03"

我的服务器:

server :: Server ServerAPI
server = route1and2Handler :<|> route1and2Handler :<|> route3Handler

所以,基本上我route1and2Handler应该有某种方式来获取当前路线。HasServer我已经尝试将请求对象放入我的处理程序并通过实现这样的实例从中提取 url :

data FullRequest

instance HasServer a => HasServer (FullRequest :> a) where
    type Server (FullRequest :> a) = Request -> Server a
    route Proxy subserver request respond =
        route (Proxy :: Proxy a) (subserver request) request respond

[编辑]我刚刚注意到我正在查看旧版本仆人的 api,这不再有效。Newroute有类型签名,route :: Proxy api -> Context context -> Delayed env (Server api) -> Router env我真的看不出Request从这里得到的方法。


而不是使route1and2Handler类型签名成为Request -> Handler Text,但我在尝试创建HasServer实例时遇到了这个错误:

`Server' is not a (visible) associated type of class `HasServer'

最后要指出的是,我的最终目标是从 中获取当前路线,Handler在数据库中为路线添加访问计数仅用于示例目的。我对计算访问次数或类似情况的更好方法不感兴趣。

4

3 回答 3

9

一个问题有两个:

  1. 如何获取当前请求或 URL?
  2. 如何获得当前的“路线”?

请注意,该 URL(例如/route12/42)与路由不同(例如 `"route12" :> Capture "id" Int :> Get '[JSON] Int)。在简短的语言编译指示和导入部分之后,让我们看看如何解决这两个问题。

{-# LANGUAGE ConstraintKinds         #-}
{-# LANGUAGE DataKinds               #-}
{-# LANGUAGE DeriveGeneric           #-}
{-# LANGUAGE FlexibleContexts        #-}
{-# LANGUAGE FlexibleInstances       #-}
{-# LANGUAGE MultiParamTypeClasses   #-}
{-# LANGUAGE OverloadedStrings       #-}
{-# LANGUAGE RankNTypes              #-}
{-# LANGUAGE ScopedTypeVariables     #-}
{-# LANGUAGE TypeFamilies            #-}
{-# LANGUAGE TypeOperators           #-}
{-# LANGUAGE UndecidableInstances    #-}
{-# LANGUAGE UndecidableSuperClasses #-}
{-# OPTIONS_GHC -Wno-orphans         #-}
module Main where

import Data.Maybe             (fromMaybe)
import Control.Monad.IO.Class (liftIO)
import System.Environment     (getArgs)
import GHC.Generics           (to, from, M1 (..), K1 (..), (:*:) (..))

-- for "unsafe" vault key creation
import System.IO.Unsafe (unsafePerformIO)

import qualified Data.ByteString.Char8    as BS8
import qualified Data.Vault.Lazy          as V
import qualified Network.Wai              as Wai
import qualified Network.Wai.Handler.Warp as Warp

import Servant
import Servant.API.Generic 
import Servant.Server.Generic
import Servant.Server.Internal.RoutingApplication (passToServer)

如何获取当前Request对象或 URL

将电流传递WAI Request给处理程序实际上非常容易。这是“懒惰”的方法,我们在请求中要求“一切”,我们必须小心处理程序(例如我们不能 touch requestBody)。此外,这个“组合器”将实现与wai服务器实现联系起来,这是一个实现细节(除了 之外,没有其他任何东西servant-server暴露wai内部Raw)。

这个想法是使Server (Wai.Request :> api) = Wai.Request -> Server api. 如果我们想象一下我们有这样的功能,我们可以编写,使用Servant.API.Generic(参见“使用泛型”食谱):

data Routes1 route = Routes1
    { route11 :: route :- Wai.Request :> "route1" :> Get '[JSON] Int
    , route12 :: route :- Wai.Request :> "route2" :> Capture "id" Int :> Get '[JSON] Int
    }
  deriving (Generic)

routes1 :: Routes1 AsServer
routes1 = Routes1
    { route11 = \req -> liftIO $ do
        let p = Wai.rawPathInfo req
        BS8.putStrLn p
        return (BS8.length p)
    , route12 = \req i -> liftIO $ do
        let p = Wai.rawPathInfo req
        BS8.putStrLn p
        return (succ i)
    }

app1 :: Application
app1 = genericServe routes1

我们定义一个Routes1数据类型,实现Routes1 AsServervalue 并将其转换为wai's Application。然而,为了编译这个例子,我们需要一个额外的实例。我们 . passToServer_route

instance HasServer api ctx => HasServer (Wai.Request :> api) ctx where
    type ServerT (Wai.Request :> api) m = Wai.Request -> ServerT api m

    hoistServerWithContext _ pc nt s =
        hoistServerWithContext (Proxy :: Proxy api) pc nt . s

    route _ ctx d = route (Proxy :: Proxy api) ctx $
        passToServer d id

这个解决方案是很好的快速修复,但可以说有更好的方法。

特定组合子

我们可能会注意到我们的两个处理程序都使用Wai.rawPathInto req调用。这应该提醒我们。具体组合器更优雅。在核心框架之外创建新组合器的能力是servant.

data RawPathInfo

instance HasServer api ctx => HasServer (RawPathInfo :> api) ctx where
    type ServerT (RawPathInfo :> api) m = BS8.ByteString -> ServerT api m

    hoistServerWithContext _ pc nt s =
        hoistServerWithContext (Proxy :: Proxy api) pc nt . s

    route _ ctx d = route (Proxy :: Proxy api) ctx $
        passToServer d Wai.rawPathInfo

使用新的RawPathInfo组合器,我们可以重新实现我们的应用程序:

data Routes2 route = Routes2
    { route21 :: route :- RawPathInfo :> "route1" :> Get '[JSON] Int
    , route22 :: route :- RawPathInfo :> "route2" :> Capture "id" Int :> Get '[JSON] Int
    }
  deriving (Generic)

routes2 :: Routes2 AsServer
routes2 = Routes2
    { route21 = \p -> liftIO $ do
        BS8.putStrLn p
        return (BS8.length p)
    , route22 = \p i -> liftIO $ do
        BS8.putStrLn p
        return (succ i)
    }

app2 :: Application
app2 = genericServe routes2

这个版本的声明性稍强一些,处理程序的限制性更强。我们将rawPathInfo选择器从处理程序移至组合器实现,删除了重复。

使用Vault

中的vaultwai Request不是众所周知或使用的。但在这种情况下,它可能很有用。Vault 在使用 WAI 的 Vault 获得乐趣和利润的博客文章中进行了解释。它填补了强类型的“动态”空白Request:我们可以将任意数据附加到请求中,这在动态类型语言的 Web 框架中很常见。根据servant-serverwai使用 Vault 是问题第一部分的第三个答案。

我们(不安全地)创建保险库的密钥:

rpiKey :: V.Key BS8.ByteString
rpiKey = unsafePerformIO V.newKey

然后我们创建一个中间件,将其rawPathInfo放入vault.

middleware :: Wai.Middleware
middleware app req respond = do
    let vault' = V.insert rpiKey (Wai.rawPathInfo req) (Wai.vault req)
        req' = req { Wai.vault = vault' }
    app req' respond

使用它,我们制作了我们的应用程序的第三个变体。请注意,我们的值可能不在保险库中,这是小的功能回归。

data Routes3 route = Routes3
    { route31 :: route :- Vault :> "route1" :> Get '[JSON] Int
    , route32 :: route :- Vault :> "route2" :> Capture "id" Int :> Get '[JSON] Int
    }
  deriving (Generic)

routes3 :: Routes3 AsServer
routes3 = Routes3
    { route31 = \v -> liftIO $ do
        let p = fromMaybe "?" $ V.lookup rpiKey v
        BS8.putStrLn p
        return (BS8.length p)
    , route32 = \v i -> liftIO $ do
        let p = fromMaybe "?" $ V.lookup rpiKey v
        BS8.putStrLn p
        return (succ i)
    }

app3 :: Application
app3 = middleware $ genericServe routes3

注意:这vault可用于将信息从中间件传递到处理程序以及从处理程序到中间件。例如,身份验证可以完全在中间件中完成,用户信息存储在保险库中供处理程序使用。

如何获取当前路线

问题的第二部分,是如何获取当前路线。有什么,我们可以route2/:id出去吗?请注意,处理程序是匿名的,与功能相同。例如,要编写递归匿名函数,我们可以使用fix组合器。我们可以使用接近它的东西来传递“路由到自身”,使用Servant.API.Generics我们也可以减少样板。

我们从普通的Routes4数据结构开始。

data Routes4 route = Routes4
    { route41 :: route :- "route1" :> Get '[JSON] Int
    , route42 :: route :- "route2" :> Capture "id" Int :> Get '[JSON] Int
    }
  deriving (Generic)

但是Routes4 AsServer,我们将使用不同的模式,而不是创建一个值。 AsRecServer route是一个处理程序,它route :- api作为第一个参数。在本例中,我们使用HasLink',但读者可以自由使用其他自动解释,例如servant-client制作代理!

data AsRecServer route
instance GenericMode (AsRecServer route) where
    type AsRecServer route :- api = (route :- api) -> (AsServer :- api)

routes4 :: Routes4 (AsRecServer (AsLink Link))
routes4 = Routes4
    { route41 = \l -> liftIO $ do
        print l
        return 42
    , route42 = \l i -> liftIO $ do
        print (l i)
        return i
    }

app4 :: Application
app4 = genericRecServe routes4

用法很简单,可惜实现不是。

毛茸茸的

实施genericRecServe令人生畏。缺少的位是一个函数genericHoist。简而言之,给定一个可以转换modeA :- apimodeB :- apifor all的函数apigenericHoist转换routes modeAroutes modeB. 也许这个功能应该存在于Servant.API.Generic

genericHoist
    :: ( GenericMode modeA, GenericMode modeB
       , Generic (routes modeA), Generic (routes modeB)
       , GServantHoist c api modeA modeB (Rep (routes modeA)) (Rep (routes modeB))
       )
    => Proxy modeA -> Proxy modeB -> Proxy c -> Proxy api
    -> (forall api'. c api' => Proxy api' -> (modeA :- api') -> (modeB :- api'))
    -> routes modeA -> routes modeB
genericHoist pa pb pc api nt = to . gservantHoist pa pb pc api nt . from

genericRecServegenericHoist预先组合了 的变体genericServe。单线的执行,给定了一堵墙的约束。

genericRecServe
    :: forall routes.
       ( HasServer (ToServantApi routes) '[]
       , GenericServant routes AsApi
       , GenericServant routes AsServer
       , GenericServant routes (AsRecServer (AsLink Link))
       , Server (ToServantApi routes) ~ ToServant routes AsServer
       , GServantHoist 
          HasLink'
          (ToServantApi routes)
          (AsRecServer (AsLink Link))
          AsServer
          (Rep (routes (AsRecServer (AsLink Link))))
          (Rep (routes AsServer))
       )
    => routes (AsRecServer (AsLink Link)) -> Application
genericRecServe
    = serve (Proxy :: Proxy (ToServantApi routes)) 
    . toServant
    . genericHoist
        (Proxy :: Proxy (AsRecServer (AsLink Link)))
        (Proxy :: Proxy AsServer)
        (Proxy :: Proxy HasLink')
        (genericApi (Proxy :: Proxy routes))
        (\p f -> f $ safeLink p p)

在那里,我们使用单实例类技巧使部分适用HasLink

class (IsElem api api, HasLink api) => HasLink' api
instance (IsElem api api, HasLink api) => HasLink' api

工作马genericHoistgservantHoistRep路线结构上工作的。重要的是要注意capi参数是类参数。这让我们在实例中限制它们。

class GServantHoist c api modeA modeB f g where
    gservantHoist
        :: Proxy modeA -> Proxy modeB -> Proxy c -> Proxy api
        -> (forall api'. c api' => Proxy api' -> (modeA :- api') -> (modeB :- api'))
        -> f x -> g x

M1(元数据) 和(产品) 的实例:*:是直接传递的,这是您所期望的:

instance
    GServantHoist c api modeA modeB f g
    =>
    GServantHoist c api modeA modeB (M1 i j f) (M1 i' j' g)
  where
    gservantHoist pa pb pc api nt
        = M1
        . gservantHoist pa pb pc api nt
        . unM1

instance
    ( GServantHoist c apiA modeA modeB f f'
    , GServantHoist c apiB modeA modeB g g'
    ) =>
    GServantHoist c (apiA :<|> apiB) modeA modeB (f :*: g) (f' :*: g')
  where
    gservantHoist pa pb pc _ nt (f :*: g) =
        gservantHoist pa pb pc (Proxy :: Proxy apiA) nt f 
        :*:
        gservantHoist pa pb pc (Proxy :: Proxy apiB) nt g

叶的实现K1说明了为什么我们需要capi 作为类参数:这里我们需要,和c api“连贯”条件,所以api, modeA,modeB和match。xy

instance
    ( c api, (modeA :- api) ~ x, (modeB :- api) ~ y )
    => GServantHoist c api modeA modeB (K1 i x) (K1 i y)
  where
    gservantHoist _pa _pb _pc api nt
        = K1
        . nt api
        . unK1

结论

使用类似Generic的方法,我们可以对处理程序进行各种转换。例如,我们可以将普通路由封装在servant“中间件”中,将路由信息放入vault中,这些信息可以用于wai Middleware统计。通过这种方式,我们可以制作 的改进版本 servant-ekg,因为目前servant-ekg可能会因重叠路线而感到困惑。

主要用于测试

main :: IO ()
main = do
    args <- getArgs
    case args of
        ("run1":_) -> run app1
        ("run2":_) -> run app2
        ("run3":_) -> run app3
        ("run4":_) -> run app4
        _ -> putStrLn "To run, pass 'run1' argument: cabal new-run cookbook-generic run"
  where
    run app = do
        putStrLn "Starting cookbook-current-route at http://localhost:8000"
        Warp.run 8000 app
于 2019-02-26T17:07:17.310 回答
2

我不知道如何自动执行此操作,但可以使用该safeLink功能“手动”完成。

这个想法是,如果你有一个 API

type ServerAPI = 
        "route01" :> Get '[HTML] Text
   :<|> "route02" :> "subroute" :> Get '[HTML] Text
   :<|> Route3

type Route3 = "route03" :> Get '[HTML] Text

safeLink您可以使用整个 API 和具有特定路由的代理传递给代理,并显示结果URI

show (safeLink (Proxy::Proxy ServerAPI) (Proxy::Proxy Route3))

如果路由有参数,您还必须传递处理程序采用的参数。例如:

type ServerAPI =
       ...
   :<|> Route4

type Route4 = "route04" :> Capture "cap" Int :> Get '[JSON] Text

在 ghci 中:

ghci> :set -XKindSignatures -XDataKinds -XTypeOperators -XTypeFamilies
ghci> :type safeLink (Proxy::Proxy ServerAPI) (Proxy::Proxy Route4)
Int -> URI

您必须为每条路线执行此操作。

于 2017-01-10T10:02:22.047 回答
0

当您向处理程序添加“路由”参数时,您可以在组合服务器的处理程序时通过函数应用程序设置参数值。

根据您的示例:

type ServerAPI = 
         "route01" :> Get '[JSON] Text
    :<|> "route02" :> "subroute" :> Get '[JSON] Text
    :<|> "route03" :> Get '[JSON] Text

route1and2Handler :: String -> Handler Text
route1and2Handler route = do
    -- addVisitCountForRouteToDatabaseOrSomethingOfThatSort...
    return (pack route)

route3Handler :: Handler Text
route3Handler = return "Hello, I'm route 03"

server :: Server ServerAPI
server = route1and2Handler "route01" :<|> route1and2Handler "route02" :<|> route3Handler

或者,如果您真正感兴趣的是对所有路由进行一些通用请求处理,那么通过在服务器和应用程序之间应用“中间件”可能会更好地实现这一点。中间件(通常是 type Application -> Application)可以访问请求。有关 wai 中间件的示例,请参阅wai-extra

为所有请求执行日志记录的示例:

import Network.Wai.Middleware.RequestLogger (logStdoutDev)

...

app :: Application
app = serve serverAPI server

main :: IO ()
main = run 8081 $ logStdoutDev app
于 2019-02-23T22:54:57.760 回答