如何通过浏览器正确减少 GET 请求的仆人 API 路径 (:>) 组合子树?

How to properly reduce servant API path (:>) combinator trees for GET request via browser?

Haskell Servant docs 提供了几个示例,用于编写 API 来提供具有类型级 DSL 的某些内容,例如

type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position

我想做的是编写一个类似的 API, 可以 将多个输入作为 GET 请求,也可以在浏览器中使用。 (以下代码经过删节和简化)。

type QuestionAPI = "question"
                 :> QueryParam "question1" Question
                 :> QueryParam "question2" Question
                 :> QueryParam "question3" Question
                 ...
                 ...
                 :> QueryParam "questionn" Question
                 :> Get '[JSON] [Answer]

这工作得很好,但是使用这个端点的函数接收 n 个参数

processQuestionAPI :: Maybe Question -> Maybe Question -> ... -> Handler [Answer]        
processQuestionAPI param1 param2 param3 ... paramN = ...

这让一切都更难阅读和推理。

我想到的第一个解决方法是使用记录!

data LotsOfQuestions = LotsOfQuestions
                { question1 :: Maybe Question
                , question2 :: Maybe Question
                , question3 :: Maybe Question
                ...
                ...
                , questionn :: Maybe Question
                }

并像这样重写端点

type QuestionAPI = "question"
                 :> ReqBody '[FromUrlEncoded] LotsOfQuestions
                 :> Get '[JSON] [Answer]

编译这段代码时 GHC 抛出了这个错误

• No instance for (Web.Internal.FormUrlEncoded.FromForm
                     LotsOfQuestion)
    arising from a use of ‘serve’

所以我只是为 LotsOfQuestions.

编写了一个自定义 FromForm 实例
instance FromForm LotsOfQuestion where
    fromForm aForm = LotsOfQuestions 
                   <$> parseMaybe "q1" aForm
                   <*> parseMaybe "q2" aForm 
                   ...
                   <*> parseMaybe "qN" aForm 

一切都已编译,服务器已启动,运行但我无法使用浏览器连接到它。

我用的URL是这个

localhost:8081/questions?q1=what&q2=where&q3=when

奇怪的是 cURL 确实有效!

这个

curl -d "q1=what&q2=where&q3=when" -X GET "localhost:8081/questions"

生产出我想要的。

环顾四周,我发现了这个 this issue,这让我相信用 GET 请求发送请求主体不是推荐的做事方式。

所以我必须用 GET 请求的等效内容替换 ReqBody,但我不确定那可能是什么。

这更像是一份进度报告,而不是最终答案。

这个主要问题是像这样的端点

type QuestionAPI = "question"
                 :> QueryParam "question1" Question
                 :> QueryParam "question2" Question
                 :> QueryParam "question3" Question
                 ...
                 ...
                 :> QueryParam "questionn" Question
                 :> Get '[JSON] [Answer]

确实有效,但使用它们的函数通常不那么容易使用,例如

processQuestionAPI :: Maybe Question -> Maybe Question -> ... -> Handler [Answer]        
processQuestionAPI param1 param2 param3 ... paramN = ...

我的解决方案是使用记录语法

data LotsOfQuestions = LotsOfQuestions
                { question1 :: Maybe Question
                , question2 :: Maybe Question
                , question3 :: Maybe Question
                ...
                ...
                , questionn :: Maybe Question
                }

但我不知道如何将该记录映射到服务 DSL。

Mark 的评论给了我一些启发。

我需要做的是实施 FromHttpApiData class,特别是 parseQueryParam

因为其中一些问题是可选的,所以实施有些迂回。

instance FromHttpApiData LotsOfQuestions where
  parseQueryParam = parseQuestions

tailMaybe :: [a] -> Maybe [a]
tailMaybe []  = Nothing
tailMaybe str = Just $ tail str

splitOnEqual :: String -> Maybe (String, Maybe String)
splitOnEqual xs = second tailMaybe . flip splitAt xs <$> elemIndex '=' xs

parseQuestions :: Text -> Either Text LotsOfQuestions
parseQuestions txt =
  LotsOfQuestions
    <$> sequence (fmap fromOrder =<< lookup "q1" txtMap)
    <*> sequence (fmap fromOrder =<< lookup "q2" txtMap)
    <*> sequence (fmap fromOrder =<< lookup "q3" txtMap)
    ...
    ...
    <*> sequence (fmap fromOrder =<< lookup "qN" txtMap)

  where txtMap = mapMaybe splitOnEqual (splitOn "&" $ unpack txt)

这里 fromOrder 是类型为 Text -> Either Text Question 的内部函数,而 splitOn 来自 Data.List.Split.

这些是我对 QuestionAPI

所做的更改
type QuestionAPI = "questions"
           :> QueryParam "are" LotsOfQuestions
           :> Get '[ JSON] [Answer]

与 API 交互的方式是通过这样的 link

http://localhost:8081/questions?are=q1=what&q2=where&q3=when