diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolWidgetChildren.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolWidgetChildren.kt index 55c068fb7c..36ad28dfca 100644 --- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolWidgetChildren.kt +++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolWidgetChildren.kt @@ -36,7 +36,7 @@ internal class ProtocolWidgetChildren( override fun remove(index: Int, count: Int) { val removingIds = ids.subList(index, index + count) - val removedIds = removingIds.toList() + val removedIds = removingIds.toTypedArray() removingIds.clear() for (removedId in removedIds) { diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/GeneratedProtocolBridgeTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/GeneratedProtocolBridgeTest.kt index 1c03c772a1..29878df56a 100644 --- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/GeneratedProtocolBridgeTest.kt +++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/GeneratedProtocolBridgeTest.kt @@ -76,7 +76,7 @@ class GeneratedProtocolBridgeTest { Create(Id(1), WidgetTag(4)), ModifierChange( Id(1), - listOf( + arrayOf( ModifierElement( ModifierTag(3), buildJsonObject { @@ -106,7 +106,7 @@ class GeneratedProtocolBridgeTest { Create(Id(1), WidgetTag(4)), ModifierChange( Id(1), - listOf( + arrayOf( ModifierElement( ModifierTag(5), buildJsonObject { @@ -135,7 +135,7 @@ class GeneratedProtocolBridgeTest { argument = it } - protocolWidget.sendEvent(Event(Id(1), EventTag(4), listOf(JsonPrimitive("PT10S")))) + protocolWidget.sendEvent(Event(Id(1), EventTag(4), arrayOf(JsonPrimitive("PT10S")))) assertThat(argument).isEqualTo(10.seconds) } diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt index ac51de2f3b..16bf17ff35 100644 --- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt +++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt @@ -123,8 +123,8 @@ public class ProtocolBridge( private class RootProtocolNode( private val children: Widget.Children, ) : ProtocolNode, Widget { - override fun updateModifier(elements: List) { - throw AssertionError("unexpected: $elements") + override fun updateModifier(elements: Array) { + throw AssertionError("unexpected: ${elements.contentToString()}") } override fun apply(change: PropertyChange, eventSink: EventSink) { diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolNode.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolNode.kt index cc0fe55f14..c88d88ae81 100644 --- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolNode.kt +++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolNode.kt @@ -41,7 +41,7 @@ public interface ProtocolNode { public fun apply(change: PropertyChange, eventSink: EventSink) - public fun updateModifier(elements: List) + public fun updateModifier(elements: Array) /** * Return one of this node's children groups by its [tag]. diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt index 21bb540978..1aceb25ab8 100644 --- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt +++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt @@ -118,7 +118,7 @@ class ProtocolBridgeTest { tag = ChildrenTag.Root, index = 0, count = 1, - removedIds = listOf(Id(1)), + removedIds = arrayOf(Id(1)), ), ), ) diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolNodeFactoryTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolNodeFactoryTest.kt index b3589d869c..4888905dbd 100644 --- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolNodeFactoryTest.kt +++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolNodeFactoryTest.kt @@ -97,7 +97,7 @@ class ProtocolNodeFactoryTest { val textInput = factory.create(WidgetTag(5))!! textInput.updateModifier( - listOf( + arrayOf( ModifierElement( tag = ModifierTag(3), value = buildJsonObject { @@ -132,7 +132,7 @@ class ProtocolNodeFactoryTest { val textInput = factory.create(WidgetTag(5))!! textInput.updateModifier( - listOf( + arrayOf( ModifierElement( tag = ModifierTag(5), value = buildJsonObject { @@ -164,7 +164,7 @@ class ProtocolNodeFactoryTest { val t = assertFailsWith { button.updateModifier( - listOf( + arrayOf( ModifierElement( tag = ModifierTag(345432), value = JsonObject(mapOf()), @@ -197,7 +197,7 @@ class ProtocolNodeFactoryTest { val textInput = factory.create(WidgetTag(5))!! textInput.updateModifier( - listOf( + arrayOf( ModifierElement( tag = ModifierTag(345432), value = buildJsonArray { @@ -342,7 +342,7 @@ class ProtocolNodeFactoryTest { recordingTextInput.onChangeCustomType!!.invoke(10.seconds) assertThat(eventSink.events.single()) - .isEqualTo(Event(Id(1), EventTag(4), listOf(JsonPrimitive("PT10S")))) + .isEqualTo(Event(Id(1), EventTag(4), arrayOf(JsonPrimitive("PT10S")))) } class RecordingTextInput : TextInput { diff --git a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt index ed57d7c873..4ec345ef9f 100644 --- a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt +++ b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt @@ -15,14 +15,21 @@ */ package app.cash.redwood.protocol +import dev.drewhamilton.poko.ArrayContentBased +import dev.drewhamilton.poko.ArrayContentSupport import dev.drewhamilton.poko.Poko +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ArraySerializer +import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.buildClassSerialDescriptor import kotlinx.serialization.descriptors.element import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure import kotlinx.serialization.json.JsonDecoder import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonEncoder @@ -32,16 +39,69 @@ import kotlinx.serialization.json.buildJsonArray import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive -@Serializable +@OptIn(ArrayContentSupport::class) +@Serializable(EventSerializer::class) @Poko public class Event( /** Identifier for the widget from which this event originated. */ public val id: Id, /** Identifies which event occurred on the widget with [id]. */ public val tag: EventTag, - public val args: List = emptyList(), + @ArrayContentBased public val args: Array = emptyArray(), ) +private object EventSerializer : KSerializer { + private val argsSerializer: KSerializer> = + @OptIn(ExperimentalSerializationApi::class) + ArraySerializer(JsonElement.serializer()) + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor( + serialName = "app.cash.redwood.protocol.Event", + ) { + element("id") + element("tag") + element( + elementName = "args", + descriptor = argsSerializer.descriptor, + isOptional = true, + ) + } + + override fun deserialize(decoder: Decoder): Event { + return decoder.decodeStructure(descriptor) { + var id: Id? = null + var tag: EventTag? = null + var args: Array = emptyArray() + + var nextIndex = decodeElementIndex(descriptor) + while (nextIndex >= 0) { + when (nextIndex) { + 0 -> id = decodeSerializableElement(descriptor, 0, Id.serializer()) + 1 -> tag = decodeSerializableElement(descriptor, 1, EventTag.serializer()) + 2 -> args = decodeSerializableElement(descriptor, 2, argsSerializer) + else -> throw IllegalStateException("Invalid index $nextIndex") + } + nextIndex = decodeElementIndex(descriptor) + } + Event( + id = checkNotNull(id), + tag = checkNotNull(tag), + args = args, + ) + } + } + + override fun serialize(encoder: Encoder, value: Event) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, Id.serializer(), value.id) + encodeSerializableElement(descriptor, 1, EventTag.serializer(), value.tag) + if (value.args.isNotEmpty()) { + encodeSerializableElement(descriptor, 2, argsSerializer, value.args) + } + } + } +} + @Serializable public sealed interface Change { /** Identifier for the widget which is the subject of this change. */ @@ -68,12 +128,13 @@ public class PropertyChange( public val value: JsonElement = JsonNull, ) : ValueChange +@OptIn(ArrayContentSupport::class) @Serializable @SerialName("modifier") @Poko public class ModifierChange( override val id: Id, - public val elements: List = emptyList(), + @ArrayContentBased public val elements: Array = emptyArray(), ) : ValueChange @Serializable(with = ModifierElementSerializer::class) @@ -143,6 +204,7 @@ public sealed interface ChildrenChange : Change { public val count: Int, ) : ChildrenChange + @OptIn(ArrayContentSupport::class) @Serializable @SerialName("remove") @Poko @@ -151,7 +213,7 @@ public sealed interface ChildrenChange : Change { override val tag: ChildrenTag, public val index: Int, public val count: Int, - public val removedIds: List, + @ArrayContentBased public val removedIds: Array, ) : ChildrenChange { init { require(count == removedIds.size) { diff --git a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt index 878ac4e4db..37cef90122 100644 --- a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt +++ b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt @@ -41,13 +41,13 @@ class ProtocolTest { } @Test fun eventNonEmptyArgs() { - val model = Event(Id(1), EventTag(2), listOf(JsonPrimitive("Hello"), JsonPrimitive(2))) + val model = Event(Id(1), EventTag(2), arrayOf(JsonPrimitive("Hello"), JsonPrimitive(2))) val json = """{"id":1,"tag":2,"args":["Hello",2]}""" assertJsonRoundtrip(Event.serializer(), model, json) } @Test fun eventEmptyArgs() { - val model = Event(Id(1), EventTag(2), listOf()) + val model = Event(Id(1), EventTag(2), arrayOf()) val json = """{"id":1,"tag":2}""" assertJsonRoundtrip(Event.serializer(), model, json) } @@ -57,10 +57,10 @@ class ProtocolTest { Create(Id(1), WidgetTag(2)), ChildrenChange.Add(Id(1), ChildrenTag(2), Id(3), 4), ChildrenChange.Move(Id(1), ChildrenTag(2), 3, 4, 5), - ChildrenChange.Remove(Id(1), ChildrenTag(2), 3, 4, listOf(Id(5), Id(6), Id(7), Id(8))), + ChildrenChange.Remove(Id(1), ChildrenTag(2), 3, 4, arrayOf(Id(5), Id(6), Id(7), Id(8))), ModifierChange( Id(1), - listOf( + arrayOf( ModifierElement( ModifierTag(1), buildJsonObject { }, @@ -100,7 +100,7 @@ class ProtocolTest { @Test fun removeCountMustMatchListSize() { val t = assertFailsWith { - ChildrenChange.Remove(Id(1), ChildrenTag(2), 3, 4, listOf(Id(5), Id(6), Id(7))) + ChildrenChange.Remove(Id(1), ChildrenTag(2), 3, 4, arrayOf(Id(5), Id(6), Id(7))) } assertThat(t).hasMessage("Count 4 != Removed ID list size 3") } diff --git a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/SnapshotChangeListTest.kt b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/SnapshotChangeListTest.kt index 8bdcf34b2b..7c6c9050d4 100644 --- a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/SnapshotChangeListTest.kt +++ b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/SnapshotChangeListTest.kt @@ -31,7 +31,7 @@ class SnapshotChangeListTest { val expected = SnapshotChangeList( listOf( Create(Id(1), WidgetTag(1)), - ModifierChange(Id(1), emptyList()), + ModifierChange(Id(1), emptyArray()), PropertyChange(Id(1), PropertyTag(1), JsonPrimitive("Hello")), Add(Id.Root, ChildrenTag.Root, Id(1), 0), ), @@ -58,11 +58,11 @@ class SnapshotChangeListTest { SnapshotChangeList( listOf( Create(Id(1), WidgetTag(1)), - ModifierChange(Id(1), emptyList()), + ModifierChange(Id(1), emptyArray()), Move(Id.Root, ChildrenTag.Root, 1, 2, 3), PropertyChange(Id(1), PropertyTag(1), JsonPrimitive("Hello")), Add(Id.Root, ChildrenTag.Root, Id(1), 0), - Remove(Id.Root, ChildrenTag.Root, 1, 2, listOf(Id(3), Id(4))), + Remove(Id.Root, ChildrenTag.Root, 1, 2, arrayOf(Id(3), Id(4))), ), ) }.hasMessage( diff --git a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt index 8804b42588..784f806e5a 100644 --- a/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt +++ b/redwood-testing/src/commonTest/kotlin/app/cash/redwood/testing/ViewTreesTest.kt @@ -69,26 +69,26 @@ class ViewTreesTest { val expected = listOf( Create(Id(1), WidgetTag(1)), - ModifierChange(Id(1), emptyList()), + ModifierChange(Id(1), emptyArray()), Create(Id(2), WidgetTag(1)), - ModifierChange(Id(2), emptyList()), + ModifierChange(Id(2), emptyArray()), Create(Id(3), WidgetTag(3)), - ModifierChange(Id(3), emptyList()), + ModifierChange(Id(3), emptyArray()), PropertyChange(Id(3), PropertyTag(1), JsonPrimitive("One Fish")), Add(Id(2), ChildrenTag(1), Id(3), 0), Create(Id(4), WidgetTag(3)), - ModifierChange(Id(4), emptyList()), + ModifierChange(Id(4), emptyArray()), PropertyChange(Id(4), PropertyTag(1), JsonPrimitive("Two Fish")), Add(Id(2), ChildrenTag(1), Id(4), 1), Add(Id(1), ChildrenTag(1), Id(2), 0), Create(Id(5), WidgetTag(1)), - ModifierChange(Id(5), emptyList()), + ModifierChange(Id(5), emptyArray()), Create(Id(6), WidgetTag(3)), - ModifierChange(Id(6), emptyList()), + ModifierChange(Id(6), emptyArray()), PropertyChange(Id(6), PropertyTag(1), JsonPrimitive("Red Fish")), Add(Id(5), ChildrenTag(1), Id(6), 0), Create(Id(7), WidgetTag(3)), - ModifierChange(Id(7), emptyList()), + ModifierChange(Id(7), emptyArray()), PropertyChange(Id(7), PropertyTag(1), JsonPrimitive("Blue Fish")), Add(Id(5), ChildrenTag(1), Id(7), 1), Add(Id(1), ChildrenTag(1), Id(5), 1), diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt index e3d49badb9..d92a2f2475 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/composeProtocolGeneration.kt @@ -21,6 +21,7 @@ import app.cash.redwood.tooling.schema.ProtocolWidget import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolChildren import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolProperty +import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BYTE @@ -704,7 +705,7 @@ internal fun generateComposeProtocolModifierSerialization( .addModifiers(INTERNAL) .receiver(Redwood.Modifier) .addParameter("json", KotlinxSerialization.Json) - .returns(LIST.parameterizedBy(Protocol.ModifierElement)) + .returns(ARRAY.parameterizedBy(Protocol.ModifierElement)) .beginControlFlow("return %M", Stdlib.buildList) .addStatement( "this@%L.forEach { element -> add(element.%M(json)) }", @@ -712,6 +713,7 @@ internal fun generateComposeProtocolModifierSerialization( schema.modifierToProtocol, ) .endControlFlow() + .addCode("⇥.toTypedArray()⇤") .build(), ) .addFunction( diff --git a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetProtocolGeneration.kt b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetProtocolGeneration.kt index d169eb13bd..e144e60edc 100644 --- a/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetProtocolGeneration.kt +++ b/redwood-tooling-codegen/src/main/kotlin/app/cash/redwood/tooling/codegen/widgetProtocolGeneration.kt @@ -21,6 +21,7 @@ import app.cash.redwood.tooling.schema.ProtocolWidget import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolChildren import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolEvent import app.cash.redwood.tooling.schema.ProtocolWidget.ProtocolProperty +import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock @@ -29,7 +30,6 @@ import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier.INTERNAL import com.squareup.kotlinpoet.KModifier.OVERRIDE import com.squareup.kotlinpoet.KModifier.PRIVATE -import com.squareup.kotlinpoet.LIST import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec @@ -281,7 +281,7 @@ internal fun generateProtocolNode( beginControlFlow("{ %L ->", trait.parameterTypes.indices.map { CodeBlock.of("arg$it") }.joinToCode()) } addStatement( - "eventSink.sendEvent(%T(change.id, %T(%L), listOf(%L)))", + "eventSink.sendEvent(%T(change.id, %T(%L), arrayOf(%L)))", Protocol.Event, Protocol.EventTag, trait.tag, @@ -360,7 +360,7 @@ internal fun generateProtocolNode( .addFunction( FunSpec.builder("updateModifier") .addModifiers(OVERRIDE) - .addParameter("elements", LIST.parameterizedBy(Protocol.ModifierElement)) + .addParameter("elements", ARRAY.parameterizedBy(Protocol.ModifierElement)) .addStatement("widget.modifier = elements.%M(json, mismatchHandler)", host.toModifier) .addStatement("container?.onModifierUpdated()") .build(), @@ -444,7 +444,7 @@ internal fun generateProtocolModifierImpls( private fun generateJsonArrayToModifier(schema: ProtocolSchema): FunSpec { return FunSpec.builder("toModifier") .addModifiers(INTERNAL) - .receiver(LIST.parameterizedBy(Protocol.ModifierElement)) + .receiver(ARRAY.parameterizedBy(Protocol.ModifierElement)) .addParameter("json", KotlinxSerialization.Json) .addParameter("mismatchHandler", WidgetProtocol.ProtocolMismatchHandler) .addStatement(