play-json на AST с параметрами типа

Я пытаюсь создать чтение и запись play-json для AST, который в основном выглядит так

abstract sealed trait Rule[A] {
    def roomId: Option[Long] = None
    def valid(in: A): Boolean
}

abstract sealed trait ValueRule[A, B] extends Rule[A] {
    def value: B
}

abstract sealed trait NoValueRule[A] extends Rule[A]
case class OnlyDuringWorkHours(override val roomId: Option[Long] = None) extends NoValueRule[((ResStart, ResEnd), Center)] {
    override def valid(in: ((ResStart, ResEnd), Center)): Boolean = true
}

case class MaxLeadTime(override val roomId: Option[Long] = None, override val value: Int) extends ValueRule[ResStart, Int] {
    override def valid(in: ResStart): Boolean = true
}

case class MaxDuration(override val roomId: Option[Long] = None, override val value: String) extends ValueRule[(ResStart, ResEnd), String] {
    override def valid(in: (ResStart, ResEnd)): Boolean = true
}

case class Rules(centerId: Long, ruleList: Seq[Rule[_]])

моя попытка сделать это выглядит так

object Rule {
    implicit def ruleReads[R, V](implicit rReads: Reads[R], vReads: Reads[V] = null): Reads[Rule[R]] = {
        val theVRead = Option(vReads)
        val nvr = ???

        if (Option(theVRead).isDefined) {
            val vr = ???
            __.read[ValueRule[R, V]](vr).map(x => x.asInstanceOf[Rule[R]]).orElse(__.read[NoValueRule[R]](nvr).map(x => x.asInstanceOf[Rule[R]]))
        } else {
            __.read[NoValueRule[R]](nvr).map(x => x.asInstanceOf[Rule[R]])
        }
    }
    implicit def ruleWrites[R, V](implicit rWrites: Writes[R], vWrites: Writes[V] = null): Writes[Rule[R]] = Writes[Rule[R]]{
        case nv: NoValueRule[R] => Json.writes[NoValueRule[R]].writes(nv)
        case v: ValueRule[R, V] => Json.writes[ValueRule[R, V]].writes(v)
    }
}
object ValueRule {
    implicit def valueRuleReads[R, V](implicit rReads: Reads[R], vReads: Reads[V]): Reads[ValueRule[R, V]] = {
        val mlt = Json.reads[MaxLeadTime]
        val md = Json.reads[MaxDuration]
         __.read[MaxDuration](md).map(x => x.asInstanceOf[ValueRule[R, V]])
        .orElse(
            __.read[MaxLeadTime](mlt).map(x => x.asInstanceOf[ValueRule[R, V]])
        )
    }
    implicit def valueRuleWrites[R, V](implicit rWrites: Writes[R], vWrites: Writes[V]): Writes[ValueRule[R, V]] = Writes[ValueRule[R, V]]{
        case mlt: MaxLeadTime => Json.writes[MaxLeadTime].writes(mlt)
        case md: MaxDuration => Json.writes[MaxDuration].writes(md)
    }
}

object NoValueRule {
    implicit def noValueRuleReads[R](implicit rReads: Reads[R]): Reads[NoValueRule[R]] = {
        val odwh = Json.reads[OnlyDuringWorkHours]
        __.read[OnlyDuringWorkHours](odwh).map(x => x.asInstanceOf[NoValueRule[R]])
    }
    implicit def noValueRuleWrites[R](implicit rWrites: Writes[R]): Writes[NoValueRule[R]] = Writes[NoValueRule[R]]{
        case odwh: OnlyDuringWorkHours => Json.writes[OnlyDuringWorkHours].writes(odwh)
    }
}
object OnlyDuringWorkHours {
    implicit val format: Format[OnlyDuringWorkHours] = Json.format[OnlyDuringWorkHours]
}

object MaxLeadTime {
    implicit val format: Format[MaxLeadTime] = Json.format[MaxLeadTime]
}
object MaxDuration {
    implicit val format: Format[MaxDuration] = Json.format[MaxDuration]
}

object Rules {
    import play.api.libs.json.Reads._
    import play.api.libs.functional.syntax._

