diff --git a/core/src/scalive/DiffEngine.scala b/core/src/scalive/DiffEngine.scala new file mode 100644 index 0000000..3cb7e8e --- /dev/null +++ b/core/src/scalive/DiffEngine.scala @@ -0,0 +1,91 @@ +package scalive + +import zio.json.ast.* + +enum Diff: + case Mod( + static: Seq[String] = Seq.empty, + dynamic: Seq[Diff.Dynamic] = Seq.empty + ) + case Split( + static: Seq[String] = Seq.empty, + dynamic: Seq[Diff.Dynamic] = Seq.empty + ) + case Static(value: String) + case Dynamic(index: Int, diff: Diff) + case Deleted + +object DiffEngine: + + def buildInitJson(lv: RenderedLiveView[?]): Json = + JsonAstBuilder + .diffToJson( + buildDiffValue(lv.static, lv.dynamic, includeUnchanged = true) + ) + + def buildDiffJson(lv: RenderedLiveView[?]): Json = + Json + .Obj( + "diff" -> JsonAstBuilder.diffToJson( + buildDiffValue(static = Seq.empty, lv.dynamic) + ) + ) + + private def buildDiffValue( + static: Seq[String], + dynamic: Seq[RenderedMod[?]], + includeUnchanged: Boolean = false + ): Diff.Mod = + Diff.Mod( + static = static, + dynamic = dynamic.zipWithIndex + .filter(includeUnchanged || _._1.wasUpdated) + .map { + case (v: RenderedMod.Dynamic[?, ?], i) => + Diff.Dynamic(i, Diff.Static(v.currentValue.toString)) + case (v: RenderedMod.When[?], i) => + if v.displayed then + if includeUnchanged || v.cond.wasUpdated then + Diff.Dynamic( + i, + buildDiffValue( + v.nested.static, + v.nested.dynamic, + includeUnchanged = true + ) + ) + else + Diff.Dynamic( + i, + buildDiffValue( + static = Seq.empty, + v.nested.dynamic, + includeUnchanged + ) + ) + else Diff.Dynamic(i, Diff.Deleted) + case (v: RenderedMod.Split[?, ?], i) => + Diff.Dynamic( + i, + Diff.Split( + static = if includeUnchanged then v.static else Seq.empty, + dynamic = v.dynamic.toList.zipWithIndex + .filter(includeUnchanged || _._1.exists(_.wasUpdated)) + .map[Diff.Dynamic]((mods, i) => + Diff.Dynamic( + i, + buildDiffValue( + static = Seq.empty, + dynamic = mods, + includeUnchanged + ) + ) + ) + .appendedAll( + v.removedIndexes + .map(i => Diff.Dynamic(i, Diff.Deleted)) + ) + ) + ) + } + ) diff --git a/core/src/scalive/JsonAstBuilder.scala b/core/src/scalive/JsonAstBuilder.scala index f6eb43f..4cd9173 100644 --- a/core/src/scalive/JsonAstBuilder.scala +++ b/core/src/scalive/JsonAstBuilder.scala @@ -1,52 +1,36 @@ package scalive +import zio.Chunk import zio.json.ast.* object JsonAstBuilder: - def buildInit(static: Seq[String], dynamic: Seq[RenderedMod[?]]): Json = - Json - .Obj("s" -> Json.Arr(static.map(Json.Str(_))*)) - .merge(buildDiffValue(dynamic, includeUnchanged = true)) - def buildDiff(dynamic: Seq[RenderedMod[?]]): Json = - Json.Obj("diff" -> buildDiffValue(dynamic)) - - private def buildDiffValue( - dynamic: Seq[RenderedMod[?]], - includeUnchanged: Boolean = false - ): Json = - Json.Obj( - dynamic.zipWithIndex.filter(includeUnchanged || _._1.wasUpdated).map { - case (v: RenderedMod.Dynamic[?, ?], i) => - i.toString -> Json.Str(v.currentValue.toString) - case (v: RenderedMod.When[?], i) => - if v.displayed then - if includeUnchanged || v.cond.wasUpdated then - i.toString -> v.nested.buildInitJson - else - i.toString -> buildDiffValue(v.nested.dynamic, includeUnchanged) - else i.toString -> Json.Bool(false) - case (v: RenderedMod.Split[?, ?], i) => - i.toString -> - Json - .Obj( - (Option - .when(includeUnchanged)( - "s" -> Json.Arr(v.static.map(Json.Str(_))*) + def diffToJson(diff: Diff): Json = + diff match + case Diff.Mod(static, dynamic) => + Json.Obj( + Option + .when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*)) + .to(Chunk) + .appendedAll( + dynamic.map(d => d.index.toString -> diffToJson(d.diff)) + ) + ) + case Diff.Split(static, dynamic) => + Json.Obj( + Option + .when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*)) + .to(Chunk) + .appendedAll( + Option.when(dynamic.nonEmpty)( + "d" -> + Json.Obj( + dynamic.map(d => d.index.toString -> diffToJson(d.diff))* ) - .toList ++ - List( - "d" -> - Json.Obj( - (v.dynamic.toList.zipWithIndex - .filter(includeUnchanged || _._1.exists(_.wasUpdated)) - .map((mods, i) => - i.toString -> buildDiffValue(mods, includeUnchanged) - ) ++ - v.removedIndexes - .map(i => i.toString -> Json.Bool(false)))* - ) - ))* ) - }* - ) + ) + ) + case Diff.Static(value) => Json.Str(value) + case Diff.Dynamic(index, diff) => + Json.Obj(index.toString -> diffToJson(diff)) + case Diff.Deleted => Json.Bool(false) diff --git a/core/src/scalive/LiveViewRenderer.scala b/core/src/scalive/LiveViewRenderer.scala index 74da1f4..5071352 100644 --- a/core/src/scalive/LiveViewRenderer.scala +++ b/core/src/scalive/LiveViewRenderer.scala @@ -1,12 +1,12 @@ package scalive -import scala.collection.mutable.ListBuffer -import scala.collection.immutable.ArraySeq -import zio.json.ast.Json -import scala.collection.mutable.ArrayBuffer -import scalive.LiveViewRenderer.buildStatic import scalive.LiveViewRenderer.buildDynamic +import scalive.LiveViewRenderer.buildStatic + import scala.annotation.nowarn +import scala.collection.immutable.ArraySeq +import scala.collection.mutable.ArrayBuffer +import scala.collection.mutable.ListBuffer class RenderedLiveView[Model] private[scalive] ( val static: ArraySeq[String], @@ -15,8 +15,6 @@ class RenderedLiveView[Model] private[scalive] ( def update(model: Model): Unit = dynamic.foreach(_.update(model)) def wasUpdated: Boolean = dynamic.exists(_.wasUpdated) - def buildInitJson: Json = JsonAstBuilder.buildInit(static, dynamic) - def buildDiffJson: Json = JsonAstBuilder.buildDiff(dynamic) sealed trait RenderedMod[Model]: def update(model: Model): Unit diff --git a/core/src/scalive/main.scala b/core/src/scalive/main.scala index 05086f7..dda0b9f 100644 --- a/core/src/scalive/main.scala +++ b/core/src/scalive/main.scala @@ -15,7 +15,7 @@ def main = ) ) ) - println(r.buildInitJson.toJsonPretty) + println(DiffEngine.buildInitJson(r).toJsonPretty) println("Edit first and last") r.update( MyModel( @@ -26,7 +26,7 @@ def main = ) ) ) - println(r.buildDiffJson.toJsonPretty) + println(DiffEngine.buildDiffJson(r).toJsonPretty) println("Add one") r.update( MyModel( @@ -38,7 +38,7 @@ def main = ) ) ) - println(r.buildDiffJson.toJsonPretty) + println(DiffEngine.buildDiffJson(r).toJsonPretty) println("Remove first") r.update( MyModel( @@ -49,12 +49,12 @@ def main = ) ) ) - println(r.buildDiffJson.toJsonPretty) + println(DiffEngine.buildDiffJson(r).toJsonPretty) println("Remove all") r.update( MyModel(List.empty) ) - println(r.buildDiffJson.toJsonPretty) + println(DiffEngine.buildDiffJson(r).toJsonPretty) final case class MyModel(elems: List[NestedModel]) final case class NestedModel(name: String, age: Int) diff --git a/core/test/src/scalive/LiveViewSpec.scala b/core/test/src/scalive/LiveViewSpec.scala index bda4f8d..a85afad 100644 --- a/core/test/src/scalive/LiveViewSpec.scala +++ b/core/test/src/scalive/LiveViewSpec.scala @@ -32,14 +32,14 @@ object LiveViewSpec extends TestSuite: ) test("init") { assertEqualsJson( - lv.buildInitJson, + DiffEngine.buildInitJson(lv), Json.Obj( "s" -> Json.Arr(Json.Str("