Rest API更新端点的编码器

问题描述

我是implementing a clientKeystone API of Openstack。对于用户,我有以下课程:

import java.time.OffsetDateTime
import io.circe.derivation.{deriveDecoder,deriveEncoder,renaming}
import io.circe.{Decoder,Encoder}

object User {
  object Update {
    implicit val encoder: Encoder[Update] = deriveEncoder(renaming.snakeCase)
  }
  case class Update(
    name: Option[String] = None,password: Option[String] = None,defaultProjectId: Option[String] = None,enabled: Option[Boolean] = None,)

  implicit val decoder: Decoder[User] = deriveDecoder(renaming.snakeCase)
}
final case class User(
  id: String,name: String,domainId: String,passwordExpiresAt: Option[OffsetDateTime] = None,enabled: Boolean = true,)

User.Update包含可能更新用户的字段。更新是使用PATCH完成的,或者说是部分更新。编码器在一个类中使用,该类具有创建,更新,删除,获取和列出用户的方法。该服务类在http4s EntityEncoder中使用带有以下内容的编码器:

import io.circe.{Encoder,Printer}
import org.http4s.{EntityDecoder,circe}
val jsonPrinter: Printer = Printer.noSpaces.copy(dropNullValues = true)
implicit def jsonEncoder[A: Encoder]: EntityEncoder[F,A] = circe.jsonEncoderWithPrinterOf(jsonPrinter)

我的问题是如何为defaultProjectId实施更新。在发送到服务器的最终json中,可能出现以下情况:

  1. 保留defaultProjectId的当前值(json对象不包含字段default_project_id

    {
      (...)
    }
    
  2. defaultProjectId更改为an-id

    {
      (...),"default_project_id: "an-id",(...)
    }
    
  3. 取消设置defaultProjectId

    {
      (...),"default_project_id: null,(...)
    }
    

当前实现:打印机中的defaultProjectId: Option[String] = None + dropNullValues可对情况1和2正确建模,但可防止情况3。

理想情况下,我的ADT如下:

sealed trait Updatable[+T]
case object KeepExistingValue extends Updatable[Nothing]
case object Unset extends Updatable[Nothing]
case class ChangeTo[T](value: T) extends Updatable[T]

用法示例(将来所有字段可能都是Updatable s)

case class Update(
  name: Option[String] = None,defaultProjectId: Updatable[String] = KeepExistingValue,)

但是我找不到一种干净的方法来编码此ADT。尝试的解决方案及其问题(所有这些都不需要在更新方法中将打印机与dropNullValues一起使用)

  1. Unset很特殊:

    // The generic implementation of Updatable
    implicit def updatableEncoder[T](implicit valueEncoder: Encoder[T]): Encoder[Updatable[T]] = {
      case KeepExistingValue => Json.Null
      case Unset => Json.fromString(Unset.getClass.getName) // Or another arbitrary value
      case ChangeTo(value) => valueEncoder(value)
    }
    
    // In the service class
    def nullifyUnsets(obj: JsonObject): JsonObject = obj.mapValues {
      case json if json.asString.contains(Unset.getClass.getName) => Json.Null
      case json => json
    }
    def update(id: String,update: Update): F[Model] = {
      // updateEncoder is of type Encoder[Update]
      updateEncoder(update).dropNullValues.mapObject(nullifyUnsets)
      (...)
    }
    

    优点:

    • 使用dropNullValues可以很好地处理KeepExistingValue情况。
    • 如果用户调用dropNullValues来导出编码器,则代码仍然有效。

    缺点:

    • 由于dropNullValuesUnset的情况为meh。
    • 我们在Json Object字段/值上进行了两次迭代,一次遍历dropNullValues,另一次遍历mapObject
    • Json.fromString(Unset.getClass.getName)是任意的,并且可能与T的合法值发生冲突,尽管可能性很小。
  2. KeepExistingValue很特殊:

    // The generic implementation of Updatable
    implicit def updatableEncoder[T](implicit valueEncoder: Encoder[T]): Encoder[Updatable[T]] = {
      case KeepExistingValue => Json.fromString(Unset.getClass.getName) // Or another arbitrary value
      case Unset => Json.Null
      case ChangeTo(value) => valueEncoder(value)
    }
    
    // In the service class
    def dropKeepExistingValues(obj: JsonObject): JsonObject = obj.filter{
      case (_,json) => !json.asString.contains(Unset.getClass.getName)
    }
    def update(id: String,update: Update): F[Model] = {
      // updateEncoder is of type Encoder[Update]
      updateEncoder(update).mapObject(dropKeepExistingValues)
      (...)
    }
    

    优点:

    • 更简单的实现,updatableEncoder实现更直接地映射到所需的Json。
    • 只需遍历Json Object字段/值。

    缺点:

    • 如果程序员调用dropNullValues派生编码器,则代码将停止工作。
    • Json.fromString(Unset.getClass.getName)是任意的,并且可能与T的合法值发生冲突,尽管可能性很小。

我确定我不是第一个遇到此问题的人,但我找不到它,我得到的最好的是this comment

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...