    implicit val rulesReads: Reads[Rules] = (
        (JsPath \ "centerId").read[Long] and
        (JsPath \ "ruleList").read[Seq[Rule]]
    )(Rules.apply _)
    implicit val rulesWrites: Writes[Rules] = (
        (JsPath \ "centerId").write[Long] and
        ???
    )(unlift(Rules.unapply))
    implicit val format: Format[Rules] = Format(rulesReads, rulesWrites)
}

Это оставляет меня с двумя проблемами.

Во-первых, если я подключу выражения, которые я считаю правильными, в Rule.ruleReads для двух экземпляров ???, Json.reads[NoValueRule[R]] и Json.reads[ValueRule[R, V]] соответственно, я получаю следующую ошибку компиляции.

cmd16.sc:8: type mismatch;
 found   : play.api.libs.json.JsResult[Helper.this.OnlyDuringWorkHours]
 required: play.api.libs.json.JsResult[Helper.this.NoValueRule[R]]
        val nvr = Json.reads[NoValueRule[R]]
                            ^cmd16.sc:11: type mismatch;
 found   : play.api.libs.json.JsResult[Helper.this.MaxLeadTime]
 required: play.api.libs.json.JsResult[Helper.this.ValueRule[R,V]]
            val vr = Json.reads[ValueRule[R, V]]
                               ^

во-вторых, если я оставлю ???, чтобы эта часть скомпилировала его, а затем не смогла скомпилировать объект правил с

cmd17.sc:71: No Json deserializer found for type Seq[cmd17Wrapper.this.cmd16.wrapper.Rule]. Try to implement an implicit Reads or Format for this type.
        (JsPath \ "ruleList").read[Seq[Rule]]
                                  ^

Я могу заставить правила читать/записывать формат и получать очень похожую ошибку

Я думаю, что проблема с 2 заключается в разнице между Правилами, содержащими Seq[Rule[_]], и моим определением неявного чтения, которое должно охватывать любое конкретное правило, но не правило, которое может быть чем угодно.

Любые идеи, как я могу заставить это работать? Я чувствую, что это должно быть возможно, но, возможно, это не так.


person Mark    schedule 02.01.2018    source источник
comment
Не могли бы вы также описать, как вы ожидаете, что результат JSON будет выглядеть? У вас есть известный фиксированный формат? Или все, что может быть закодировано и декодировано кодом Scala, в порядке? Проблема в том, что ваши подтипы Rule очень похожи по форме, поэтому очевидное решение — добавить в JSON некоторый явный дискриминатор типов. Это нормально? Или вы действительно хотите различать фактические типы по тонким различиям в JSON?   -  person SergGr    schedule 03.01.2018
comment
добавление в json чего-то, что явно не отображается в классе case, совершенно нормально, хотя я еще не пробовал, я думаю, в этом случае мне пришлось бы писать пользовательские чтения и записи для каждого тогда?   -  person Mark    schedule 03.01.2018


Ответы (1)


Хотя я думаю, что вам следует попробовать какую-нибудь библиотеку на основе макросов, которую можно найти, погуглив «запечатанную черту воспроизведения json», например Воспроизведите кодеки, производные от JSON , вот написанное от руки решение, которое может вам подойти:

object PlayJson {

  import play.api.libs.json._

  // fake types instead of your real ones
  type ResStart = Int
  type ResEnd = Int
  type Center = Int

  sealed trait Rule[A] {
    def roomId: Option[Long] = None

    def valid(in: A): Boolean
  }

  sealed trait ValueRule[A, B] extends Rule[A] {
    def value: B
  }

  sealed trait NoValueRule[A] extends Rule[A]

  case class OnlyDuringWorkHours(override val roomId: Option[Long] = None) extends NoValueRule[((ResStart, ResEnd), Center)] {
    override def valid(in: ((ResStart, ResEnd), Center)): Boolean = true
  }

  case class MaxLeadTime(override val roomId: Option[Long] = None, override val value: Int) extends ValueRule[ResStart, Int] {
    override def valid(in: ResStart): Boolean = true
  }

  case class MaxDuration(override val roomId: Option[Long] = None, override val value: String) extends ValueRule[(ResStart, ResEnd), String] {
    override def valid(in: (ResStart, ResEnd)): Boolean = true
  }

  case class Rules(centerId: Long, ruleList: Seq[Rule[_]])


