You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为Servant的NamedRoutes实现针对返回列表端点添加查询参数的类型族实例

Servant的NamedRoutes实现针对返回列表端点添加查询参数的类型族实例

嗨,我来帮你搞定Servant里用NamedRoutes时,给返回列表的端点自动加limit查询参数的问题!

首先,先回顾下你之前针对传统Servant API写的类型族AddLimit——它能识别返回列表的Get端点,自动给这类端点加上QueryParam "limit" Int,对用:<|>:组合的API很管用,但碰到NamedRoutes就歇菜了,这是因为NamedRoutes用的是Haskell记录类型来封装路由,和传统的API组合方式完全不一样。

问题核心

你的传统类型族只处理了:<|>:、路径段、Get这些Servant原生的API组合子,但NamedRoutes的结构是:

  1. 用一个Haskell记录类型(比如MyNamedAPI)来定义所有路由
  2. 每个记录字段对应一个路由,类型是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] Int
  • bar对应的路由会自动加上limit查询参数:mode :- "bar" :> QueryParam "limit" Int :> Get '[JSON] [Int]

为什么这样能行

  1. AddLimit新增的mode :- route实例,会递归处理每个路由内部的定义,把返回列表的Get端点加上查询参数
  2. 基于Generic的AddLimitNamedRoutes类型类,会自动遍历NamedRoutes记录的每个字段,对每个字段里的mode :- route都应用AddLimit处理
  3. 所有的处理都是在类型层面完成的,完全符合Servant的类型驱动设计思路

备注:内容来源于stack exchange,提问作者l7r7

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 14:18:09