Обработка запросов PATCH с помощью Akka HTTP и circe для обнуляемых полей - PullRequest
4 голосов
/ 24 января 2020

Есть ли общий подход для обработки запросов PATCH в REST API с использованием библиотеки circe? По умолчанию 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 (с полями, допускающими обнуление) вместо записи пользовательских код c, который возвращается к no value, если поле не указано в полезной нагрузке JSON? Спасибо

Ответы [ 2 ]

1 голос
/ 31 января 2020

Вот как я это сделал:

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

Такой подход позволяет Вы смоделируете намерение более четко, все еще имея возможность использовать обобщенный c вывод, чтобы избежать большинства шаблонов написания ваших кодеков.

1 голос
/ 29 января 2020

Мне кажется, что центральная проблема здесь (как вы упомянули) заключается в том, что Option[String] выражает 2 состояния, тогда как на самом деле вам требуется 3, а именно:

  • значение присутствует и не равно нулю * Значение 1005 *
  • присутствует, а значение NULL
  • отсутствует

Один из способов решить эту проблему - заключить поля в новый тип

case class PatchField[T](value: Option[T])

Это позволит вам написать класс запроса следующим образом:

case class PatchUserRequest (
    firstName: Option[PatchField[String]],
    lastName: Option[PatchField[String]]
)

Это означает, что ваши полезные данные теперь будут иметь следующую форму:

{
  "firstName": {"value" : "Foo" },
  "lastName": {"value" : "Bar" }
}

{
  "firstName": {"value": "Foo"}
}

{
  "firstName": {"value": null}
}

Я не являюсь конечно, если есть способ принудительно установить, что Circe различает guish между нулем и значениями, которые вообще отсутствуют, но я чувствую, что это может быть хорошим компромиссом.

...