Бесформенный разбор Json LabelledGeneric
Я новичок в Shapeless и извиняюсь, если не вижу простого решения.
Представьте, что у нас есть класс данных
case class Test(x: Int, y: String, z: Double) extends Row
Json (не содержит все поля класса case)
{ "x": 10, "y": "foo" }
и особый случай класса для декодирования JSON
case class UpdateRequest[T <: Row](updates: List[Update[T]])
Update[Row] - это запечатанная черта, помогающая выполнять компоновку обновлений в Slick, размещать ее здесь очень просто, и на самом деле дело не в проблеме.
Цель состоит в том, чтобы проанализировать json (я использую circe) и проверить, существует ли каждое поле json в классе case, предоставляемом в качестве параметра типа для UpdateRequest, и попытаться декодировать значение json с типом, полученным из класса case.
Например мне нужно работать что-то вроде
parse(json).as[UpdateRequest[Test]] ...
Поэтому нам нужен нестандартный декодер, смешанный с бесформенным.
Это общее описание, если вы показываете полное решение было бы здорово, но текущая проблема заключается в том, что я не могу найти конкретное поле по имени из списка полей
Например, я могу декодировать определенное поле, как
def decode[T, U](s: (Symbol with Tagged[U], T), c: HCursor)(implicit decoder: Decoder[T]) = {
c.downField(s._1.name).as[T]
}
val test = Test(1, "foo", 1.5)
val lg = LabelledGeneric[Test]
val fields = Fields[lg.Repr].apply(lg.to(test))
decode(fields.head)
Но как пройтись по всем полям и сначала найти по имени?
Я предполагаю, что это может быть как
def decode[...](fields: [...], c: HCursor, fieldName: String)(implicit decoder: Decoder[T]) = {
// try to find field by name, if exists try to decode
...
}
Спасибо за помощь заранее.
РЕДАКТИРОВАТЬ
Пошаговый упрощенный пример.
У нас есть классы данных, которые представляют строки в БД.
trait Row
case class User(id: Int, age: Int, name: String) extends Row
case class SomeOtherData(id: Int, field1: List[String], field2: Double) extends Row
...
У нас есть API, который принимает любой JSON на таких маршрутах, как
PUT http://192.168.0.1/users/:userId
PUT http://192.168.0.1/other/:otherId
...
Например, мы вызываем PUT http://192.168.0.1/users/:userId next json
{ "age": 100 }
У нас есть специальный класс для декодирования JSON
UpdateRequest[T <: Row](updates: List[Update[T]])
где "обновления" будут похожи
List(
Update((_: User).age, 100)
)
Полный пример этого подхода вы можете найти по адресу https://www.missingfaktor.me/writing/2018/08/12/composable-table-updates-in-slick/
Но, опять же, не имеет значения, каким он будет в конце гонки, потому что причина проблемы в другом.
Итак, мы анализируем входящий json как UpdateRequest[User]. 1) Мы просматриваем все поля в Json и пытаемся найти каждое из них в LabelledGeneric[User] 2) Если поле найдено, мы пытаемся декодировать его до типа найденного с помощью circe. В противном случае DecodingFailure.
Это может быть похоже (типы и реализация не верны, просто пример, чтобы показать идею)
object UpdateRequest {
import shapeless._
import shapeless.ops.record._
def decode[T, U](s: (Symbol with Tagged[U], T), c: HCursor)(implicit decoder: Decoder[T]) = {
c.downField(s._1.name).as[T]
}
implicit def decoder[R <: Row, HL <: HList]()(implicit gen: LabelledGeneric.Aux[R, HL]): Decoder[UpdateRequest[R]] = new Decoder[UpdateRequest[R]] {
final def apply(c: HCursor): Decoder.Result[UpdateRequest[R]] = {
c.keys match {
case Some(keys) =>
// we got "age" key from json
// for each json key we try to find field in LabelledGeneric's Repr
// (maybe we need Fields here instead)
// so we found "age" in case class User and determine the type is Int
// and then try to decode to Int
val field = ... //found field from Repr
for {
age <- decode(field, c)
} yield ...
// and after make it as UpdateRequest[Row] (not needed to implement, the problem is above)
case None => Left(DecodingFailure("Empty json", Nil))
}
}
}
}
Спасибо всем заранее.
1 ответ
Это не совсем ваш случай, но кажется, что он очень близок к тому, что вам нужно. Вероятно, вы можете использовать эту идею. Я добавил несколько заглушек, чтобы код работал без каких-либо зависимостей. Идея заключалась в том, чтобы перебирать поля case вместо полей из json. Однако дизайн выглядит странно, поскольку мне пришлось создать экземпляр класса case, чтобы извлечь из него имена и типы полей без использования значений.
trait Decoder[T] {
def apply(t:String): T
}
implicit val decoderStr: Decoder[String] = (t) => t
implicit val decoderInt: Decoder[Int] = (t) => t.toInt
implicit val decoderFloat: Decoder[Double] = (t) => t.toDouble
val c = new {
def downField(name:String):String = name match {
case "x" => "5"
case "y" => "some string"
case "z" => "2.0"
}
}
implicit class Stbb (a: String) {
def as[T](implicit dc: Decoder[T])= dc.apply(a)
}
//end of stubs
import shapeless._
import shapeless.labelled._
case class Test(x: Int, y: String, z: Double)
val test = Test(1, "foo", 1.5)
val lg = LabelledGeneric[Test]
val genericTest = lg.to(test)
object mapToEncoded extends Poly1 {
implicit def toEncodedElements[K,A](implicit key: Witness.Aux[K], dec: Decoder[A]) =
at[FieldType[K,A]](_ => Some(c.downField(key.value.toString.drop(1)).as[A])))
}
//HList with Option[T] values decoded to proper type
val res = genericTest.map(mapToEncoded)
println(res)
Вам, вероятно, просто нужно заменить Option
с Decoder.Result
и позаботиться об исключениях в toEncodedElements