feat(Tracer Bullet)

This commit is contained in:
2025-08-11 23:47:05 +02:00
parent 582678e226
commit a50b1b3822
43 changed files with 1665 additions and 292 deletions
+42 -18
View File
@@ -1,27 +1,51 @@
// Dieses Modul definiert die Kern-Domänenobjekte des Shared Kernels.
// Es enthält keine Implementierungsdetails, nur reine Datenklassen und Enums.
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.kotlin.serialization)
}
kotlin {
compilerOptions {
freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime")
// Target platforms
jvm {
compilerOptions {
freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime")
}
}
js(IR) {
browser()
}
sourceSets {
val commonMain by getting {
dependencies {
// Kern-Abhängigkeiten für das Domänen-Modul (common for all platforms)
api(libs.uuid)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
}
}
val jvmMain by getting {
dependencies {
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
// definierten Bibliotheken hat (JVM-specific)
api(projects.platform.platformDependencies)
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}
val jvmTest by getting {
dependencies {
// Stellt die Test-Bibliotheken bereit (JVM-specific)
implementation(projects.platform.platformTesting)
implementation(libs.bundles.testing.jvm)
}
}
}
}
dependencies {
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
// definierten Bibliotheken hat.
api(projects.platform.platformDependencies)
// Kern-Abhängigkeiten für das Domänen-Modul.
api(libs.uuid)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
// Stellt die Test-Bibliotheken bereit.
testImplementation(projects.platform.platformTesting)
testImplementation(libs.bundles.testing.jvm)
}
@@ -1,5 +1,6 @@
package at.mocode.core.domain.event
import at.mocode.core.domain.model.*
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.UuidSerializer
import com.benasher44.uuid.Uuid
@@ -7,38 +8,38 @@ import com.benasher44.uuid.uuid4
import kotlin.time.Clock
import kotlin.time.Instant
import kotlinx.serialization.Serializable
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalTime::class)
/**
* Basis-Interface für alle Domänen-Events im System.
* Ein Domänen-Event repräsentiert etwas fachlich Bedeutsames, das passiert ist.
*/
interface DomainEvent {
val eventId: Uuid
val aggregateId: Uuid
val eventType: String
val eventId: EventId
val aggregateId: AggregateId
val eventType: EventType
val timestamp: Instant
val version: Long
val correlationId: Uuid?
val causationId: Uuid?
val version: EventVersion
val correlationId: CorrelationId?
val causationId: CausationId?
}
/**
* Abstrakte Basisklasse für Domänen-Events, um Boilerplate-Code zu reduzieren.
*/
@Serializable
@OptIn(ExperimentalTime::class)
abstract class BaseDomainEvent(
@Serializable(with = UuidSerializer::class)
override val aggregateId: Uuid,
override val eventType: String,
override val version: Long,
@Serializable(with = UuidSerializer::class)
override val eventId: Uuid = uuid4(),
override val aggregateId: AggregateId,
override val eventType: EventType,
override val version: EventVersion,
override val eventId: EventId = EventId(uuid4()),
@Serializable(with = KotlinInstantSerializer::class)
override val timestamp: Instant = Clock.System.now(),
@Serializable(with = UuidSerializer::class)
override val correlationId: Uuid? = null,
@Serializable(with = UuidSerializer::class)
override val causationId: Uuid? = null
override val correlationId: CorrelationId? = null,
override val causationId: CausationId? = null
) : DomainEvent
/**
@@ -5,6 +5,7 @@ import at.mocode.core.domain.serialization.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlin.time.Clock
import kotlin.time.Instant
import kotlin.time.ExperimentalTime
import kotlinx.serialization.Serializable
/**
@@ -16,9 +17,9 @@ interface BaseDto
* Base DTO for domain entities that have unique ID and audit timestamps.
*/
@Serializable
@OptIn(ExperimentalTime::class)
abstract class EntityDto : BaseDto {
@Serializable(with = UuidSerializer::class)
abstract val id: Uuid
abstract val id: EntityId
@Serializable(with = KotlinInstantSerializer::class)
abstract val createdAt: Instant
@@ -41,6 +42,7 @@ data class ErrorDto(
* A standardized and consistent wrapper for all API responses.
*/
@Serializable
@OptIn(ExperimentalTime::class)
data class ApiResponse<T>(
val data: T?,
val success: Boolean,
@@ -49,10 +51,12 @@ data class ApiResponse<T>(
val timestamp: Instant = Clock.System.now()
) {
companion object {
@OptIn(ExperimentalTime::class)
fun <T> success(data: T): ApiResponse<T> {
return ApiResponse(data = data, success = true)
}
@OptIn(ExperimentalTime::class)
fun <T> error(
code: String,
message: String,
@@ -65,6 +69,7 @@ data class ApiResponse<T>(
)
}
@OptIn(ExperimentalTime::class)
fun <T> error(errors: List<ErrorDto>): ApiResponse<T> {
return ApiResponse(data = null, success = false, errors = errors)
}
@@ -0,0 +1,134 @@
package at.mocode.core.domain.model
import at.mocode.core.domain.serialization.UuidSerializer
import com.benasher44.uuid.Uuid
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
/**
* Value classes for strongly typed IDs and domain values.
* These provide compile-time type safety without runtime overhead.
*/
// === ID Value Classes ===
/**
* A strongly typed wrapper for entity IDs.
*/
@Serializable
@JvmInline
value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
}
/**
* A strongly typed wrapper for event IDs.
*/
@Serializable
@JvmInline
value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
}
/**
* A strongly typed wrapper for aggregate IDs.
*/
@Serializable
@JvmInline
value class AggregateId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
}
/**
* A strongly typed wrapper for correlation IDs used in event tracing.
*/
@Serializable
@JvmInline
value class CorrelationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
}
/**
* A strongly typed wrapper for causation IDs used in event tracing.
*/
@Serializable
@JvmInline
value class CausationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
override fun toString(): String = value.toString()
}
// === Domain Value Classes ===
/**
* A strongly typed wrapper for event types.
*/
@Serializable
@JvmInline
value class EventType(val value: String) {
init {
require(value.isNotBlank()) { "Event type cannot be blank" }
require(value.matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))) {
"Event type must start with a letter and contain only alphanumeric characters"
}
}
override fun toString(): String = value
}
/**
* A strongly typed wrapper for event version numbers.
*/
@Serializable
@JvmInline
value class EventVersion(val value: Long) : Comparable<EventVersion> {
init {
require(value >= 0) { "Event version must be non-negative" }
}
override fun toString(): String = value.toString()
override fun compareTo(other: EventVersion): Int = value.compareTo(other.value)
}
/**
* A strongly typed wrapper for error codes.
*/
@Serializable
@JvmInline
value class ErrorCode(val value: String) {
init {
require(value.isNotBlank()) { "Error code cannot be blank" }
require(value.matches(Regex("^[A-Z][A-Z0-9_]*$"))) {
"Error code must be uppercase and contain only letters, numbers, and underscores"
}
}
override fun toString(): String = value
}
/**
* A strongly typed wrapper for page numbers in pagination.
*/
@Serializable
@JvmInline
value class PageNumber(val value: Int) {
init {
require(value >= 0) { "Page number must be non-negative" }
}
override fun toString(): String = value.toString()
}
/**
* A strongly typed wrapper for page sizes in pagination.
*/
@Serializable
@JvmInline
value class PageSize(val value: Int) {
init {
require(value > 0) { "Page size must be positive" }
require(value <= 1000) { "Page size cannot exceed 1000" }
}
override fun toString(): String = value.toString()
}
@@ -3,6 +3,7 @@ package at.mocode.core.domain.serialization
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuidFrom
import kotlin.time.Instant // KORRIGIERT: Finaler Wechsel zu kotlin.time
import kotlin.time.ExperimentalTime
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
@@ -19,6 +20,7 @@ object UuidSerializer : KSerializer<Uuid> {
override fun deserialize(decoder: Decoder): Uuid = uuidFrom(decoder.decodeString())
}
@OptIn(ExperimentalTime::class)
object KotlinInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
@@ -1,6 +1,7 @@
package at.mocode.core.domain
import at.mocode.core.domain.event.BaseDomainEvent
import at.mocode.core.domain.model.*
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import kotlinx.serialization.Serializable
@@ -22,21 +23,21 @@ class DomainEventTest {
@Serializable
data class TestEvent(
@Transient
override val aggregateId: Uuid = uuid4(),
override val aggregateId: AggregateId = AggregateId(uuid4()),
@Transient
override val version: Long = 1L,
override val version: EventVersion = EventVersion(1L),
val testPayload: String = "Test"
) : BaseDomainEvent(
aggregateId = aggregateId,
eventType = "TestEventOccurred", // Ein klar definierter Event-Typ
eventType = EventType("TestEventOccurred"), // Ein klar definierter Event-Typ
version = version
)
@Test
fun `BaseDomainEvent should auto-generate eventId and timestamp upon creation`() {
// Arrange
val aggregateId = uuid4()
val version = 1L
val aggregateId = AggregateId(uuid4())
val version = EventVersion(1L)
// Act
val event = TestEvent(aggregateId, version)
@@ -46,6 +47,6 @@ class DomainEventTest {
assertNotNull(event.timestamp, "timestamp should be automatically generated and not null")
assertEquals(aggregateId, event.aggregateId, "aggregateId should be set correctly")
assertEquals(version, event.version, "version should be set correctly")
assertEquals("TestEventOccurred", event.eventType, "eventType should be set correctly")
assertEquals(EventType("TestEventOccurred"), event.eventType, "eventType should be set correctly")
}
}