Skip to content

Commit

Permalink
publish event documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
slu-it committed Jul 22, 2024
1 parent ae09457 commit 93938e5
Show file tree
Hide file tree
Showing 10 changed files with 225 additions and 9 deletions.
16 changes: 16 additions & 0 deletions buildSrc/src/main/kotlin/documentation/model/model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ data class Application(
override val distanceFromUs: Distance?,
val dependents: List<Dependent> = emptyList(),
val dependencies: List<Dependency> = emptyList(),
val events: List<Event> = emptyList(),
) : Component

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

data class HttpEndpoint(val method: String, val path: String)

data class Event(
val name: String,
val type: String,
val description: String,
val example: String,
val fields: List<Field>,
) {
data class Field(
val property: String,
val type: String,
val nullable: Boolean,
val description: String?,
)
}
13 changes: 12 additions & 1 deletion buildSrc/src/main/kotlin/documentation/tasks.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import documentation.model.Application
import documentation.model.Dependency
import documentation.model.Dependent
import documentation.model.Event
import documentation.model.HttpEndpoint
import java.io.File
import kotlin.reflect.KClass
Expand All @@ -22,8 +23,10 @@ fun generateApplicationDescription(sourceFolder: File, targetFolder: File, appli
val baseApplicationDescription = loadBaseApplicationDescription(sourceFolder, applicationId)
val dependents = loadDependents(sourceFolder)
val dependencies = loadDependencies(sourceFolder)
val events = loadEvents(sourceFolder)

val applicationDescription = baseApplicationDescription.copy(dependents = dependents, dependencies = dependencies)
val applicationDescription = baseApplicationDescription
.copy(dependents = dependents, dependencies = dependencies, events = events)

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

private fun loadEvents(sourceFolder: File): List<Event> =
listJsonFilesInFolder(File(sourceFolder, "events"))
.map { file -> loadEvent(file) }

private fun loadEvent(file: File): Event {
return objectMapper.readValue<Event>(file)
}

private fun listJsonFilesInFolder(folder: File): List<File> =
if (folder.isDirectory) {
folder.listFiles()!!
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/application/api/OrdersController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class OrdersController {

@PostMapping
@ResponseStatus(CREATED)
fun create(@RequestBody request: CreationRequest): OrderRepresentation {
fun place(@RequestBody request: CreationRequest): OrderRepresentation {
TODO("not implemented")
}

Expand Down
14 changes: 9 additions & 5 deletions src/main/kotlin/application/business/events.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@ package application.business
import java.time.Instant
import java.util.UUID

interface OrderEvent {
interface Event {
val id: UUID
val timestamp: Instant
val order: OrderData

fun getEventName(): String
fun getEventType(): String
}

interface OrderEvent : Event {
val order: OrderData
}

data class OrderData(
val orderId: UUID,
val customerId: UUID,
val orderDate: Instant,
val status: String,
val status: OrderStatus,
// etc ..
)

Expand All @@ -24,6 +26,7 @@ data class OrderPlaced(
override val timestamp: Instant,
override val order: OrderData,
) : OrderEvent {
override fun getEventName() = "Order Placed"
override fun getEventType() = "orders.placed"
}

Expand All @@ -32,5 +35,6 @@ data class OrderCanceled(
override val timestamp: Instant,
override val order: OrderData,
) : OrderEvent {
override fun getEventName() = "Order Canceled"
override fun getEventType() = "orders.canceled"
}
5 changes: 5 additions & 0 deletions src/main/kotlin/application/business/types.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package application.business

enum class OrderStatus {
PLACED, PROCESSING, COMPLETED, CANCELLED;
}
23 changes: 23 additions & 0 deletions src/main/kotlin/application/config/serialization.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package application.config

import com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES
import com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS
import com.fasterxml.jackson.databind.SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper

/**
* Produces an [ObjectMapper] intended for usage when (de)serializing events.
*/
fun objectMapperForEvents(): ObjectMapper = jacksonObjectMapper()
.registerModule(JavaTimeModule())
.disable(FAIL_ON_UNKNOWN_PROPERTIES)
.disable(FAIL_ON_IGNORED_PROPERTIES)
.disable(WRITE_DATES_AS_TIMESTAMPS)
.disable(WRITE_DURATIONS_AS_TIMESTAMPS)
.enable(FAIL_ON_NULL_FOR_PRIMITIVES)
.setDefaultPropertyInclusion(NON_NULL)
76 changes: 76 additions & 0 deletions src/test/kotlin/application/business/OrderEventTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package application.business

import application.business.OrderStatus.PLACED
import application.business.OrderStatus.PROCESSING
import application.documentation.ArchitectureDocumentation.createOrReplaceEvent
import application.documentation.EventDescriptionSpec
import application.documentation.eventDescription
import org.junit.jupiter.api.Test
import java.time.Instant.parse
import java.util.UUID.fromString

class OrderEventTests {

@Test
fun `OrderPlaced event`() {
val description = eventDescription(
example = OrderPlaced(
id = fromString("3d6fd447-a311-4028-8248-356e3621d450"),
timestamp = parse("2024-07-22T12:34:56.789Z"),
order = OrderData(
orderId = fromString("a64914f7-7404-4e85-8e1a-778068fae307"),
customerId = fromString("ed2a43d7-e49b-408d-8b5f-e2e2305954c2"),
status = PLACED
)
),
description = "Emitted whenever a new order is placed.",
fields = {
orderDataFields("order")
}
)
createOrReplaceEvent(description)
}

@Test
fun `OrderCanceled event`() {
val description = eventDescription(
example = OrderCanceled(
id = fromString("4c97c099-0c00-4e56-841d-fbfe81770936"),
timestamp = parse("2024-07-22T12:34:56.789Z"),
order = OrderData(
orderId = fromString("a64914f7-7404-4e85-8e1a-778068fae307"),
customerId = fromString("ed2a43d7-e49b-408d-8b5f-e2e2305954c2"),
status = PROCESSING
)
),
description = "Emitted whenever an order is canceled.",
fields = {
orderDataFields("order")
}
)
createOrReplaceEvent(description)
}

private fun EventDescriptionSpec.orderDataFields(objectName: String) {
field(
property = objectName,
type = "Object",
)
field(
property = "$objectName.orderId",
type = "UUID4",
description = "The ID of the order."
)
field(
property = "$objectName.customerId",
type = "UUID4",
description = "The ID of the customer that placed the order."
)
field(
property = "$objectName.status",
type = "Enumeration",
description = "The status of the order. Might have one of the following values: "
+ OrderStatus.entries.joinToString()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,25 @@ object ArchitectureDocumentation {
}
}

fun createOrReplaceEvent(description: EventDescription) {
val folder = File(rootFolder, "events")
val file = File(folder, description.type + ".json")

createOrReplaceFile(file) {
write(toJsonString(description))
}
}

private fun toJsonString(value: Any): String =
objectMapper.writeValueAsString(value)


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

private fun createOrReplaceFile(file: File, writer: BufferedWriter.() -> Unit) {
file.parentFile.mkdirs()
FileOutputStream(file, true).use { it.bufferedWriter().use(writer) }
FileOutputStream(file, false).use { it.bufferedWriter().use(writer) }
}
}
58 changes: 58 additions & 0 deletions src/test/kotlin/application/documentation/events.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package application.documentation

import application.business.Event
import application.config.objectMapperForEvents
import com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT

private val eventObjectMapper = objectMapperForEvents()
.enable(INDENT_OUTPUT) // pretty format the JSON examples

fun eventDescription(example: Event, description: String, fields: EventDescriptionSpec.() -> Unit): EventDescription =
EventDescriptionSpec(example, description).apply(fields).build()

class EventDescriptionSpec(
private val example: Event,
private val description: String
) {
private val fields = mutableListOf<EventDescription.Field>()

init {
field(
property = "id",
type = "UUID4",
nullable = false,
description = "The unique ID of the event."
)
field(
property = "timestamp",
type = "ISO-8601 Date+Time (UTC)",
nullable = false,
description = "The exact instant the event occurred at its source."
)
}

fun field(
property: String,
type: String,
nullable: Boolean = false,
description: String? = null
) {
check(fields.none { it.property == property }) { "Field '$property' is already documented." }
fields.add(
EventDescription.Field(
property = property,
type = type,
nullable = nullable,
description = description,
)
)
}

internal fun build() = EventDescription(
name = example.getEventName(),
type = example.getEventType(),
description = description,
example = eventObjectMapper.writeValueAsString(example),
fields = fields
)
}
15 changes: 15 additions & 0 deletions src/test/kotlin/application/documentation/model.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,18 @@ enum class Distance { OWNED, CLOSE, EXTERNAL }
enum class Credentials { JWT, BASIC_AUTH }

data class HttpEndpoint(val method: String, val path: String)

data class EventDescription(
val name: String,
val type: String,
val description: String,
val example: String,
val fields: List<Field>,
) {
data class Field(
val property: String,
val type: String,
val nullable: Boolean,
val description: String?,
)
}

0 comments on commit 93938e5

Please sign in to comment.