我想获得与我的处理程序相对应的当前路线。这是我的服务器的模型仅供参考:
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旧版本的仆人,这已经无效了。新route
的类型签名为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
中获取当前路线,在数据库中添加路线访问次数只是为了举例。我对计算访问次数或类似事件的方式不感兴趣。
答案 0 :(得分:2)
一个问题有两个:
请注意,URL(例如/route12/42
)与路由不同
(例如`“” route12“:>捕获” id“ Int:>获取'[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
传递给处理程序实际上很容易。
这是一种“懒惰”的方法,我们在请求中要求“一切”,
并且我们必须小心处理程序(例如,我们无法触摸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 AsServer
值并将其打开
放入wai
的{{1}}中。但是,要编译此示例,我们需要
其他实例。我们在内部 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
vault
中的wai
值不是众所周知的或未使用的。
但是在这种情况下,它可能会很有用。
Using WAI's vault for fun and profit博客文章中介绍了保险柜。
它填补了强类型Request
的“动态”空白:我们可以将任意数据附加到请求,
就像在Web框架中使用动态类型语言一样。
由于Request
基于servant-server
,因此使用保管库是第三个答案
到问题的第一部分。
我们(不安全地)创建金库的密钥:
wai
然后我们创建一个中间件,它将把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中
处理程序使用。
问题的第二部分是如何获取当前路线。
可以,我们可以解决vault
吗?
请注意,处理程序是 anonymous ,在相同意义上,函数也是。
例如。要编写递归匿名函数,我们可以使用route2/:id
组合器。
我们可以使用接近的东西来传递“路线到自身”,
使用fix
也可以减少样板。
我们从外观普通的Servant.API.Generics
数据结构开始。
Routes4
但是,我们将使用不同的 mode 而不是设置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
的{{1}}转换为modeA :- api
的函数,
modeB :- api
将api
转换为genericHoist
。
也许此功能应该存在于routes modeA
中?
routes 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
组成,并包含genericRecServe
的变体。
在限制条件下,单线实施。
genericHoist
我们使用单实例类技巧来部分适用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
genericHoist
上的路线结构。
请务必注意,gservantHoist
和Rep
参数是类参数。
这让我们在实例中约束它们。
c
api
(元数据)和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
的原因
作为类参数:在这里,我们需要c
和“连贯”条件,
因此api
,c api
,api
,modeA
和modeB
匹配。
x
使用类似的y
方法,我们可以对处理程序进行各种转换。
例如,我们可以将普通路由包装在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
答案 1 :(得分:1)
我不知道如何自动执行此操作,但可以使用safeLink
功能“手动”完成。
这个想法是,如果你有一个API
type ServerAPI =
"route01" :> Get '[HTML] Text
:<|> "route02" :> "subroute" :> Get '[HTML] Text
:<|> Route3
type Route3 = "route03" :> Get '[HTML] Text
您可以使用整个API和具有特定路线的代理传递给safeLink
代理,并显示生成的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
您必须为每条路线执行此操作。
答案 2 :(得分:0)
在向处理程序添加“ route”参数时,可以在合并服务器的处理程序时通过函数应用程序设置参数值。
根据您的示例:
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
或者,如果您真正感兴趣的是对所有路由进行通用请求处理,则最好通过在服务器和应用程序之间应用“中间件”来实现。中间件(通常为Application -> Application
类型)可以访问该请求。有关wai中间件的示例,请参见wai-extra。
对所有请求执行日志记录的示例:
import Network.Wai.Middleware.RequestLogger (logStdoutDev)
...
app :: Application
app = serve serverAPI server
main :: IO ()
main = run 8081 $ logStdoutDev app