From b7bf682b8d43c06041c0f04185ab3f9ff62be373 Mon Sep 17 00:00:00 2001 From: Jake Wharton Date: Thu, 15 Feb 2024 11:31:18 -0500 Subject: [PATCH] Allow reserving property and children tags on widgets --- .../app/cash/redwood/schema/annotations.kt | 34 ++++- .../redwood/tooling/schema/schemaParser.kt | 40 ++++++ .../tooling/schema/SchemaParserTest.kt | 120 ++++++++++++++++++ 3 files changed, 193 insertions(+), 1 deletion(-) diff --git a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/annotations.kt b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/annotations.kt index 0f8c1ab162..eb5b105016 100644 --- a/redwood-schema/src/main/kotlin/app/cash/redwood/schema/annotations.kt +++ b/redwood-schema/src/main/kotlin/app/cash/redwood/schema/annotations.kt @@ -105,7 +105,39 @@ public annotation class Schema( */ @Retention(RUNTIME) @Target(CLASS) -public annotation class Widget(val tag: Int) +public annotation class Widget( + val tag: Int, + /** + * Property tags which are reserved. These cannot be used by a [@Property][Property] annotation. + * This is useful for ensuring tags from old, retired properties are not accidentally reused. + * + * ```kotlin + * @Widget( + * tag = 12, + * reservedProperties = [ + * 3, // Retired double-click event. + * ], + * ) + * data class MyButton(…) + * ``` + */ + val reservedProperties: IntArray = [], + /** + * Children tags which are reserved. These cannot be used by a [@Children][Children] annotation. + * This is useful for ensuring tags from old, retired children are not accidentally reused. + * + * ```kotlin + * @Widget( + * tag = 12, + * reservedChildren = [ + * 2, // Retired action item slot. + * ], + * ) + * data class Toolbar(…) + * ``` + */ + val reservedChildren: IntArray = [], +) /** * Annotates a [Widget] property which represents a property on the associated UI widget. Properties diff --git a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt index 3501ff6859..5949700d4b 100644 --- a/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt +++ b/redwood-tooling-schema/src/main/kotlin/app/cash/redwood/tooling/schema/schemaParser.kt @@ -369,6 +369,26 @@ private fun parseWidget( ) } + val badReservedChildren = annotation.reservedChildren + .filterNotTo(HashSet(), HashSet()::add) + require(badReservedChildren.isEmpty()) { + "Widget ${memberType.qualifiedName} reserved children contains duplicates $badReservedChildren" + } + + val reservedChildren = traits.filterIsInstance() + .filter { it.tag in annotation.reservedChildren } + if (reservedChildren.isNotEmpty()) { + throw IllegalArgumentException( + buildString { + append("Widget ${memberType.qualifiedName} @Children tags must not be included in reserved set ") + appendLine(annotation.reservedChildren.contentToString()) + for (children in reservedChildren) { + append("\n- @Children(${children.tag}) ${children.name}") + } + }, + ) + } + val badProperties = traits.filterIsInstance() .groupBy(ProtocolProperty::tag) .filterValues { it.size > 1 } @@ -384,6 +404,26 @@ private fun parseWidget( ) } + val badReservedProperties = annotation.reservedProperties + .filterNotTo(HashSet(), HashSet()::add) + require(badReservedProperties.isEmpty()) { + "Widget ${memberType.qualifiedName} reserved properties contains duplicates $badReservedProperties" + } + + val reservedProperties = traits.filterIsInstance() + .filter { it.tag in annotation.reservedProperties } + if (reservedProperties.isNotEmpty()) { + throw IllegalArgumentException( + buildString { + append("Widget ${memberType.qualifiedName} @Property tags must not be included in reserved set ") + appendLine(annotation.reservedProperties.contentToString()) + for (children in reservedProperties) { + append("\n- @Property(${children.tag}) ${children.name}") + } + }, + ) + } + return ParsedProtocolWidget( tag = tag, type = memberFqType, diff --git a/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt b/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt index c4acac3392..ec86687fed 100644 --- a/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt +++ b/redwood-tooling-schema/src/test/kotlin/app/cash/redwood/tooling/schema/SchemaParserTest.kt @@ -719,6 +719,126 @@ class SchemaParserTest( assertThat(schema.modifiers).hasSize(0) } + @Schema( + [ + WidgetDuplicateReservedProperties::class, + ], + ) + interface SchemaDuplicateReservedProperties + + @Widget(1, reservedProperties = [1, 2, 3, 1, 3, 1]) + object WidgetDuplicateReservedProperties + + @Test fun widgetDuplicateReservedProperties() { + assertFailure { + parser.parse(SchemaDuplicateReservedProperties::class) + }.isInstanceOf() + .hasMessage("Widget app.cash.redwood.tooling.schema.SchemaParserTest.WidgetDuplicateReservedProperties reserved properties contains duplicates [1, 3]") + } + + @Schema( + [ + WidgetDuplicateReservedChildren::class, + ], + ) + interface SchemaDuplicateReservedChildren + + @Widget(1, reservedChildren = [1, 2, 3, 1, 3, 1]) + object WidgetDuplicateReservedChildren + + @Test fun widgetDuplicateReservedChildren() { + assertFailure { + parser.parse(SchemaDuplicateReservedChildren::class) + }.isInstanceOf() + .hasMessage("Widget app.cash.redwood.tooling.schema.SchemaParserTest.WidgetDuplicateReservedChildren reserved children contains duplicates [1, 3]") + } + + @Schema( + [ + WidgetReservedPropertyCollision::class, + ], + ) + interface SchemaReservedPropertyCollision + + @Widget(1, reservedProperties = [2]) + data class WidgetReservedPropertyCollision( + @Property(1) val text: String, + @Property(2) val color: Int, + ) + + @Test fun widgetReservedPropertyCollision() { + assertFailure { + parser.parse(SchemaReservedPropertyCollision::class) + }.isInstanceOf() + .hasMessage( + """ + |Widget app.cash.redwood.tooling.schema.SchemaParserTest.WidgetReservedPropertyCollision @Property tags must not be included in reserved set [2] + | + |- @Property(2) color + """.trimMargin(), + ) + } + + @Schema( + [ + WidgetReservedChildrenCollision::class, + ], + ) + interface SchemaReservedChildrenCollision + + @Widget(1, reservedChildren = [2]) + data class WidgetReservedChildrenCollision( + @Children(1) val left: () -> Unit, + @Children(2) val right: () -> Unit, + ) + + @Test fun widgetReservedChildrenCollision() { + assertFailure { + parser.parse(SchemaReservedChildrenCollision::class) + }.isInstanceOf() + .hasMessage( + """ + |Widget app.cash.redwood.tooling.schema.SchemaParserTest.WidgetReservedChildrenCollision @Children tags must not be included in reserved set [2] + | + |- @Children(2) right + """.trimMargin(), + ) + } + + @Schema( + [ + WidgetReservedPropertyDoesNotApplyToChildren::class, + ], + ) + interface SchemaReservedPropertyDoesNotApplyToChildren + + @Widget(1, reservedProperties = [1]) + data class WidgetReservedPropertyDoesNotApplyToChildren( + @Children(1) val content: () -> Unit, + ) + + @Test fun widgetReservedPropertyDoesNotApplyToChildren() { + val schema = parser.parse(SchemaReservedPropertyDoesNotApplyToChildren::class).schema + assertThat(schema.widgets.single().traits).hasSize(1) + } + + @Schema( + [ + WidgetReservedChildrenDoesNotApplyToProperty::class, + ], + ) + interface SchemaReservedChildrenDoesNotApplyToProperty + + @Widget(1, reservedChildren = [1]) + data class WidgetReservedChildrenDoesNotApplyToProperty( + @Property(1) val text: String, + ) + + @Test fun widgetReservedChildrenDoesNotApplyToProperty() { + val schema = parser.parse(SchemaReservedChildrenDoesNotApplyToProperty::class).schema + assertThat(schema.widgets.single().traits).hasSize(1) + } + @Schema( members = [], dependencies = [