如何处理不完整的 JSON/Record 类型(IE 缺少我稍后会填写的必填字段)?

How to deal with incomplete JSON/Record types (IE missing required fields which I'll later fill in)?

编辑:对于那些有类似疾病的人,我发现这与 "Extensible Records Problem" 有关,我将亲自研究更多。


EDIT2:我已经开始解决这个问题(几周后),方法是非常明确地说明数据类型,并且每个语义数据单元具有多种数据类型。例如,如果数据库包含一个 X,我的代码有一个 XAction 用于表示我想用 X 做的事情,而 XResponse 用于中继 Xs 到 http 客户端。然后我需要构建用于在实例之间穿梭的支持代码。不理想,但是,我喜欢它的明确性,希望当我的模型具体化时,它真的不需要太多维护,而且应该非常可靠。


我不确定解决这个问题的正确抽象级别是什么(即记录?还是 Yesod?)所以我只列出简单的案例。

简单案例/TL;DR

我想将请求 body 解码为类型

data Comment = Comment {userid :: ..., comment :: ...}

但实际上我不希望请求 body 包含 userid,服务器将根据他们的 Auth Headers 提供它,(或者我想获取数据的任何地方默认填写字段)。

所以他们实际上传递给我的是:

data SimpleComment = SimpleComment {comment :: ...} deriving (Generic, FromJSON)

我把它变成 Comment。但是同时维护这两种 nearly-identical 类型很麻烦,而不是 DRY。

如何解决这个问题?


问题详情

我有一个记录类型:

data Comment = Comment {userid :: ..., comment :: ...}

我有一条POST路线:

postCommentR :: Handler Value
postCommentR = do
  c <- requireJsonBody :: (Handler Comment)
  insertedComment <- runDB ...
  returnJson insertedComment

请注意,路由要求用户提供他们的 userid(在 Comment 类型中,这至少是多余的,因为他们的 ID 与他们的身份验证相关联 headers。在最糟糕的是,这意味着我需要检查用户是在添加他们自己的 ID,还是丢弃了他们提供的 ID,在这种情况下,他们为什么要在第一种情况下提供它。

所以,我想要 Comment 减去 userid 的记录类型,但我不知道如何巧妙地做到这一点。


我当前(糟糕但有效)的解决方案

所以我用派生的 FromJSON(针对请求 body)创建了一个自定义类型,它几乎与 Comment 类型完全冗余。

data SimpleComment = SimpleComment {comment :: ...} deriving (Generic, FromJSON)

然后我的新路由需要根据这个解码请求body,然后将一个SimpleComment和一个userid字段合并为一个Comment

postComment2R :: Handler Value
postComment2R = do
  c <- requireJsonBody :: (Handler SimpleComment)
  (uid, _) requireAuthPair
  insertedComment <- runDB $ insertEntity (Comment { commentUserid  = uid
                                                   , commentComment = comment c})
  returnJson ...

谈论样板。而且我的用例比这个简单的 Comment 类型更复杂。

如果考虑到这一点,您可能会说,我正在使用 Yesod 脚手架

我也在寻找解决这个问题的好方法。 :-)

我在代码中通常做的是直接对Aeson的类型进行操作Value。这是我当前项目中的一些示例代码:

import qualified Data.HashMap.Strict as HM

removeKey :: Text -> Value -> Value
removeKey key (Object xs) = Object $ HM.delete key xs
removeKey _ ys = ys

我直接对值 Object 进行操作并删除 javascript 对象中存在的特定键。

在 Yesod 处理程序代码中,我执行以下处理:

myHandler :: Handler RepJson
myHandler = do
    userId <- insert $ User "sibi" 23
    guser <- getJuser user
    let guser' =  removeKey "someId" $ toJSON guser
    return $ repJson $ object [ "details" .= guser' ]

在某些情况下,我实际上想向传出的 JSON 对象添加一些特定的键。对于那些,我定义了特定的辅助函数,它们在 Value 类型上运行。虽然这并不完美,但它一直在帮助我避免大量样板代码。

为了获得一个类型减去一个字段,我通常做的只是拥有一个接受该字段和 return 类型的函数。在您的情况下,您只需要为 UserId -> Comment 声明一个 JSON 实例。好吧,这看起来不太自然,您必须手动进行,但实际上效果很好,尤其是当 Comment 中只有一个 UserId 类型的字段时。

我喜欢的一个解决方案是对 from/go 进入数据库的事物使用包装器:

data Authenticated a = Authenticated
  { uid :: Uid
  , thing :: a
  } deriving (Show)

然后你可以让 Comment 只是 SimpleComment 并在你知道用户 ID 后将其变成 Authenticated Comment