Skip to content

Commit 15413d5

Browse files
authored
feat(analytics-events): make events.toml writable without Kotlin (#1547)
1 parent ed880ef commit 15413d5

11 files changed

Lines changed: 1905 additions & 634 deletions

File tree

‎build-logic/convention/src/main/kotlin/AnalyticsCatalogueConventionPlugin.kt‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import com.getcode.buildlogic.analytics.CheckAnalyticsEventsPage
12
import com.getcode.buildlogic.analytics.GenerateAnalyticsEvents
3+
import com.getcode.buildlogic.analytics.WriteAnalyticsEventsPage
24
import org.gradle.api.Plugin
35
import org.gradle.api.Project
46
import org.gradle.kotlin.dsl.configure
@@ -21,6 +23,9 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
2123
* every compilation, including the Kotlin/Native ones the XCFramework build links. Lint reads
2224
* source directories straight off disk, so it is made to depend on the task explicitly, as in
2325
* [KmpTestFixturesConventionPlugin].
26+
*
27+
* `EVENTS.md`, the catalogue as a readable page, is checked in next to `events.toml`.
28+
* `writeAnalyticsEventsPage` rewrites it; `checkAnalyticsEventsPage` fails when it is stale.
2429
*/
2530
class AnalyticsCatalogueConventionPlugin : Plugin<Project> {
2631
override fun apply(target: Project) {
@@ -30,6 +35,18 @@ class AnalyticsCatalogueConventionPlugin : Plugin<Project> {
3035
outputDirectory.set(layout.buildDirectory.dir("generated/analytics/commonMain/kotlin"))
3136
}
3237

38+
val catalogueFile = layout.projectDirectory.file("events.toml")
39+
val pageFile = layout.projectDirectory.file("EVENTS.md")
40+
tasks.register<WriteAnalyticsEventsPage>("writeAnalyticsEventsPage") {
41+
catalogue.set(catalogueFile)
42+
page.set(pageFile)
43+
}
44+
tasks.register<CheckAnalyticsEventsPage>("checkAnalyticsEventsPage") {
45+
catalogue.set(catalogueFile)
46+
page.set(pageFile)
47+
fixCommand.set("./gradlew ${path.substringBeforeLast(':')}:writeAnalyticsEventsPage")
48+
}
49+
3350
tasks.matching { it.name.startsWith("lint") || it.name.endsWith("LintModel") }
3451
.configureEach { dependsOn(generateAnalyticsEvents) }
3552

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.getcode.buildlogic.analytics
22

3-
/** Reads `events.toml` and writes the builders and value enums it describes. */
3+
/** Reads `events.toml` and writes the builders and value enums it describes, and `EVENTS.md`. */
44
internal object AnalyticsCatalogue {
55
fun parse(text: String): Catalogue = CatalogueParser.parse(text)
66

77
/** The generated sources, keyed by path relative to the source root. */
88
fun emit(catalogue: Catalogue): Map<String, String> = KotlinEmitter.emit(catalogue)
9+
10+
/** `EVENTS.md`: the catalogue as a readable page. */
11+
fun document(catalogue: Catalogue): String = MarkdownEmitter.emit(catalogue)
912
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.getcode.buildlogic.analytics
2+
3+
import org.gradle.api.DefaultTask
4+
import org.gradle.api.GradleException
5+
import org.gradle.api.file.RegularFileProperty
6+
import org.gradle.api.provider.Property
7+
import org.gradle.api.tasks.Input
8+
import org.gradle.api.tasks.InputFile
9+
import org.gradle.api.tasks.Internal
10+
import org.gradle.api.tasks.OutputFile
11+
import org.gradle.api.tasks.PathSensitive
12+
import org.gradle.api.tasks.PathSensitivity
13+
import org.gradle.api.tasks.TaskAction
14+
15+
private fun page(catalogue: RegularFileProperty): String = try {
16+
AnalyticsCatalogue.document(AnalyticsCatalogue.parse(catalogue.get().asFile.readText()))
17+
} catch (malformed: CatalogueException) {
18+
throw GradleException(malformed.message.orEmpty(), malformed)
19+
}
20+
21+
/** Writes `EVENTS.md` from `events.toml`. The page is checked in, so this writes into the module. */
22+
abstract class WriteAnalyticsEventsPage : DefaultTask() {
23+
24+
@get:InputFile
25+
@get:PathSensitive(PathSensitivity.NONE)
26+
abstract val catalogue: RegularFileProperty
27+
28+
@get:OutputFile
29+
abstract val page: RegularFileProperty
30+
31+
@TaskAction
32+
fun write() = page.get().asFile.writeText(page(catalogue))
33+
}
34+
35+
/** Fails when the checked-in `EVENTS.md` is not what `events.toml` generates. */
36+
abstract class CheckAnalyticsEventsPage : DefaultTask() {
37+
38+
@get:InputFile
39+
@get:PathSensitive(PathSensitivity.NONE)
40+
abstract val catalogue: RegularFileProperty
41+
42+
/** Read only if it exists: a missing page is reported, not a missing-input failure. */
43+
@get:Internal
44+
abstract val page: RegularFileProperty
45+
46+
/** The command the failure tells the reader to run. */
47+
@get:Input
48+
abstract val fixCommand: Property<String>
49+
50+
@TaskAction
51+
fun check() {
52+
val file = page.get().asFile
53+
val expected = page(catalogue)
54+
if (!file.exists() || file.readText() != expected) {
55+
throw GradleException(
56+
"${file.name} is out of date with events.toml. Run `${fixCommand.get()}` and commit the result.",
57+
)
58+
}
59+
}
60+
}

‎build-logic/convention/src/main/kotlin/com/getcode/buildlogic/analytics/Catalogue.kt‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ internal data class Catalogue(
1111
val domains: List<Domain>,
1212
)
1313

14-
/** A value enum: a Kotlin enum whose cases carry the string sent to Mixpanel. */
14+
/** A value list: a Kotlin enum whose cases carry the string sent to Mixpanel. */
1515
internal data class EnumDef(val name: String, val doc: String?, val cases: List<EnumCase>)
1616

1717
internal data class EnumCase(val name: String, val value: String, val drift: String?)
1818

19-
/** One generated `object`, e.g. `ChatEvents`. */
20-
internal data class Domain(val name: String, val doc: String?, val builders: List<Builder>)
19+
/** One generated `object`, e.g. `ChatEvents`, named from the [title] entries give, e.g. `Chat`. */
20+
internal data class Domain(val name: String, val title: String, val doc: String?, val builders: List<Builder>)
2121

2222
internal data class Builder(
2323
val name: String,

0 commit comments

Comments
 (0)