Skip to content

Commit 93938e5

Browse files
committed
publish event documentation
1 parent ae09457 commit 93938e5

File tree

10 files changed

+225
-9
lines changed

10 files changed

+225
-9
lines changed

buildSrc/src/main/kotlin/documentation/model/model.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ data class Application(
1616
override val distanceFromUs: Distance?,
1717
val dependents: List<Dependent> = emptyList(),
1818
val dependencies: List<Dependency> = emptyList(),
19+
val events: List<Event> = emptyList(),
1920
) : Component
2021

2122
data class Dependent(
@@ -37,3 +38,18 @@ enum class Distance { OWNED, CLOSE, DISTANT }
3738
enum class Credentials { JWT, BASIC_AUTH }
3839

3940
data class HttpEndpoint(val method: String, val path: String)
41+
42+
data class Event(
43+
val name: String,
44+
val type: String,
45+
val description: String,
46+
val example: String,
47+
val fields: List<Field>,
48+
) {
49+
data class Field(
50+
val property: String,
51+
val type: String,
52+
val nullable: Boolean,
53+
val description: String?,
54+
)
55+
}

buildSrc/src/main/kotlin/documentation/tasks.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
88
import documentation.model.Application
99
import documentation.model.Dependency
1010
import documentation.model.Dependent
11+
import documentation.model.Event
1112
import documentation.model.HttpEndpoint
1213
import java.io.File
1314
import kotlin.reflect.KClass
@@ -22,8 +23,10 @@ fun generateApplicationDescription(sourceFolder: File, targetFolder: File, appli
2223
val baseApplicationDescription = loadBaseApplicationDescription(sourceFolder, applicationId)
2324
val dependents = loadDependents(sourceFolder)
2425
val dependencies = loadDependencies(sourceFolder)
26+
val events = loadEvents(sourceFolder)
2527

26-
val applicationDescription = baseApplicationDescription.copy(dependents = dependents, dependencies = dependencies)
28+
val applicationDescription = baseApplicationDescription
29+
.copy(dependents = dependents, dependencies = dependencies, events = events)
2730

2831
val file = File(targetFolder, applicationDescription.id + ".json")
2932
objectMapper.writeValue(file, applicationDescription)
@@ -62,6 +65,14 @@ private fun loadDependency(file: File): Dependency {
6265
return dependency.copy(httpEndpoints = httpEndpoints)
6366
}
6467

68+
private fun loadEvents(sourceFolder: File): List<Event> =
69+
listJsonFilesInFolder(File(sourceFolder, "events"))
70+
.map { file -> loadEvent(file) }
71+
72+
private fun loadEvent(file: File): Event {
73+
return objectMapper.readValue<Event>(file)
74+
}
75+
6576
private fun listJsonFilesInFolder(folder: File): List<File> =
6677
if (folder.isDirectory) {
6778
folder.listFiles()!!

src/main/kotlin/application/api/OrdersController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class OrdersController {
2020

2121
@PostMapping
2222
@ResponseStatus(CREATED)
23-
fun create(@RequestBody request: CreationRequest): OrderRepresentation {
23+
fun place(@RequestBody request: CreationRequest): OrderRepresentation {
2424
TODO("not implemented")
2525
}
2626

src/main/kotlin/application/business/events.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@ package application.business
33
import java.time.Instant
44
import java.util.UUID
55

6-
interface OrderEvent {
6+
interface Event {
77
val id: UUID
88
val timestamp: Instant
9-
val order: OrderData
10-
9+
fun getEventName(): String
1110
fun getEventType(): String
1211
}
1312

13+
interface OrderEvent : Event {
14+
val order: OrderData
15+
}
16+
1417
data class OrderData(
1518
val orderId: UUID,
1619
val customerId: UUID,
17-
val orderDate: Instant,
18-
val status: String,
20+
val status: OrderStatus,
1921
// etc ..
2022
)
2123

@@ -24,6 +26,7 @@ data class OrderPlaced(
2426
override val timestamp: Instant,
2527
override val order: OrderData,
2628
) : OrderEvent {
29+
override fun getEventName() = "Order Placed"
2730
override fun getEventType() = "orders.placed"
2831
}
2932

@@ -32,5 +35,6 @@ data class OrderCanceled(
3235
override val timestamp: Instant,
3336
override val order: OrderData,
3437
) : OrderEvent {
38+
override fun getEventName() = "Order Canceled"
3539
override fun getEventType() = "orders.canceled"
3640
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package application.business
2+
3+
enum class OrderStatus {
4+
PLACED, PROCESSING, COMPLETED, CANCELLED;
5+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package application.config
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
4+
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES
5+
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES
6+
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
7+
import com.fasterxml.jackson.databind.ObjectMapper
8+
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
9+
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS
10+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
11+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
12+
13+
/**
14+
* Produces an [ObjectMapper] intended for usage when (de)serializing events.
15+
*/
16+
fun objectMapperForEvents(): ObjectMapper = jacksonObjectMapper()
17+
.registerModule(JavaTimeModule())
18+
.disable(FAIL_ON_UNKNOWN_PROPERTIES)
19+
.disable(FAIL_ON_IGNORED_PROPERTIES)
20+
.disable(WRITE_DATES_AS_TIMESTAMPS)
21+
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
22+
.enable(FAIL_ON_NULL_FOR_PRIMITIVES)
23+
.setDefaultPropertyInclusion(NON_NULL)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package application.business
2+
3+
import application.business.OrderStatus.PLACED
4+
import application.business.OrderStatus.PROCESSING
5+
import application.documentation.ArchitectureDocumentation.createOrReplaceEvent
6+
import application.documentation.EventDescriptionSpec
7+
import application.documentation.eventDescription
8+
import org.junit.jupiter.api.Test
9+
import java.time.Instant.parse
10+
import java.util.UUID.fromString
11+
12+
class OrderEventTests {
13+
14+
@Test
15+
fun `OrderPlaced event`() {
16+
val description = eventDescription(
17+
example = OrderPlaced(
18+
id = fromString("3d6fd447-a311-4028-8248-356e3621d450"),
19+
timestamp = parse("2024-07-22T12:34:56.789Z"),
20+
order = OrderData(
21+
orderId = fromString("a64914f7-7404-4e85-8e1a-778068fae307"),
22+
customerId = fromString("ed2a43d7-e49b-408d-8b5f-e2e2305954c2"),
23+
status = PLACED
24+
)
25+
),
26+
description = "Emitted whenever a new order is placed.",
27+
fields = {
28+
orderDataFields("order")
29+
}
30+
)
31+
createOrReplaceEvent(description)
32+
}
33+
34+
@Test
35+
fun `OrderCanceled event`() {
36+
val description = eventDescription(
37+
example = OrderCanceled(
38+
id = fromString("4c97c099-0c00-4e56-841d-fbfe81770936"),
39+
timestamp = parse("2024-07-22T12:34:56.789Z"),
40+
order = OrderData(
41+
orderId = fromString("a64914f7-7404-4e85-8e1a-778068fae307"),
42+
customerId = fromString("ed2a43d7-e49b-408d-8b5f-e2e2305954c2"),
43+
status = PROCESSING
44+
)
45+
),
46+
description = "Emitted whenever an order is canceled.",
47+
fields = {
48+
orderDataFields("order")
49+
}
50+
)
51+
createOrReplaceEvent(description)
52+
}
53+
54+
private fun EventDescriptionSpec.orderDataFields(objectName: String) {
55+
field(
56+
property = objectName,
57+
type = "Object",
58+
)
59+
field(
60+
property = "$objectName.orderId",
61+
type = "UUID4",
62+
description = "The ID of the order."
63+
)
64+
field(
65+
property = "$objectName.customerId",
66+
type = "UUID4",
67+
description = "The ID of the customer that placed the order."
68+
)
69+
field(
70+
property = "$objectName.status",
71+
type = "Enumeration",
72+
description = "The status of the order. Might have one of the following values: "
73+
+ OrderStatus.entries.joinToString()
74+
)
75+
}
76+
}

src/test/kotlin/application/documentation/ArchitectureDocumentation.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,25 @@ object ArchitectureDocumentation {
5656
}
5757
}
5858

59+
fun createOrReplaceEvent(description: EventDescription) {
60+
val folder = File(rootFolder, "events")
61+
val file = File(folder, description.type + ".json")
62+
63+
createOrReplaceFile(file) {
64+
write(toJsonString(description))
65+
}
66+
}
67+
5968
private fun toJsonString(value: Any): String =
6069
objectMapper.writeValueAsString(value)
6170

62-
6371
private fun createOrAppendFile(file: File, writer: BufferedWriter.() -> Unit) {
6472
file.parentFile.mkdirs()
6573
FileOutputStream(file, true).use { it.bufferedWriter().use(writer) }
6674
}
6775

6876
private fun createOrReplaceFile(file: File, writer: BufferedWriter.() -> Unit) {
6977
file.parentFile.mkdirs()
70-
FileOutputStream(file, true).use { it.bufferedWriter().use(writer) }
78+
FileOutputStream(file, false).use { it.bufferedWriter().use(writer) }
7179
}
7280
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package application.documentation
2+
3+
import application.business.Event
4+
import application.config.objectMapperForEvents
5+
import com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT
6+
7+
private val eventObjectMapper = objectMapperForEvents()
8+
.enable(INDENT_OUTPUT) // pretty format the JSON examples
9+
10+
fun eventDescription(example: Event, description: String, fields: EventDescriptionSpec.() -> Unit): EventDescription =
11+
EventDescriptionSpec(example, description).apply(fields).build()
12+
13+
class EventDescriptionSpec(
14+
private val example: Event,
15+
private val description: String
16+
) {
17+
private val fields = mutableListOf<EventDescription.Field>()
18+
19+
init {
20+
field(
21+
property = "id",
22+
type = "UUID4",
23+
nullable = false,
24+
description = "The unique ID of the event."
25+
)
26+
field(
27+
property = "timestamp",
28+
type = "ISO-8601 Date+Time (UTC)",
29+
nullable = false,
30+
description = "The exact instant the event occurred at its source."
31+
)
32+
}
33+
34+
fun field(
35+
property: String,
36+
type: String,
37+
nullable: Boolean = false,
38+
description: String? = null
39+
) {
40+
check(fields.none { it.property == property }) { "Field '$property' is already documented." }
41+
fields.add(
42+
EventDescription.Field(
43+
property = property,
44+
type = type,
45+
nullable = nullable,
46+
description = description,
47+
)
48+
)
49+
}
50+
51+
internal fun build() = EventDescription(
52+
name = example.getEventName(),
53+
type = example.getEventType(),
54+
description = description,
55+
example = eventObjectMapper.writeValueAsString(example),
56+
fields = fields
57+
)
58+
}

src/test/kotlin/application/documentation/model.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,18 @@ enum class Distance { OWNED, CLOSE, EXTERNAL }
2424
enum class Credentials { JWT, BASIC_AUTH }
2525

2626
data class HttpEndpoint(val method: String, val path: String)
27+
28+
data class EventDescription(
29+
val name: String,
30+
val type: String,
31+
val description: String,
32+
val example: String,
33+
val fields: List<Field>,
34+
) {
35+
data class Field(
36+
val property: String,
37+
val type: String,
38+
val nullable: Boolean,
39+
val description: String?,
40+
)
41+
}

0 commit comments

Comments
 (0)