Skip to content

Commit

Permalink
Allow reserving property and children tags on widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton committed Feb 15, 2024
1 parent ca0cb5a commit b7bf682
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,26 @@ private fun parseWidget(
)
}

val badReservedChildren = annotation.reservedChildren
.filterNotTo(HashSet(), HashSet<Int>()::add)
require(badReservedChildren.isEmpty()) {
"Widget ${memberType.qualifiedName} reserved children contains duplicates $badReservedChildren"
}

val reservedChildren = traits.filterIsInstance<ProtocolChildren>()
.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<ProtocolProperty>()
.groupBy(ProtocolProperty::tag)
.filterValues { it.size > 1 }
Expand All @@ -384,6 +404,26 @@ private fun parseWidget(
)
}

val badReservedProperties = annotation.reservedProperties
.filterNotTo(HashSet(), HashSet<Int>()::add)
require(badReservedProperties.isEmpty()) {
"Widget ${memberType.qualifiedName} reserved properties contains duplicates $badReservedProperties"
}

val reservedProperties = traits.filterIsInstance<ProtocolProperty>()
.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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IllegalArgumentException>()
.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<IllegalArgumentException>()
.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<IllegalArgumentException>()
.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<IllegalArgumentException>()
.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 = [
Expand Down

0 comments on commit b7bf682

Please sign in to comment.