仆人客户端分页

问题描述 投票:0回答:1

给出以下 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 中就好了。

我发现了一些现有技术并有一些想法,但没有什么能让我更接近我的目标:

  • 我找到的最接近的匹配是在 servant-github 包中,它基本上完全符合我的要求,但它已经有一段时间没有更新了,并且与最新版本的servant不兼容。我还没有找到一种方法将该功能迁移到最新的仆人客户端
  • servant 存储库中有 this issues,它讨论分页,但没有讨论相应的客户端功能。它还提到了 servant-pagination 包,它也不涵盖客户端功能并使用不同的分页方法。
  • servant-client支持客户端中间件,这使得拦截请求成为可能,但它只提供对原始响应的访问,因此没有机会知道如何反序列化和累积响应
  • 我认为 hoistClient 也不起作用,因为不可能为非分页情况与分页情况实现不同的行为。我可能在这里遗漏了一些东西,但据我所知,不可能根据 monad 内的
    a
    来改变行为。
haskell servant
1个回答
0
投票

除了通常的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}
© www.soinside.com 2019 - 2024. All rights reserved.