Skip to content

Commit

Permalink
Allow reserving widget and modifier tags
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeWharton committed Feb 14, 2024
1 parent 7ad1b0f commit ca0cb5a
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import kotlin.reflect.KClass
* Annotates an otherwise unused type with a set of [Widget]-annotated or [Modifier]-annotated
* classes which are all part of this schema.
*
* ```
* ```kotlin
* @Schema([
* Row::class,
* RowAlignment::class,
Expand All @@ -42,6 +42,44 @@ import kotlin.reflect.KClass
public annotation class Schema(
val members: Array<KClass<*>>,
val dependencies: Array<Dependency> = [],
/**
* Widget tags which are reserved. These cannot be used by a widget in [members].
* This is useful for ensuring tags from old, retired widgets are not accidentally reused.
*
* ```kotlin
* @Schema(
* members = [
* Row::class,
* Button::class,
* Text::class,
* ],
* reservedWidgets = [
* 4, // Retired Column widget.
* ],
* )
* interface MySchema
* ```
*/
val reservedWidgets: IntArray = [],
/**
* Modifier tags which are reserved. These cannot be used by a modifier in [members].
* This is useful for ensuring tags from old, retired modifiers are not accidentally reused.
*
* ```kotlin
* @Schema(
* members = [
* Row::class,
* Button::class,
* Text::class,
* ],
* reservedModifiers = [
* 3, // Retired RowAlignment modifier.
* ],
* )
* interface MySchema
* ```
*/
val reservedModifiers: IntArray = [],
) {
@Retention(RUNTIME)
@Target // None, use only within @Schema.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ internal fun parseProtocolSchemaSet(schemaType: KClass<*>): ProtocolSchemaSet {
}

val widgets = mutableListOf<ParsedProtocolWidget>()
val modifier = mutableListOf<ParsedProtocolModifier>()
val modifiers = mutableListOf<ParsedProtocolModifier>()
for (memberType in memberTypes) {
val widgetAnnotation = memberType.findAnnotation<WidgetAnnotation>()
val modifierAnnotation = memberType.findAnnotation<ModifierAnnotation>()
Expand All @@ -118,7 +118,7 @@ internal fun parseProtocolSchemaSet(schemaType: KClass<*>): ProtocolSchemaSet {
} else if (widgetAnnotation != null) {
widgets += parseWidget(memberType, widgetAnnotation)
} else if (modifierAnnotation != null) {
modifier += parseModifier(memberType, modifierAnnotation)
modifiers += parseModifier(memberType, modifierAnnotation)
} else {
throw AssertionError()
}
Expand All @@ -137,7 +137,26 @@ internal fun parseProtocolSchemaSet(schemaType: KClass<*>): ProtocolSchemaSet {
)
}

val badModifiers = modifier.groupBy(ProtocolModifier::tag).filterValues { it.size > 1 }
val badReservedWidgets = schemaAnnotation.reservedWidgets
.filterNotTo(HashSet(), HashSet<Int>()::add)
require(badReservedWidgets.isEmpty()) {
"Schema reserved widgets contains duplicates $badReservedWidgets"
}

val reservedWidgets = widgets.filter { it.tag in schemaAnnotation.reservedWidgets }
if (reservedWidgets.isNotEmpty()) {
throw IllegalArgumentException(
buildString {
append("Schema @Widget tags must not be included in reserved set ")
appendLine(schemaAnnotation.reservedWidgets.contentToString())
for (widget in reservedWidgets) {
append("\n- @Widget(${widget.tag}) ${widget.type}")
}
},
)
}

val badModifiers = modifiers.groupBy(ProtocolModifier::tag).filterValues { it.size > 1 }
if (badModifiers.isNotEmpty()) {
throw IllegalArgumentException(
buildString {
Expand All @@ -150,11 +169,30 @@ internal fun parseProtocolSchemaSet(schemaType: KClass<*>): ProtocolSchemaSet {
)
}

val badReservedModifiers = schemaAnnotation.reservedModifiers
.filterNotTo(HashSet(), HashSet<Int>()::add)
require(badReservedModifiers.isEmpty()) {
"Schema reserved modifiers contains duplicates $badReservedModifiers"
}

val reservedModifiers = modifiers.filter { it.tag in schemaAnnotation.reservedModifiers }
if (reservedModifiers.isNotEmpty()) {
throw IllegalArgumentException(
buildString {
append("Schema @Modifier tags must not be included in reserved set ")
appendLine(schemaAnnotation.reservedModifiers.contentToString())
for (widget in reservedModifiers) {
append("\n- @Modifier(${widget.tag}, …) ${widget.type}")
}
},
)
}

val widgetScopes = widgets
.flatMap { it.traits }
.filterIsInstance<Widget.Children>()
.mapNotNull { it.scope }
val modifierScopes = modifier
val modifierScopes = modifiers
.flatMap { it.scopes }
val scopes = buildSet {
addAll(widgetScopes)
Expand Down Expand Up @@ -210,7 +248,7 @@ internal fun parseProtocolSchemaSet(schemaType: KClass<*>): ProtocolSchemaSet {
type = schemaType.toFqType(),
scopes = scopes.toList(),
widgets = widgets,
modifiers = modifier,
modifiers = modifiers,
taggedDependencies = dependencies.mapValues { (_, schema) -> schema.type },
)
val schemaSet = ParsedProtocolSchemaSet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import assertk.assertions.contains
import assertk.assertions.containsExactly
import assertk.assertions.containsMatch
import assertk.assertions.hasMessage
import assertk.assertions.hasSize
import assertk.assertions.isEmpty
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
Expand Down Expand Up @@ -624,6 +625,100 @@ class SchemaParserTest(
assertThat(modifier.tag).isEqualTo(1)
}

@Schema(
members = [],
reservedWidgets = [1, 2, 3, 1, 3, 1],
)
interface SchemaDuplicateReservedWidgets

@Test fun schemaDuplicateReservedWidgetsFails() {
assertFailure {
parser.parse(SchemaDuplicateReservedWidgets::class)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Schema reserved widgets contains duplicates [1, 3]")
}

@Schema(
members = [],
reservedModifiers = [1, 2, 3, 1, 3, 1],
)
interface SchemaDuplicateReservedModifiers

@Test fun schemaDuplicateReservedModifiersFails() {
assertFailure {
parser.parse(SchemaDuplicateReservedModifiers::class)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage("Schema reserved modifiers contains duplicates [1, 3]")
}

@Schema(
members = [ReservedWidget::class],
reservedWidgets = [1, 2, 3],
)
interface SchemaReservedWidgetCollision

@Widget(2)
object ReservedWidget

@Test fun schemaReservedWidgetCollision() {
assertFailure {
parser.parse(SchemaReservedWidgetCollision::class)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage(
"""
|Schema @Widget tags must not be included in reserved set [1, 2, 3]
|
|- @Widget(2) app.cash.redwood.tooling.schema.SchemaParserTest.ReservedWidget
""".trimMargin(),
)
}

@Schema(
members = [ReservedModifier::class],
reservedModifiers = [1, 2, 3],
)
interface SchemaReservedModifierCollision

@Modifier(2, TestScope::class)
object ReservedModifier

@Test fun schemaReservedModifierCollision() {
assertFailure {
parser.parse(SchemaReservedModifierCollision::class)
}.isInstanceOf<IllegalArgumentException>()
.hasMessage(
"""
|Schema @Modifier tags must not be included in reserved set [1, 2, 3]
|
|- @Modifier(2, …) app.cash.redwood.tooling.schema.SchemaParserTest.ReservedModifier
""".trimMargin(),
)
}

@Schema(
members = [ReservedModifier::class],
reservedWidgets = [1, 2, 3],
)
interface SchemaReservedWidgetDoesNotApplyToModifier

@Test fun schemaReservedWidgetDoesNotApplyToModifier() {
val schema = parser.parse(SchemaReservedWidgetDoesNotApplyToModifier::class).schema
assertThat(schema.widgets).hasSize(0)
assertThat(schema.modifiers).hasSize(1)
}

@Schema(
members = [ReservedWidget::class],
reservedModifiers = [1, 2, 3],
)
interface SchemaReservedModifierDoesNotApplyToWidget

@Test fun schemaReservedModifierDoesNotApplyToWidget() {
val schema = parser.parse(SchemaReservedModifierDoesNotApplyToWidget::class).schema
assertThat(schema.widgets).hasSize(1)
assertThat(schema.modifiers).hasSize(0)
}

@Schema(
members = [],
dependencies = [
Expand Down

0 comments on commit ca0cb5a

Please sign in to comment.