温馨提示:本文翻译自stackoverflow.com,查看原文请点击:scala - Handling PATCH requests with Akka HTTP and circe for nullable fields
circe scala akka-http

scala - 使用Akka HTTP处理PATCH请求并绕过可空字段

发布于 2020-04-04 10:55:51

是否存在使用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有效负载中指定字段的话,它会回退)谢谢

查看更多

提问者
Alexey Sirenko
被浏览
140
Travis Brown 2020-01-31 22:20

这是我做这种事情的方式:

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))

这种方法使您可以更清晰地建模意图,同时仍然可以使用通用派生来避免编写编解码器的大部分样板。