Intermediate diff representation

This commit is contained in:
Paul-Henri Froidmont 2025-08-12 03:30:56 +02:00
parent 13c15cc289
commit 844a1f7953
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
5 changed files with 147 additions and 74 deletions

View file

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

View file

@ -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 =
def diffToJson(diff: Diff): Json =
diff match
case Diff.Mod(static, dynamic) =>
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(_))*)
Option
.when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*))
.to(Chunk)
.appendedAll(
dynamic.map(d => d.index.toString -> diffToJson(d.diff))
)
.toList ++
List(
)
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(
(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)))*
dynamic.map(d => d.index.toString -> diffToJson(d.diff))*
)
))*
)
}*
)
)
case Diff.Static(value) => Json.Str(value)
case Diff.Dynamic(index, diff) =>
Json.Obj(index.toString -> diffToJson(diff))
case Diff.Deleted => Json.Bool(false)

View file

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

View file

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

View file

@ -32,14 +32,14 @@ object LiveViewSpec extends TestSuite:
)
test("init") {
assertEqualsJson(
lv.buildInitJson,
DiffEngine.buildInitJson(lv),
Json.Obj(
"s" -> Json.Arr(Json.Str("<div>Static string</div>"))
)
)
}
test("diff") {
assertEqualsJson(lv.buildDiffJson, emptyDiff)
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
}
}
@ -54,7 +54,7 @@ object LiveViewSpec extends TestSuite:
)
test("init") {
assertEqualsJson(
lv.buildInitJson,
DiffEngine.buildInitJson(lv),
Json
.Obj(
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
@ -63,12 +63,12 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff no update") {
assertEqualsJson(lv.buildDiffJson, emptyDiff)
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
}
test("diff with update") {
lv.update(TestModel(title = "title updated"))
assertEqualsJson(
lv.buildDiffJson,
DiffEngine.buildDiffJson(lv),
Json.Obj(
"diff" -> Json.Obj("0" -> Json.Str("title updated"))
)
@ -77,7 +77,7 @@ object LiveViewSpec extends TestSuite:
test("diff with update and no change") {
lv.update(TestModel(title = "title updated"))
lv.update(TestModel(title = "title updated"))
assertEqualsJson(lv.buildDiffJson, emptyDiff)
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
}
}
@ -96,7 +96,7 @@ object LiveViewSpec extends TestSuite:
)
test("init") {
assertEqualsJson(
lv.buildInitJson,
DiffEngine.buildInitJson(lv),
Json
.Obj(
"s" -> Json.Arr(Json.Str("<div>"), Json.Str("</div>")),
@ -105,16 +105,16 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff no update") {
assertEqualsJson(lv.buildDiffJson, emptyDiff)
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
}
test("diff with unrelated update") {
lv.update(TestModel(title = "title updated"))
assertEqualsJson(lv.buildDiffJson, emptyDiff)
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
}
test("diff when true") {
lv.update(TestModel(bool = true))
assertEqualsJson(
lv.buildDiffJson,
DiffEngine.buildDiffJson(lv),
Json.Obj(
"diff" -> Json.Obj(
"0" ->
@ -132,7 +132,7 @@ object LiveViewSpec extends TestSuite:
lv.update(TestModel(bool = true))
lv.update(TestModel(bool = true, nestedTitle = "nested title updated"))
assertEqualsJson(
lv.buildDiffJson,
DiffEngine.buildDiffJson(lv),
Json.Obj(
"diff" -> Json.Obj(
"0" ->
@ -176,7 +176,7 @@ object LiveViewSpec extends TestSuite:
)
test("init") {
assertEqualsJson(
lv.buildInitJson,
DiffEngine.buildInitJson(lv),
Json
.Obj(
"s" -> Json.Arr(Json.Str("<div><ul>"), Json.Str("</ul></div>")),
@ -205,11 +205,11 @@ object LiveViewSpec extends TestSuite:
)
}
test("diff no update") {
assertEqualsJson(lv.buildDiffJson, emptyDiff)
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
}
test("diff with unrelated update") {
lv.update(initModel.copy(title = "title updated"))
assertEqualsJson(lv.buildDiffJson, emptyDiff)
assertEqualsJson(DiffEngine.buildDiffJson(lv), emptyDiff)
}
test("diff with item changed") {
lv.update(
@ -218,7 +218,7 @@ object LiveViewSpec extends TestSuite:
)
)
assertEqualsJson(
lv.buildDiffJson,
DiffEngine.buildDiffJson(lv),
Json.Obj(
"diff" -> Json.Obj(
"0" ->
@ -239,7 +239,7 @@ object LiveViewSpec extends TestSuite:
initModel.copy(items = initModel.items.appended(NestedModel("d", 35)))
)
assertEqualsJson(
lv.buildDiffJson,
DiffEngine.buildDiffJson(lv),
Json.Obj(
"diff" -> Json.Obj(
"0" ->
@ -261,7 +261,7 @@ object LiveViewSpec extends TestSuite:
initModel.copy(items = initModel.items.tail)
)
assertEqualsJson(
lv.buildDiffJson,
DiffEngine.buildDiffJson(lv),
Json.Obj(
"diff" -> Json.Obj(
"0" ->
@ -286,7 +286,7 @@ object LiveViewSpec extends TestSuite:
test("diff all removed") {
lv.update(initModel.copy(items = List.empty))
assertEqualsJson(
lv.buildDiffJson,
DiffEngine.buildDiffJson(lv),
Json.Obj(
"diff" -> Json.Obj(
"0" ->