为Servant的NamedRoutes实现针对返回列表端点添加查询参数的类型族实例
Servant的NamedRoutes实现针对返回列表端点添加查询参数的类型族实例
嗨,我来帮你搞定Servant里用NamedRoutes时,给返回列表的端点自动加limit查询参数的问题!
首先,先回顾下你之前针对传统Servant API写的类型族AddLimit——它能识别返回列表的Get端点,自动给这类端点加上QueryParam "limit" Int,对用:<|>:组合的API很管用,但碰到NamedRoutes就歇菜了,这是因为NamedRoutes用的是Haskell记录类型来封装路由,和传统的API组合方式完全不一样。
问题核心
你的传统类型族只处理了:<|>:、路径段、Get这些Servant原生的API组合子,但NamedRoutes的结构是:
- 用一个Haskell记录类型(比如
MyNamedAPI)来定义所有路由 - 每个记录字段对应一个路由,类型是
mode :- route(mode是NamedRoutes需要的标记类型,route是实际的API路径+端点定义)
所以我们需要给AddLimit类型族加上针对这两种新结构的处理逻辑,还要能递归遍历记录的每个字段。
解决方案代码
首先,我们先扩展原来的AddLimit类型族,加上对mode :- route结构的支持:
{-# LANGUAGE TypeFamilies, FlexibleInstances, UndecidableInstances, DeriveGeneric #-} import GHC.Generics import Servant.API import Servant.API.NamedRoutes -- 你原来的AddLimit类型族,先保留并扩展 type family AddLimit a :: Type where AddLimit ((sym :: Symbol) :> rest) = sym :> AddLimit rest AddLimit (first :> rest) = first :> AddLimit rest AddLimit (left :<|>: right) = AddLimit left :<|>: AddLimit right AddLimit (Get contentTypes [res]) = QueryParam "limit" Int :> Get contentTypes [res] AddLimit (Get contentTypes res) = Get contentTypes res -- 新增:处理NamedRoutes里的 mode :- route 结构 AddLimit (mode :- route) = mode :- AddLimit route -- 其他需要的实例可以继续补充,比如Post、Put等返回列表的情况 -- 然后定义辅助类型类,用来遍历NamedRoutes的记录类型 class AddLimitNamedRoutes api where type AddLimitNamed api :: Type instance (Generic api, GAddLimitNamedRoutes (Rep api)) => AddLimitNamedRoutes api where type AddLimitNamed api = To (GAddLimitNamedRoutes (Rep api)) -- 辅助类型类,处理Generic的表示 class GAddLimitNamedRoutes f where type GAddLimitNamedRoutes f :: Type -> Type -- 处理记录的元数据(比如类型名) instance GAddLimitNamedRoutes f => GAddLimitNamedRoutes (D1 c f) where type GAddLimitNamedRoutes (D1 c f) = D1 c (GAddLimitNamedRoutes f) -- 处理记录的每个字段的元数据(比如字段名) instance (Selector s, GAddLimitNamedRoutes f) => GAddLimitNamedRoutes (S1 s f) where type GAddLimitNamedRoutes (S1 s f) = S1 s (GAddLimitNamedRoutes f) -- 处理字段里的 mode :- route 结构,调用AddLimit处理内部的route instance AddLimit route => GAddLimitNamedRoutes (K1 i (mode :- route)) where type GAddLimitNamedRoutes (K1 i (mode :- route)) = K1 i (mode :- AddLimit route) -- 处理多个字段的组合(记录里的字段是用:*:组合的) instance (GAddLimitNamedRoutes f, GAddLimitNamedRoutes g) => GAddLimitNamedRoutes (f :*: g) where type GAddLimitNamedRoutes (f :*: g) = GAddLimitNamedRoutes f :*: GAddLimitNamedRoutes g
怎么用这个方案
假设你的NamedRoutes定义是这样的:
data MyNamedAPI mode = MyNamedAPI { foo :: mode :- "foo" :> Get '[JSON] Int , bar :: mode :- "bar" :> Get '[JSON] [Int] } deriving (Generic) -- 必须派生Generic,这样才能遍历字段
现在你只需要用AddLimitNamed (MyNamedAPI mode)就能得到处理后的API类型:
foo对应的路由保持不变:mode :- "foo" :> Get '[JSON] Intbar对应的路由会自动加上limit查询参数:mode :- "bar" :> QueryParam "limit" Int :> Get '[JSON] [Int]
为什么这样能行
AddLimit新增的mode :- route实例,会递归处理每个路由内部的定义,把返回列表的Get端点加上查询参数- 基于
Generic的AddLimitNamedRoutes类型类,会自动遍历NamedRoutes记录的每个字段,对每个字段里的mode :- route都应用AddLimit处理 - 所有的处理都是在类型层面完成的,完全符合Servant的类型驱动设计思路
备注:内容来源于stack exchange,提问作者l7r7
相关产品推荐
相关产品推荐