  object CompoundFormat {
    final val discriminatorKey = "$type$"

    private case class UnsafeFormatWrapper[U, R <: U : ClassTag](format: OFormat[R]) extends OFormat[U] {
      def typeName: String = {
        val clazz = implicitly[ClassTag[R]].runtimeClass
        try {
          clazz.getSimpleName
        }
        catch {
          // getSimpleName might fail for inner classes because of the name mangling
          case _: InternalError => clazz.getName
        }
      }

      override def reads(json: JsValue): JsResult[U] = format.reads(json)

      override def writes(o: U): JsObject = {
        val base = format.writes(o.asInstanceOf[R])
        base + (discriminatorKey, JsString(typeName))
      }
    }

  }

  class CompoundFormat[A]() extends OFormat[A] {

    import CompoundFormat._

    private val innerFormatsByName = mutable.Map.empty[String, UnsafeFormatWrapper[A, _]]
    private val innerFormatsByClass = mutable.Map.empty[Class[_], UnsafeFormatWrapper[A, _]]

    override def reads(json: JsValue): JsResult[A] = {
      val jsObject = json.asInstanceOf[JsObject]
      val name = jsObject(discriminatorKey).asInstanceOf[JsString].value
      val innerFormat = innerFormatsByName.getOrElse(name, throw new RuntimeException(s"Unknown child type $name"))
      innerFormat.reads(jsObject)
    }

    override def writes(o: A): JsObject = {
      val innerFormat = innerFormatsByClass.getOrElse(o.getClass, throw new RuntimeException(s"Unknown child type ${o.getClass}"))
      innerFormat.writes(o)
    }

    def addSubType[R <: A : ClassTag](format: OFormat[R]): Unit = {
      val wrapper = new UnsafeFormatWrapper[A, R](format)
      innerFormatsByName.put(wrapper.typeName, wrapper)
      innerFormatsByClass.put(implicitly[ClassTag[R]].runtimeClass, wrapper)
    }
  }

  def buildRuleFormat: OFormat[Rule[_]] = {
    val compoundFormat = new CompoundFormat[Rule[_]]
    compoundFormat.addSubType(Json.format[OnlyDuringWorkHours])
    compoundFormat.addSubType(Json.format[MaxLeadTime])
    compoundFormat.addSubType(Json.format[MaxDuration])
    compoundFormat
  }

  def test(): Unit = {
    implicit val ruleFormat = buildRuleFormat
    implicit val rulesFormat = Json.format[Rules]

    val rules0 = Rules(1, List(
      OnlyDuringWorkHours(Some(1)),
      MaxLeadTime(Some(2), 2),
      MaxDuration(Some(3), "Abc")
    ))

    val json = Json.toJsObject(rules0)
    println(s"encoded: '$json'")
    val rulesDecoded = Json.fromJson[Rules](json)
    println(s"decoded: $rulesDecoded")
  }
}

вызов PlayJson.test печатает

закодировано: '{"centerId":1,"ruleList":[{"roomId":1,"$type$":"OnlyDuringWorkHours"},{"roomId":2,"value":2,"$type$ ":"MaxLeadTime"},{"roomId":3,"value":"Abc","$type$":"MaxDuration"}]}'


расшифровано: JsSuccess(Rules(1,List(OnlyDuringWorkHours(Some(1)), MaxLeadTime(Some(2),2), MaxDuration(Some(3),Abc))),)

Основная идея состоит в том, чтобы иметь CompoundFormat для запечатанного признака, который хранит сопоставление между именем класса и соответствующим OFormat для каждого дочернего элемента.


Обновление (о проблемах с отражением)

Вот неуниверсальная версия CompoundFormat, которая, как я ожидаю, будет похожа на то, что может генерировать библиотека на основе макросов (на самом деле я ожидаю, что хорошая библиотека на основе макросов также обработает случай, когда некоторые из потомков запечатанного типажа являются одноэлементными object, а не чем class, который этот код не обрабатывает):

object ExplicitRuleFormat {
  implicit val format: OFormat[Rule[_]] = new ExplicitRuleFormat()

  private object InnerFormats {

