是否存在使用circe库处理REST API中PATCH请求的通用方法?默认情况下,circe不允许仅使用指定的部分字段来解码部分JSON,即,它要求设置所有字段。您可以使用withDefaults
配置,但是将无法知道是否null
指定接收到的字段。这是可能解决方案的简化示例。Left[Unit]
当根本没有指定该字段时,它用作值来处理情况:
# possible payloads
{
"firstName": "Foo",
"lastName": "Bar"
}
{
"firstName": "Foo"
}
{
"firstName": null
}
import de.heikoseeberger.akkahttpcirce.FailFastCirceSupport._
import io.circe.generic.auto._
import io.circe.{Decoder, HCursor}
case class User(firstName: Option[String], lastName: String)
// In PATCH request only 1 field can be specified. The rest could be omitted. Left represents `not specified`
case class PatchUserRequest(firstName: Either[Unit, Option[String]], lastName: Either[Unit, String])
object PatchUserRequest {
implicit val decode: Decoder[PatchUserRequest] = new Decoder[PatchUserRequest] {
final def apply(c: HCursor): Decoder.Result[PatchUserRequest] =
for {
// Here we handle `no field specified` error cases as Left[Unit]
foo <- c.downField("firstName").as[Option[String]] match {
case Left(noFieldSpecified) => Right(Left(()))
case Right(result) => Right(Right(result))
}
bar <- c.downField("lastName").as[String] match {
case Left(noFieldSpecified) => Right(Left(()))
case Right(result) => Right(Right(result))
}
} yield PatchUserRequest(foo, bar)
}
}
object Apis extends Directives {
var user = User("Foo", "Bar")
val create = path("user")(post(entity(as[User])(newUser => user = newUser)))
val patch = path("user")(patch(entity(as[PatchUserRequest])(patchRequest => patch(patchRequest))))
// If field is specified - update the record, ignore otherwise
def patch(request: PatchUserRequest) {
request.firstName.foreach(newFirstName => user = user.copy(firstName = newFirstName)
request.lastName.foreach(newlastName => user = user.copy(lastName = newlastName)
}
有没有更好的方法来处理PATCH请求(具有可为空的字段),而不是编写自定义编解码器(no value
如果未在JSON有效负载中指定字段的话,它会回退)?谢谢
这是我做这种事情的方式:
import io.circe.{Decoder, Encoder, FailedCursor, Json}
import java.util.UUID
sealed trait UpdateOrDelete[+A]
case object Missing extends UpdateOrDelete[Nothing]
case object Delete extends UpdateOrDelete[Nothing]
final case class UpdateWith[A](value: A) extends UpdateOrDelete[A]
object UpdateOrDelete {
implicit def decodeUpdateOrDelete[A](
implicit decodeA: Decoder[A]
): Decoder[UpdateOrDelete[A]] = Decoder.withReattempt {
// We're trying to decode a field but it's missing.
case c: FailedCursor if !c.incorrectFocus => Right(Missing)
case c => Decoder.decodeOption[A].tryDecode(c).map {
case Some(a) => UpdateWith(a)
case None => Delete
}
}
// Random UUID to _definitely_ avoid collisions
private[this] val marker: String = s"$$marker-${UUID.randomUUID()}-marker$$"
private[this] val markerJson: Json = Json.fromString(marker)
implicit def encodeUpdateOrDelete[A](
implicit encodeA: Encoder[A]
): Encoder[UpdateOrDelete[A]] = Encoder.instance {
case UpdateWith(a) => encodeA(a)
case Delete => Json.Null
case Missing => markerJson
}
def filterMarkers[A](encoder: Encoder.AsObject[A]): Encoder.AsObject[A] =
encoder.mapJsonObject(
_.filter {
case (_, value) => value != markerJson
}
)
}
接着:
import io.circe.generic.semiauto._
case class UserPatch(
id: Long,
firstName: UpdateOrDelete[String],
lastName: UpdateOrDelete[String]
)
object UserPatch {
implicit val decodeUserPatch: Decoder[UserPatch] = deriveDecoder
implicit val encodeUserPatch: Encoder.AsObject[UserPatch] =
UpdateOrDelete.filterMarkers(deriveEncoder[UserPatch])
}
接着:
scala> import io.circe.syntax._
import io.circe.syntax._
scala> UserPatch(101, Missing, Delete).asJson
res0: io.circe.Json =
{
"id" : 101,
"lastName" : null
}
scala> UserPatch(101, UpdateWith("Foo"), Missing).asJson
res1: io.circe.Json =
{
"id" : 101,
"firstName" : "Foo"
}
scala> io.circe.jawn.decode[UserPatch]("""{"id":1}""")
res2: Either[io.circe.Error,UserPatch] = Right(UserPatch(1,Missing,Missing))
这种方法使您可以更清晰地建模意图,同时仍然可以使用通用派生来避免编写编解码器的大部分样板。