Skip to content

Commit

Permalink
Create RedwoodVersion type for sharing across host-guest bridge (#1929)
Browse files Browse the repository at this point in the history
In order to work around bugs in the future or change the protocol in a backwards-incompatible way, the version gives the other side of the bridge the ability to change itself to remain compatible.

For now, we only have the host version hooked up to the guest side. The guest version being exposed to the host will come in a follow-up.
  • Loading branch information
JakeWharton authored Apr 4, 2024
1 parent 9247a0c commit a743d06
Show file tree
Hide file tree
Showing 32 changed files with 466 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Changed:
- Protocol host code is now under `your.package.protocol.host`.
- The 'app.cash.redwood.generator.compose.protocol' and 'app.cash.redwood.generator.widget.protocol' Gradle plugins are now deprecated and will be removed in the next release. Use 'app.cash.redwood.generator.protocol.guest' and 'app.cash.redwood.generator.protocol.host', respectively.
- The 'redwood-tooling-codegen' CLI flags for protocol codegen have changed from `--compose-protocol` and `--widget-protocol` to `--protocol-guest` and `--protocol-host`, respectively.
- Entrypoints to the protocol on the host-side and guest-side now require supplying the version of Redwood in use on the other side in order to ensure compatibility and work around any bugs in older versions. This uses a new `RedwoodVersion` type, and will be automatically wired if using our Treehouse artifacts.

Fixed:
- Fix failure to release JS resources when calling `CoroutineScope` is being cancelled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import app.cash.redwood.Modifier
import app.cash.redwood.RedwoodCodegenApi
import app.cash.redwood.layout.testing.RedwoodLayoutTestingWidgetFactory
import app.cash.redwood.lazylayout.testing.RedwoodLazyLayoutTestingWidgetFactory
import app.cash.redwood.protocol.guest.guestRedwoodVersion
import app.cash.redwood.protocol.host.ProtocolBridge
import app.cash.redwood.protocol.host.hostRedwoodVersion
import app.cash.redwood.testing.TestRedwoodComposition
import app.cash.redwood.testing.WidgetValue
import app.cash.redwood.widget.MutableListChildren
Expand Down Expand Up @@ -55,10 +57,15 @@ class ProtocolChangeListenerTest : AbstractChangeListenerTest() {
widgetSystem: TestSchemaWidgetSystem<WidgetValue>,
snapshot: () -> T,
): TestRedwoodComposition<T> {
val composeBridge = TestSchemaProtocolBridge.create()
val widgetBridge = ProtocolBridge(MutableListChildren(), TestSchemaProtocolFactory(widgetSystem)) {
throw AssertionError()
}
val composeBridge = TestSchemaProtocolBridge.create(
hostVersion = hostRedwoodVersion,
)
val widgetBridge = ProtocolBridge(
guestVersion = guestRedwoodVersion,
container = MutableListChildren(),
factory = TestSchemaProtocolFactory(widgetSystem),
eventSink = { throw AssertionError() },
)
return TestRedwoodComposition(this, composeBridge.widgetSystem, composeBridge.root) {
composeBridge.getChangesOrNull()?.let { changes ->
widgetBridge.sendChanges(changes)
Expand Down
8 changes: 6 additions & 2 deletions redwood-protocol-guest/api/android/redwood-protocol-guest.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge :
}

public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge$Factory {
public abstract fun create (Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
public static synthetic fun create$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
public abstract fun create-Bvskwvs (Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
public static synthetic fun create-Bvskwvs$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
}

public abstract interface class app/cash/redwood/protocol/guest/ProtocolMismatchHandler {
Expand Down Expand Up @@ -54,3 +54,7 @@ public final class app/cash/redwood/protocol/guest/ProtocolWidgetChildren : app/
public final fun visitIds (Lkotlin/jvm/functions/Function1;)V
}

public final class app/cash/redwood/protocol/guest/VersionKt {
public static final fun getGuestRedwoodVersion ()Ljava/lang/String;
}

8 changes: 6 additions & 2 deletions redwood-protocol-guest/api/jvm/redwood-protocol-guest.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge :
}

public abstract interface class app/cash/redwood/protocol/guest/ProtocolBridge$Factory {
public abstract fun create (Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
public static synthetic fun create$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
public abstract fun create-Bvskwvs (Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
public static synthetic fun create-Bvskwvs$default (Lapp/cash/redwood/protocol/guest/ProtocolBridge$Factory;Ljava/lang/String;Lkotlinx/serialization/json/Json;Lapp/cash/redwood/protocol/guest/ProtocolMismatchHandler;ILjava/lang/Object;)Lapp/cash/redwood/protocol/guest/ProtocolBridge;
}

public abstract interface class app/cash/redwood/protocol/guest/ProtocolMismatchHandler {
Expand Down Expand Up @@ -54,3 +54,7 @@ public final class app/cash/redwood/protocol/guest/ProtocolWidgetChildren : app/
public final fun visitIds (Lkotlin/jvm/functions/Function1;)V
}

public final class app/cash/redwood/protocol/guest/VersionKt {
public static final fun getGuestRedwoodVersion ()Ljava/lang/String;
}

15 changes: 15 additions & 0 deletions redwood-protocol-guest/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import app.cash.redwood.buildsupport.KmpTargets

apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'com.android.library'
apply plugin: 'com.github.gmazzo.buildconfig'

redwoodBuild {
composeCompiler()
Expand Down Expand Up @@ -52,3 +53,17 @@ android {
unitTests.returnDefaultValues = true
}
}

buildConfig {
useKotlinOutput {
topLevelConstants = true
}

className("Version")
packageName('app.cash.redwood.protocol.guest')
buildConfigField(
"app.cash.redwood.protocol.RedwoodVersion",
"guestRedwoodVersion",
"RedwoodVersion(\"${project.version}\")",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import app.cash.redwood.protocol.ChildrenTag
import app.cash.redwood.protocol.Event
import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.Id
import app.cash.redwood.protocol.RedwoodVersion
import app.cash.redwood.widget.Widget
import app.cash.redwood.widget.WidgetSystem
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -52,6 +53,7 @@ public interface ProtocolBridge : EventSink {
public interface Factory {
/** Create a new [ProtocolBridge] with its own protocol state and set of tracked widgets. */
public fun create(
hostVersion: RedwoodVersion,
json: Json = Json.Default,
mismatchHandler: ProtocolMismatchHandler = ProtocolMismatchHandler.Throwing,
): ProtocolBridge
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ class GeneratedProtocolBridgeTest {
contextual(Duration::class, DurationIsoSerializer)
}
}
val bridge = TestSchemaProtocolBridge.create(json)
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
json = json,
)
val textInput = bridge.widgetSystem.TestSchema.TextInput()

textInput.customType(10.seconds)
Expand All @@ -67,7 +71,11 @@ class GeneratedProtocolBridgeTest {
contextual(Duration::class, DurationIsoSerializer)
}
}
val bridge = TestSchemaProtocolBridge.create(json)
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
json = json,
)
val button = bridge.widgetSystem.TestSchema.Button()

button.modifier = with(object : TestScope {}) {
Expand Down Expand Up @@ -97,7 +105,11 @@ class GeneratedProtocolBridgeTest {
contextual(Duration::class, DurationIsoSerializer)
}
}
val bridge = TestSchemaProtocolBridge.create(json)
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
json = json,
)
val button = bridge.widgetSystem.TestSchema.Button()

button.modifier = with(object : TestScope {}) {
Expand Down Expand Up @@ -127,7 +139,11 @@ class GeneratedProtocolBridgeTest {
contextual(Duration::class, DurationIsoSerializer)
}
}
val bridge = TestSchemaProtocolBridge.create(json)
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
json = json,
)
val textInput = bridge.widgetSystem.TestSchema.TextInput()

val protocolWidget = textInput as ProtocolWidget
Expand All @@ -143,7 +159,10 @@ class GeneratedProtocolBridgeTest {
}

@Test fun unknownEventThrowsDefault() {
val bridge = TestSchemaProtocolBridge.create()
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
)
val button = bridge.widgetSystem.TestSchema.Button() as ProtocolWidget

val t = assertFailsWith<IllegalArgumentException> {
Expand All @@ -155,7 +174,11 @@ class GeneratedProtocolBridgeTest {

@Test fun unknownEventCallsHandler() {
val handler = RecordingProtocolMismatchHandler()
val bridge = TestSchemaProtocolBridge.create(mismatchHandler = handler)
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
mismatchHandler = handler,
)
val button = bridge.widgetSystem.TestSchema.Button() as ProtocolWidget

button.sendEvent(Event(Id(1), EventTag(3456543)))
Expand All @@ -164,7 +187,10 @@ class GeneratedProtocolBridgeTest {
}

@Test fun unknownEventNodeThrowsDefault() {
val bridge = TestSchemaProtocolBridge.create()
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
)
val t = assertFailsWith<IllegalArgumentException> {
bridge.sendEvent(Event(Id(3456543), EventTag(1)))
}
Expand All @@ -173,7 +199,11 @@ class GeneratedProtocolBridgeTest {

@Test fun unknownEventNodeCallsHandler() {
val handler = RecordingProtocolMismatchHandler()
val bridge = TestSchemaProtocolBridge.create(mismatchHandler = handler)
val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
mismatchHandler = handler,
)

bridge.sendEvent(Event(Id(3456543), EventTag(1)))

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.protocol.guest

import kotlin.test.Test
import kotlin.test.assertNotNull

class GuestVersionTest {
@Test fun parses() {
assertNotNull(guestRedwoodVersion)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.JsonPrimitive

class ProtocolTest {
private val bridge = TestSchemaProtocolBridge.create()
private val bridge = TestSchemaProtocolBridge.create(
// Use latest guest version as the host version to avoid any compatibility behavior.
hostVersion = guestRedwoodVersion,
)

@Test fun widgetVersionPropagated() = runTest {
val composition = ProtocolRedwoodComposition(
Expand Down
6 changes: 5 additions & 1 deletion redwood-protocol-host/api/android/redwood-protocol-host.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public abstract interface class app/cash/redwood/protocol/host/GeneratedProtocol
}

public final class app/cash/redwood/protocol/host/ProtocolBridge : app/cash/redwood/protocol/ChangesSink {
public fun <init> (Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;)V
public synthetic fun <init> (Ljava/lang/String;Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun sendChanges (Ljava/util/List;)V
}

Expand Down Expand Up @@ -42,3 +42,7 @@ public abstract class app/cash/redwood/protocol/host/ProtocolNode {
public abstract fun visitIds (Lkotlin/jvm/functions/Function1;)V
}

public final class app/cash/redwood/protocol/host/VersionKt {
public static final fun getHostRedwoodVersion ()Ljava/lang/String;
}

6 changes: 5 additions & 1 deletion redwood-protocol-host/api/jvm/redwood-protocol-host.api
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public abstract interface class app/cash/redwood/protocol/host/GeneratedProtocol
}

public final class app/cash/redwood/protocol/host/ProtocolBridge : app/cash/redwood/protocol/ChangesSink {
public fun <init> (Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;)V
public synthetic fun <init> (Ljava/lang/String;Lapp/cash/redwood/widget/Widget$Children;Lapp/cash/redwood/protocol/host/ProtocolFactory;Lapp/cash/redwood/protocol/EventSink;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun sendChanges (Ljava/util/List;)V
}

Expand Down Expand Up @@ -42,3 +42,7 @@ public abstract class app/cash/redwood/protocol/host/ProtocolNode {
public abstract fun visitIds (Lkotlin/jvm/functions/Function1;)V
}

public final class app/cash/redwood/protocol/host/VersionKt {
public static final fun getHostRedwoodVersion ()Ljava/lang/String;
}

15 changes: 15 additions & 0 deletions redwood-protocol-host/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import app.cash.redwood.buildsupport.KmpTargets

apply plugin: 'org.jetbrains.kotlin.multiplatform'
apply plugin: 'com.android.library'
apply plugin: 'com.github.gmazzo.buildconfig'

redwoodBuild {
publishing()
Expand Down Expand Up @@ -34,3 +35,17 @@ kotlin {
android {
namespace 'app.cash.redwood.protocol.widget'
}

buildConfig {
useKotlinOutput {
topLevelConstants = true
}

className("Version")
packageName('app.cash.redwood.protocol.host')
buildConfigField(
"app.cash.redwood.protocol.RedwoodVersion",
"hostRedwoodVersion",
"RedwoodVersion(\"${project.version}\")",
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import app.cash.redwood.protocol.EventSink
import app.cash.redwood.protocol.Id
import app.cash.redwood.protocol.ModifierChange
import app.cash.redwood.protocol.PropertyChange
import app.cash.redwood.protocol.RedwoodVersion
import app.cash.redwood.protocol.WidgetTag
import app.cash.redwood.widget.ChangeListener
import app.cash.redwood.widget.Widget
Expand All @@ -44,6 +45,8 @@ import kotlin.native.ObjCName
@OptIn(RedwoodCodegenApi::class)
@ObjCName("ProtocolBridge", exact = true)
public class ProtocolBridge<W : Any>(
@Suppress("UNUSED_PARAMETER")
guestVersion: RedwoodVersion,
container: Widget.Children<W>,
factory: ProtocolFactory<W>,
private val eventSink: EventSink,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2024 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.redwood.protocol.host

import kotlin.test.Test
import kotlin.test.assertNotNull

class HostVersionTest {
@Test fun parses() {
assertNotNull(hostRedwoodVersion)
}
}
Loading

0 comments on commit a743d06

Please sign in to comment.