Расшифровывает запечатанную черту в Argonaut на основе структуры JSON?

Учитывая следующий пример:

sealed trait Id

case class NewId(prefix: String, id: String) extends Id
case class RevisedId(prefix: String, id: String, rev: String) extends Id

case class User(key: Id, name: String)

val json = """
{
  "key": {
    "prefix": "user",
    "id": "Rt01",
    "rev": "0-1"
  },
  "name": "Bob Boberson"
}
"""

implicit val CodecUser: CodecJson[User] = casecodec2(User.apply, User.unapply)("key", "name")

implicit val CodecId: CodecJson[Id] = ???

json.decodeOption[User]

Мне нужно написать CodecJson за Id это будет декодировать объект, когда он имеет правильную структуру.

Добавление некоторого поля дискриминатора является типичным предложением для этого, но я не хочу менять JSON, который я уже создаю / потребляю spray-json а также json4s,

В этих библиотеках ваши кодеры / декодеры в основном просто PartialFunction[JValue, A] а также PartialFunction[A, JValue], Если ваше значение не определено в домене, это ошибка. Это действительно простое, элегантное решение, я думаю. В дополнение к этому у вас есть Экстракторы для типов JSON, так что легко сопоставить объект с полями / структурой.

Rapture идет на шаг дальше, делая порядок полей неважным и игнорируя наличие несовпадающих полей, так что вы можете просто сделать что-то вроде:

case json"""{ "prefix": $prefix, "id": $id, "rev": $rev }""" => 
  RevisedId(prefix, id, rev)

Это действительно просто / мощно.

У меня проблемы с выяснением, как сделать что-то подобное с argonaut, Это лучшее, что я придумал до сих пор:

val CodecNewId = casecodec2(NewId.apply, NewId.unapply)("prefix", "id")
val CodecRevisedId = casecodec3(RevisedId.apply, RevisedId.unapply)("prefix", "id", "rev")

implicit val CodecId: CodecJson[Id] =
  CodecJson.derived[Id](
    EncodeJson {
      case id: NewId => CodecNewId(id)
      case id: IdWithRev => RevisedId(id)
    },
    DecodeJson[Id](c => {
      val q = RevisedId(c).map(a => a: Id)
      q.result.fold(_ => CodecNewId(c).map(a => a: Id), _ => q)
    })
  )

Так что есть несколько проблем с этим. Я должен определить дополнительные кодеки, которые я не собираюсь использовать. Вместо использования экстракторов case-класса в EncodeJson за CodecJson[Id]Я делегирую другие кодеры, которые я определил. Просто не чувствует себя очень прямолинейно или чисто для классов, которые имеют только 2 или 3 поля.

Код для DecodeJson Раздел тоже довольно грязный. Помимо дополнительного набора типов в ifEmpty сторона fold это идентично коду в DecodeJson.|||,

У кого-нибудь есть более идиоматический способ написания базового кодека для Sum-типов в аргонавте, который не требует дискриминатора и может вместо этого соответствовать структуре json?

1 ответ

Это лучшее, что я смог придумать. У него нет такого фундаментального ощущения элегантности, как у частичных функций, но это гораздо более кратко и легче для расшифровки, чем моя первая попытка.

val CodecNewId = casecodec2(NewId.apply, NewId.unapply)("prefix", "id")
val CodecRevisedId = casecodec3(RevisedId.apply, RevisedId.unapply)("prefix", "id", "rev")

implicit val CodecId: CodecJson[Id] = CodecJson(
  {
    case id: NewId => CodecNewId(id)
    case id: RevisedId => CodecRevisedId(id)
  },
   (CodecRevisedId ||| CodecNewId.map(a => a: Id))(_))

Мы все еще используем "конкретные" кодеки для каждого подтипа. Но мы ушли от CodecJson.derive вызов, нам не нужно оборачивать нашу функцию кодирования в EncodeJsonи мы можем map наш DecodeJson функция вместо приведения типов, чтобы мы могли вернуться к использованию ||| вместо того, чтобы копировать его реализацию, что делает код намного более читабельным.

Это определенно полезное решение, если не совсем то, на что я надеялся.

Другие вопросы по тегам