    final val discriminatorKey = "$type$"
    implicit val onlyDuringWorkHoursFormat = Json.format[OnlyDuringWorkHours]
    final val onlyDuringWorkHoursTypeName = "OnlyDuringWorkHours"
    implicit val maxLeadTimeFormat = Json.format[MaxLeadTime]
    final val maxLeadTimeTypeName = "MaxLeadTime"
    implicit val maxDurationFormat = Json.format[MaxDuration]
    final val maxDurationTypeName = "MaxDuration"
  }

}

class ExplicitRuleFormat extends OFormat[Rule[_]] {

  import ExplicitRuleFormat.InnerFormats._

  override def reads(json: JsValue): JsResult[Rule[_]] = {
    val jsObject = json.asInstanceOf[JsObject]
    val name = jsObject(discriminatorKey).asInstanceOf[JsString].value
    name match {
      case s if onlyDuringWorkHoursTypeName.equals(s) => Json.fromJson[OnlyDuringWorkHours](jsObject)
      case s if maxLeadTimeTypeName.equals(s) => Json.fromJson[MaxLeadTime](jsObject)
      case s if maxDurationTypeName.equals(s) => Json.fromJson[MaxDuration](jsObject)
    }
  }

  override def writes(r: Rule[_]): JsObject = r match {
    case rr: OnlyDuringWorkHours => writeImpl(rr, onlyDuringWorkHoursTypeName)
    case rr: MaxLeadTime => writeImpl(rr, maxLeadTimeTypeName)
    case rr: MaxDuration => writeImpl(rr, maxDurationTypeName)
  }

  def writeImpl[R <: Rule[_]](r: R, typeName: String)(implicit w: OWrites[R]): JsObject = {
    Json.toJsObject(r) + (discriminatorKey, JsString(typeName))
  }
}

и с этим test становится:

def test(): Unit = {
  import ExplicitRuleFormat.format
  implicit val rulesFormat = Json.format[Rules]

  val rules0 = Rules(1, List(
    OnlyDuringWorkHours(Some(1)),
    MaxLeadTime(Some(2), 2),
    MaxDuration(Some(3), "Abc")
  ))

  val json = Json.toJsObject(rules0)
  println(s"encoded: '$json'")
  val rulesDecoded = Json.fromJson[Rules](json)
  println(s"decoded: $rulesDecoded")
}

Фактически вы просто заменяете implicit val ruleFormat = buildRuleFormat на import ExplicitRuleFormat.format.

person SergGr    schedule 03.01.2018
comment
это, кажется, отлично работает, спасибо. жаль, что это было не так много кода для grok, но он определенно делает то, что я хочу, спасибо. - person Mark; 03.01.2018
comment
как вы думаете, вы могли бы обойти отражение, определив перечисления для каждого правила и сопоставления с образцом? - person Mark; 03.01.2018
comment
@ Ir1sh, я не уверен, чего именно вы пытаетесь избежать здесь, что вы называете отражением. Если вы имеете в виду, можете ли вы явно указать другие имена, кроме Class.getSimpleName/Class.getName - да, вы, очевидно, можете. Если это что-то другое - вы должны указать это более явно. - person SergGr; 04.01.2018
comment
почему оболочка небезопасного формата небезопасна? это не потому, что он использует отражение? Использует ли ClassTag отражение? - person Mark; 04.01.2018
comment
@ Ir1sh, нет, это небезопасно, потому что OFormat, представляющее собой комбинацию Reads+OWrites, является инвариантным, поэтому я должен использовать явное приведение в format.writes(o.asInstanceOf[R]), которое основано на том факте, что то, что я туда поместил, на самом деле будет иметь правильный суб- тип. Если вы сделаете CompoundFormat необобщенным и используете явное сопоставление с образцом вместо innerFormatsByClass, я думаю, вы сможете избежать этого приведения. P.S. ClassTag разрешается компилятором во время компиляции. Все, что мне нужно, это runtimeClass, который в Java мне пришлось бы вместо этого явно передать addSubType. В Scala я могу использовать этот прием. - person SergGr; 04.01.2018
comment
@Ir1sh, чтобы прояснить свою точку зрения, я добавил в свой ответ необщий код ExplicitRuleFormat. Я считаю, что это фактически то же самое, что и моя первоначальная реализация, но с той разницей, что она адаптирована специально для признака Rule[_]. - person SergGr; 04.01.2018