问题描述
我是implementing a client的Keystone 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中,可能出现以下情况:
-
保留
defaultProjectId
的当前值(json对象不包含字段default_project_id
:{ (...) }
-
将
defaultProjectId
更改为an-id
:{ (...),"default_project_id: "an-id",(...) }
-
取消设置
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
一起使用)
-
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
来导出编码器,则代码仍然有效。
缺点:
- 由于
dropNullValues
,Unset
的情况为meh。 - 我们在Json Object字段/值上进行了两次迭代,一次遍历
dropNullValues
,另一次遍历mapObject
。 -
Json.fromString(Unset.getClass.getName)
是任意的,并且可能与T
的合法值发生冲突,尽管可能性很小。
- 使用
-
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 (将#修改为@)