Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow reserving widget and modifier tags #1802

Merged
merged 1 commit into from
Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading