给出以下 Servant API 定义:
type API =
"single-content" :> Get '[JSON] Int
:<|> "contents" :> QueryParam "page" Int :> Get '[JSON] (Headers '[Header "Link" String] [Int])
第二个端点是分页的,并且如果有更多元素,则在响应中包含
next
链接标头。
我可以使用servant-client生成客户端函数:
paginatedClient :: Maybe Int -> ClientM (Headers '[Header "Link" String] [Int])
singleClient :: ClientM Int
singleClient :<|> paginatedClient = client (Proxy :: Proxy API)
我正在寻找一种方法来扩展分页端点的客户端功能,以便它自动从响应标头中获取链接,调用下一页并累积结果。
理想情况下,与默认客户端相比,类型签名不会改变。如果请求存在于与
ClientM
不同的 monad 中就好了。
我发现了一些现有技术并有一些想法,但没有什么能让我更接近我的目标:
a
来改变行为。除了通常的Servant包和导入之外,这个答案还取决于http-client。
该函数采用 URL 字符串和 Servant 客户端操作,并使用 URL 参数覆盖该操作执行的所有 HTTP 请求的路径(和查询字符串)。
import Network.HTTP.Client.Internal qualified as Http
overrideUrl :: String -> ClientM a -> ClientM a
overrideUrl url action = do
request <- Http.parseRequest url
let transformClientRequest original =
original { Http.path = request.path, Http.queryString = request.queryString }
transformMakeClientRequest f baseUrl servantReq = do
httpReq <- f baseUrl servantReq
pure $ transformClientRequest httpReq
transformClientEnv clientEnv =
clientEnv {
makeClientRequest =
transformMakeClientRequest clientEnv.makeClientRequest
}
local transformClientEnv action
这是一个采用 Servant 客户端操作的函数,该操作返回一个幺半群值以及“下一页”链接,并在跟踪链接时收集结果:
paginated ::
forall (s :: Symbol) rest a . Monoid a =>
ClientM (Headers (Header s String ': rest) a) ->
ClientM (Headers (Header s String ': rest) a)
paginated initial = do
let go action acc = do
r <- action
let acc' = acc <> getResponse r
HCons header _ = getHeadersHList r
case header of
UndecodableHeader {} -> liftIO $ throwIO $ userError "undecodable header"
MissingHeader -> pure $ r { getResponse = acc' }
Header next -> go (overrideUrl next initial) acc'
go initial mempty
paginated
利用 overrideUrl
每次转到不同的链接,同时保持相同的请求标头和其他配置。
现在的问题是如何将
paginated
装饰器应用到你的客户端。它不是在类型级别完成的。相反,您必须获取 API 客户端值,进入要分页的特定客户端函数,并使用装饰器对其进行转换以获得新的 API 客户端。
如果使用
NamedRoutes
,装饰 API 客户端值会容易得多,因为客户端函数将成为记录中的名称字段,而不是位置结构中的匿名槽。
命名路由的示例:
data Foo mode = Foo {
firstContent ::
mode
:- "contents"
:> Get '[JSON] (Headers '[Header "Link" String] [Int]),
extraContent ::
mode
:- "contents-extra"
:> Capture "page" Int :> Get '[JSON] (Headers '[Header "Link" String] [Int])
} deriving stock (Generic)
fooClient :: Client ClientM PaginatedApi
fooClient = client (Proxy @PaginatedApi)
fooClientDecorated :: Client ClientM PaginatedApi
fooClientDecorated = fooClient { firstContent = paginated fooClient.firstContent}