mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 05:26:59 +01:00
Support message parameters
This commit is contained in:
parent
763788fb89
commit
681feced9f
16 changed files with 277 additions and 106 deletions
|
|
@ -44,10 +44,11 @@ object DiffBuilder:
|
|||
case Some((entries, keysCount)) =>
|
||||
val static =
|
||||
entries.collectFirst { case (_, Some(el)) => el.static }.getOrElse(List.empty)
|
||||
val includeStatic = !trackUpdates || keysCount == entries.count(_._2.isDefined)
|
||||
List(
|
||||
Some(
|
||||
Diff.Comprehension(
|
||||
static = if trackUpdates then Seq.empty else static,
|
||||
static = if includeStatic then static else Seq.empty,
|
||||
entries = entries.map {
|
||||
case (key, Some(el)) =>
|
||||
Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates))
|
||||
|
|
@ -58,7 +59,6 @@ object DiffBuilder:
|
|||
)
|
||||
)
|
||||
case None => List(None)
|
||||
|
||||
}
|
||||
|
||||
end DiffBuilder
|
||||
|
|
|
|||
|
|
@ -37,14 +37,17 @@ sealed trait Dyn[T]:
|
|||
private[scalive] def callOnEveryChild(f: T => Unit): Unit
|
||||
|
||||
extension [T](parent: Dyn[List[T]])
|
||||
def splitByIndex(project: (Int, Dyn[T]) => HtmlElement): Mod =
|
||||
// TODO fix
|
||||
def splitBy[Key](key: T => Key)(project: (Key, Dyn[T]) => HtmlElement): Mod =
|
||||
Mod.Content.DynSplit(
|
||||
new SplitVar(
|
||||
parent.apply(_.zipWithIndex),
|
||||
key = _._2,
|
||||
project = (index, v) => project(index, v(_._1))
|
||||
parent,
|
||||
key = key,
|
||||
project = (k, v) => project(k, v)
|
||||
)
|
||||
)
|
||||
def splitByIndex(project: (Int, Dyn[T]) => HtmlElement): Mod =
|
||||
parent(_.zipWithIndex).splitBy(_._2)((index, v) => project(index, v(_._1)))
|
||||
|
||||
private class Var[T] private (initial: T) extends Dyn[T]:
|
||||
private[scalive] var currentValue: T = initial
|
||||
|
|
@ -93,7 +96,6 @@ private class SplitVar[I, O, Key](
|
|||
key: I => Key,
|
||||
project: (Key, Dyn[I]) => O):
|
||||
|
||||
// Deleted elements have value none
|
||||
private val memoized: mutable.Map[Key, (Var[I], O)] =
|
||||
mutable.Map.empty
|
||||
|
||||
|
|
@ -141,4 +143,6 @@ private class SplitVar[I, O, Key](
|
|||
private[scalive] def callOnEveryChild(f: O => Unit): Unit =
|
||||
memoized.values.foreach((_, output) => f(output))
|
||||
|
||||
private[scalive] def currentValues: Iterable[O] = memoized.values.map(_._2)
|
||||
|
||||
end SplitVar
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package scalive
|
||||
|
||||
import scalive.JSCommands.JSCommand
|
||||
import scalive.Mod.Attr
|
||||
import scalive.Mod.Content
|
||||
import scalive.codecs.BooleanAsAttrPresenceEncoder
|
||||
import scalive.codecs.Encoder
|
||||
import zio.json.*
|
||||
|
||||
import java.util.Base64
|
||||
import scala.util.Random
|
||||
|
||||
class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
|
||||
def static: Seq[String] = StaticBuilder.build(this)
|
||||
def attrMods: Seq[Mod.Attr] =
|
||||
|
|
@ -25,6 +29,8 @@ class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
|
|||
|
||||
def prepended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.prependedAll(mod))
|
||||
def apended(mod: Mod*): HtmlElement = HtmlElement(tag, mods.appendedAll(mod))
|
||||
def findBinding[Msg](id: String): Option[Map[String, String] => Msg] =
|
||||
mods.iterator.iterator.map(_.findBinding(id)).collectFirst { case Some(f) => f }
|
||||
|
||||
private[scalive] def syncAll(): Unit = mods.foreach(_.syncAll())
|
||||
private[scalive] def setAllUnchanged(): Unit = dynamicMods.foreach(_.setAllUnchanged())
|
||||
|
|
@ -62,13 +68,30 @@ class HtmlAttr[V](val name: String, val codec: Encoder[V, String]):
|
|||
)
|
||||
else Mod.Attr.Dyn(name, value(codec.encode))
|
||||
|
||||
class HtmlAttrJsonValue(val name: String):
|
||||
class HtmlAttrBinding(val name: String):
|
||||
def apply(cmd: JSCommand): Mod.Attr =
|
||||
Mod.Attr.JsBinding(name, cmd.toJson, cmd.bindings)
|
||||
|
||||
def :=[V: JsonEncoder](value: V): Mod.Attr =
|
||||
Mod.Attr.Static(name, value.toJson, isJson = true)
|
||||
def apply[Msg](msg: Msg): Mod.Attr =
|
||||
apply(_ => msg)
|
||||
|
||||
def :=[V: JsonEncoder](value: Dyn[V]): Mod.Attr =
|
||||
Mod.Attr.Dyn(name, value(_.toJson), isJson = true)
|
||||
def apply[Msg](f: Map[String, String] => Msg): Mod.Attr =
|
||||
Mod.Attr.Binding(
|
||||
name,
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(Random().nextBytes(12)),
|
||||
f
|
||||
)
|
||||
def withValue[Msg](f: String => Msg): Mod.Attr =
|
||||
apply(m => f(m("value")))
|
||||
|
||||
trait BindingAdapter[F, Msg]:
|
||||
def createMessage(f: F): Map[String, String] => Msg
|
||||
object BindingAdapter:
|
||||
given fromString[Msg]: BindingAdapter[String => Msg, Msg] = f => m => f(m("value"))
|
||||
given fromMap[Msg]: BindingAdapter[Map[String, String] => Msg, Msg] = f => f
|
||||
|
||||
final case class BindingParams(params: Map[String, String]):
|
||||
def apply(key: String) = params.apply(key)
|
||||
|
||||
sealed trait Mod
|
||||
sealed trait StaticMod extends Mod
|
||||
|
|
@ -76,8 +99,12 @@ sealed trait DynamicMod extends Mod
|
|||
|
||||
object Mod:
|
||||
enum Attr extends Mod:
|
||||
case Static(name: String, value: String, isJson: Boolean = false) extends Attr with StaticMod
|
||||
case StaticValueAsPresence(name: String, value: Boolean) extends Attr with StaticMod
|
||||
case Static(name: String, value: String) extends Attr with StaticMod
|
||||
case StaticValueAsPresence(name: String, value: Boolean) extends Attr with StaticMod
|
||||
case Binding(name: String, id: String, f: Map[String, String] => ?) extends Attr with StaticMod
|
||||
case JsBinding(name: String, jsonValue: String, bindings: Map[String, ?])
|
||||
extends Attr
|
||||
with StaticMod
|
||||
case Dyn(name: String, value: scalive.Dyn[String], isJson: Boolean = false)
|
||||
extends Attr
|
||||
with DynamicMod
|
||||
|
|
@ -96,7 +123,9 @@ object Mod:
|
|||
extension (mod: Mod)
|
||||
private[scalive] def setAllUnchanged(): Unit =
|
||||
mod match
|
||||
case Attr.Static(_, _, _) => ()
|
||||
case Attr.Static(_, _) => ()
|
||||
case Attr.Binding(_, _, _) => ()
|
||||
case Attr.JsBinding(_, _, _) => ()
|
||||
case Attr.StaticValueAsPresence(_, _) => ()
|
||||
case Attr.Dyn(_, value, _) => value.setUnchanged()
|
||||
case Attr.DynValueAsPresence(_, value) => value.setUnchanged()
|
||||
|
|
@ -118,8 +147,10 @@ extension (mod: Mod)
|
|||
|
||||
private[scalive] def syncAll(): Unit =
|
||||
mod match
|
||||
case Attr.Static(_, _, _) => ()
|
||||
case Attr.Static(_, _) => ()
|
||||
case Attr.StaticValueAsPresence(_, _) => ()
|
||||
case Attr.Binding(_, _, _) => ()
|
||||
case Attr.JsBinding(_, _, _) => ()
|
||||
case Attr.Dyn(_, value, _) => value.sync()
|
||||
case Attr.DynValueAsPresence(_, value) => value.sync()
|
||||
case Content.Text(text) => ()
|
||||
|
|
@ -137,4 +168,25 @@ extension (mod: Mod)
|
|||
case Content.DynSplit(v) =>
|
||||
v.sync()
|
||||
v.callOnEveryChild(_.syncAll())
|
||||
|
||||
private[scalive] def findBinding[Msg](id: String): Option[Map[String, String] => Msg] =
|
||||
mod match
|
||||
case Attr.Static(_, _) => None
|
||||
case Attr.StaticValueAsPresence(_, _) => None
|
||||
case Attr.Binding(_, eventId, f) =>
|
||||
if id == eventId then Some(f.asInstanceOf[Map[String, String] => Msg])
|
||||
else None
|
||||
case Attr.JsBinding(_, _, bindings) =>
|
||||
bindings.get(id).map(msg => _ => msg.asInstanceOf[Msg])
|
||||
case Attr.Dyn(_, value, _) => None
|
||||
case Attr.DynValueAsPresence(_, value) => None
|
||||
case Content.Text(text) => None
|
||||
case Content.Tag(el) => el.findBinding(id)
|
||||
case Content.DynText(dyn) => None
|
||||
case Content.DynElement(dyn) => dyn.currentValue.findBinding(id)
|
||||
case Content.DynOptionElement(dyn) => dyn.currentValue.flatMap(_.findBinding(id))
|
||||
case Content.DynElementColl(dyn) =>
|
||||
dyn.currentValue.iterator.map(_.findBinding(id)).collectFirst { case Some(f) => f }
|
||||
case Content.DynSplit(v) =>
|
||||
v.currentValues.iterator.map(_.findBinding(id)).collectFirst { case Some(f) => f }
|
||||
end extension
|
||||
|
|
|
|||
|
|
@ -3,16 +3,21 @@ package scalive
|
|||
import zio.json.*
|
||||
import zio.json.ast.Json
|
||||
|
||||
import java.util.Base64
|
||||
import scala.util.Random
|
||||
|
||||
val JS: JSCommands.JSCommand = JSCommands.empty
|
||||
|
||||
object JSCommands:
|
||||
opaque type JSCommand = List[Json]
|
||||
opaque type JSCommand = List[(Json, Option[Binding[?]])]
|
||||
|
||||
final case class Binding[Msg](id: String, msg: Msg)
|
||||
|
||||
def empty: JSCommand = List.empty
|
||||
|
||||
object JSCommand:
|
||||
given JsonEncoder[JSCommand] =
|
||||
JsonEncoder[Json].contramap(ops => Json.Arr(ops.reverse*))
|
||||
JsonEncoder[Json].contramap(ops => Json.Arr(ops.map(_._1).reverse*))
|
||||
|
||||
private def classNames(names: String): Seq[String] = names.split("\\s+")
|
||||
private def transitionClasses(names: String | (String, String, String))
|
||||
|
|
@ -24,7 +29,15 @@ object JSCommands:
|
|||
|
||||
extension (ops: JSCommand)
|
||||
private def addOp[A: JsonEncoder](kind: String, args: A): JSCommand =
|
||||
(kind, args).toJsonAST.fold(e => throw new IllegalArgumentException(e), identity) :: ops
|
||||
(kind, args).toJsonAST.fold(e => throw new IllegalArgumentException(e), (_, None)) :: ops
|
||||
|
||||
private def addOp[A: JsonEncoder, Msg](kind: String, args: A, binding: Binding[Msg])
|
||||
: JSCommand =
|
||||
(kind, args).toJsonAST
|
||||
.fold(e => throw new IllegalArgumentException(e), (_, Some(binding))) :: ops
|
||||
|
||||
private[scalive] def bindings: Map[String, ?] =
|
||||
ops.flatMap(_._2).map(b => (b.id, b.msg)).toMap
|
||||
|
||||
def addClass = ClassOp("add_class", ops)
|
||||
def toggleClass = ClassOp("toggle_class", ops)
|
||||
|
|
@ -100,20 +113,22 @@ object JSCommands:
|
|||
def popFocus() =
|
||||
ops.addOp("pop_focus", Json.Obj.empty)
|
||||
|
||||
def push[A: JsonEncoder](
|
||||
event: A,
|
||||
def push[Msg](
|
||||
event: Msg,
|
||||
target: String = "",
|
||||
loading: String = "",
|
||||
pageLoading: Boolean = false
|
||||
) =
|
||||
val bindingId = Base64.getUrlEncoder().withoutPadding().encodeToString(Random().nextBytes(12))
|
||||
ops.addOp(
|
||||
"push",
|
||||
Args.Push(
|
||||
event.toJson,
|
||||
bindingId,
|
||||
Option.when(target.nonEmpty)(target),
|
||||
Option.when(loading.nonEmpty)(loading),
|
||||
Option.when(!pageLoading)(pageLoading)
|
||||
)
|
||||
),
|
||||
Binding(bindingId, event)
|
||||
)
|
||||
|
||||
def pushFocus(to: String = "") =
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys:
|
|||
new HtmlAttr(s"phx-$suffix", BooleanAsTrueFalseStringEncoder)
|
||||
private def phxAttrInt(suffix: String): HtmlAttr[Int] =
|
||||
new HtmlAttr(s"phx-$suffix", IntAsStringEncoder)
|
||||
private def phxAttrJson(suffix: String): HtmlAttrJsonValue =
|
||||
new HtmlAttrJsonValue(s"phx-$suffix")
|
||||
private def phxAttrBinding(suffix: String): HtmlAttrBinding =
|
||||
new HtmlAttrBinding(s"phx-$suffix")
|
||||
private def dataPhxAttr(suffix: String): HtmlAttr[String] =
|
||||
dataAttr(s"phx-$suffix")
|
||||
|
||||
|
|
@ -33,41 +33,44 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys:
|
|||
private[scalive] lazy val linkState = dataPhxAttr("link-state")
|
||||
|
||||
// Click
|
||||
lazy val click = phxAttrJson("click")
|
||||
lazy val clickAway = phxAttrJson("click-away")
|
||||
lazy val onClick = phxAttrBinding("click")
|
||||
lazy val onClickAway = phxAttrBinding("click-away")
|
||||
|
||||
// Focus
|
||||
lazy val blur = phxAttrJson("blur")
|
||||
lazy val focus = phxAttrJson("focus")
|
||||
lazy val windowBlur = phxAttrJson("window-blur")
|
||||
lazy val onBlur = phxAttrBinding("blur")
|
||||
lazy val onFocus = phxAttrBinding("focus")
|
||||
lazy val onWindowBlur = phxAttrBinding("window-blur")
|
||||
|
||||
// Keyboard
|
||||
lazy val keydown = phxAttrJson("keydown")
|
||||
lazy val keyup = phxAttrJson("keyup")
|
||||
lazy val windowKeydown = phxAttrJson("window-keydown")
|
||||
lazy val windowKeyup = phxAttrJson("window-keyup")
|
||||
lazy val key = phxAttr("key")
|
||||
lazy val onKeydown = phxAttrBinding("keydown")
|
||||
lazy val onKeyup = phxAttrBinding("keyup")
|
||||
lazy val onWindowKeydown = phxAttrBinding("window-keydown")
|
||||
lazy val onWindowKeyup = phxAttrBinding("window-keyup")
|
||||
// For accepted values, see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
|
||||
lazy val key = phxAttr("key")
|
||||
|
||||
// Scroll
|
||||
lazy val viewportTop = phxAttrJson("viewport-top")
|
||||
lazy val viewportBottom = phxAttrJson("viewport-bottom")
|
||||
lazy val onViewportTop = phxAttrBinding("viewport-top")
|
||||
lazy val onViewportBottom = phxAttrBinding("viewport-bottom")
|
||||
|
||||
// Form
|
||||
lazy val change = phxAttrJson("change")
|
||||
lazy val submit = phxAttrJson("submit")
|
||||
lazy val autoRecover = phxAttrJson("auto-recover")
|
||||
lazy val onChange = phxAttrBinding("change")
|
||||
lazy val onSubmit = phxAttrBinding("submit")
|
||||
lazy val autoRecover = phxAttrBinding("auto-recover")
|
||||
lazy val triggerAction = phxAttrBool("trigger-action")
|
||||
|
||||
// Button
|
||||
lazy val disableWith = phxAttr("disable-with")
|
||||
|
||||
// Socket connection lifecycle
|
||||
lazy val connected = phxAttrJson("connected")
|
||||
lazy val disconnected = phxAttrJson("disconnected")
|
||||
lazy val onConnected = phxAttrBinding("connected")
|
||||
lazy val onDisconnected = phxAttrBinding("disconnected")
|
||||
|
||||
// DOM element lifecycle
|
||||
lazy val mounted = phxAttrJson("mounted")
|
||||
lazy val remove = phxAttrJson("remove")
|
||||
lazy val update = new HtmlAttr["update" | "stream" | "ignore"](s"phx-update", Encoder(identity))
|
||||
lazy val onMounted = phxAttrBinding("mounted")
|
||||
lazy val onRemove = phxAttrBinding("remove")
|
||||
lazy val onUpdate =
|
||||
new HtmlAttr["update" | "stream" | "ignore"](s"phx-update", Encoder(identity))
|
||||
|
||||
// Client hooks
|
||||
lazy val hook = phxAttr("hook")
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ object StaticBuilder:
|
|||
|
||||
private def buildStaticFragments(el: HtmlElement): Seq[Option[String]] =
|
||||
val attrs = el.attrMods.flatMap {
|
||||
case Attr.Static(name, value, isJson) =>
|
||||
if isJson then List(Some(s" $name='$value'"))
|
||||
else List(Some(s""" $name="$value""""))
|
||||
case Attr.Static(name, value) => List(Some(s" $name='$value'"))
|
||||
case Attr.StaticValueAsPresence(name, value) => List(Some(s" $name"))
|
||||
case Attr.Binding(name, id, _) => List(Some(s""" $name="$id""""))
|
||||
case Attr.JsBinding(name, json, _) => List(Some(s" $name='$json'"))
|
||||
case Attr.Dyn(name, value, isJson) =>
|
||||
if isJson then List(Some(s" $name='"), None, Some("'"))
|
||||
else List(Some(s""" $name=""""), None, Some('"'.toString))
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
package scalive
|
||||
|
||||
import scalive.WebSocketMessage.LiveResponse
|
||||
import scalive.WebSocketMessage.Payload
|
||||
import scalive.WebSocketMessage.Payload.EventType
|
||||
import zio.Chunk
|
||||
import zio.json.*
|
||||
import zio.json.ast.Json
|
||||
|
||||
final case class WebSocketMessage(
|
||||
// Live session ID, auto increment defined by the client on join
|
||||
joinRef: Option[Int],
|
||||
// Message ID, global auto increment defined by the client on every message
|
||||
messageRef: Option[Int],
|
||||
// LiveView instance id
|
||||
topic: String,
|
||||
eventType: String,
|
||||
payload: WebSocketMessage.Payload):
|
||||
val meta = WebSocketMessage.Meta(joinRef, messageRef, topic, eventType)
|
||||
def okReply =
|
||||
WebSocketMessage(
|
||||
joinRef,
|
||||
messageRef,
|
||||
topic,
|
||||
"phx_reply",
|
||||
Payload.Reply("ok", LiveResponse.Empty)
|
||||
)
|
||||
object WebSocketMessage:
|
||||
|
||||
final case class Meta(
|
||||
joinRef: Option[Int],
|
||||
messageRef: Option[Int],
|
||||
topic: String,
|
||||
eventType: String)
|
||||
|
||||
given JsonCodec[WebSocketMessage] = JsonCodec[Json].transformOrFail(
|
||||
{
|
||||
case Json.Arr(
|
||||
Chunk(joinRef, Json.Str(messageRef), Json.Str(topic), Json.Str(eventType), payload)
|
||||
) =>
|
||||
val payloadParsed = eventType match
|
||||
case "heartbeat" => Right(Payload.Heartbeat)
|
||||
case "phx_join" => payload.as[Payload.Join]
|
||||
case "phx_leave" => Right(Payload.Leave)
|
||||
case "phx_close" => Right(Payload.Close)
|
||||
case "event" => payload.as[Payload.Event]
|
||||
case s => Left(s"Unknown event type : $s")
|
||||
|
||||
payloadParsed.map(
|
||||
WebSocketMessage(
|
||||
joinRef.asString.map(_.toInt),
|
||||
Some(messageRef.toInt),
|
||||
topic,
|
||||
eventType,
|
||||
_
|
||||
)
|
||||
)
|
||||
case v => Left(s"Could not parse socket message ${v.toJson}")
|
||||
},
|
||||
m =>
|
||||
Json.Arr(
|
||||
m.joinRef.map(ref => Json.Str(ref.toString)).getOrElse(Json.Null),
|
||||
m.messageRef.map(ref => Json.Str(ref.toString)).getOrElse(Json.Null),
|
||||
Json.Str(m.topic),
|
||||
Json.Str(m.eventType),
|
||||
m.payload match
|
||||
case Payload.Heartbeat => Json.Obj.empty
|
||||
case p: Payload.Join => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||
case Payload.Leave => Json.Obj.empty
|
||||
case Payload.Close => Json.Obj.empty
|
||||
case p: Payload.Reply => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||
case p: Payload.Event => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||
case p: Payload.Diff => p.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||
)
|
||||
)
|
||||
|
||||
enum Payload:
|
||||
case Heartbeat
|
||||
case Join(
|
||||
url: Option[String],
|
||||
redirect: Option[String],
|
||||
// params: Map[String, String],
|
||||
session: String,
|
||||
static: Option[String],
|
||||
sticky: Boolean)
|
||||
case Leave
|
||||
case Close
|
||||
case Reply(status: String, response: LiveResponse)
|
||||
case Diff(diff: scalive.Diff)
|
||||
case Event(`type`: Payload.EventType, event: String, value: Map[String, String])
|
||||
object Payload:
|
||||
given JsonCodec[Payload.Join] = JsonCodec.derived
|
||||
given JsonEncoder[Payload.Reply] = JsonEncoder.derived
|
||||
given JsonCodec[Payload.Event] = JsonCodec.derived
|
||||
given JsonEncoder[Payload.Diff] = JsonEncoder[scalive.Diff].contramap(_.diff)
|
||||
|
||||
def okReply(response: LiveResponse) =
|
||||
Payload.Reply("ok", response)
|
||||
|
||||
enum EventType:
|
||||
case Click
|
||||
object EventType:
|
||||
given JsonCodec[EventType] = JsonCodec[String].transformOrFail(
|
||||
{
|
||||
case "click" => Right(Click)
|
||||
case s => Left(s"Unsupported event type: $s")
|
||||
},
|
||||
{ case Click =>
|
||||
"click"
|
||||
}
|
||||
)
|
||||
|
||||
enum LiveResponse:
|
||||
case Empty
|
||||
case InitDiff(rendered: scalive.Diff)
|
||||
case Diff(diff: scalive.Diff)
|
||||
object LiveResponse:
|
||||
given JsonEncoder[LiveResponse] =
|
||||
JsonEncoder[Json].contramap {
|
||||
case Empty => Json.Obj.empty
|
||||
case InitDiff(rendered) =>
|
||||
Json.Obj(
|
||||
"liveview_version" -> Json.Str("1.1.8"),
|
||||
"rendered" -> rendered.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||
)
|
||||
case Diff(diff) =>
|
||||
Json.Obj(
|
||||
"diff" -> diff.toJsonAST.getOrElse(throw new IllegalArgumentException())
|
||||
)
|
||||
}
|
||||
end WebSocketMessage
|
||||
|
|
@ -15,6 +15,9 @@ lazy val DoubleAsIsEncoder: Encoder[Double, Double] = AsIsEncoder()
|
|||
lazy val DoubleAsStringEncoder: Encoder[Double, String] =
|
||||
Encoder[Double, String](_.toString)
|
||||
|
||||
lazy val BooleanAsStringEncoder: Encoder[Boolean, String] =
|
||||
Encoder[Boolean, String](_.toString)
|
||||
|
||||
val BooleanAsIsEncoder: Encoder[Boolean, Boolean] = AsIsEncoder()
|
||||
|
||||
lazy val BooleanAsAttrPresenceEncoder: Encoder[Boolean, String] =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue