Merge pull request #18

* MP-19 Refactoring: Einführung der "Registry" & "Masterdata" Trennung …

* MP-19 Refactoring: Frontend Tabula Rasa

* MP-19 Refactoring: Frontend Tabula Rasa

* refactoring:

* MP-20 fix(docker/clients): include `:domains` module in web/desktop b…

* MP-20 fix(web-app build): resolve JS compile error and add dev/prod b…

* MP-20 fix(web-app): remove vendor.js reference and harden JS bootstra…

* MP-20 fixing: clients

* MP-20 fixing: clients
This commit is contained in:
StefanMo
2025-11-30 14:13:12 +01:00
committed by GitHub
parent 596a05b69c
commit 9ea2b74a81
254 changed files with 5485 additions and 15971 deletions
+59 -57
View File
@@ -1,71 +1,73 @@
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvmToolchain(21)
jvmToolchain(21)
jvm {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
jvm {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
}
js(IR) {
browser {
testTask {
enabled = false
}
}
}
sourceSets {
// Opt-in to experimental Kotlin UUID API across all source sets
all {
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
// Opt-in für kotlin.time.ExperimentalTime projektweit, solange Teile noch experimentell sind
languageSettings.optIn("kotlin.time.ExperimentalTime")
}
js(IR) {
browser {
testTask {
enabled = false
}
}
}
commonMain.dependencies {
// Core dependencies (that aren't included in platform-dependencies)
// Note: core-domain should NOT depend on core-utils to avoid circular dependencies
// core-utils depends on core-domain, not the other way around
sourceSets {
// Opt-in to experimental Kotlin UUID API across all source sets
all {
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
}
commonMain.dependencies {
// Core dependencies (that aren't included in platform-dependencies)
// Note: core-domain should NOT depend on core-utils to avoid circular dependencies
// core-utils depends on core-domain, not the other way around
// Serialization and date-time for commonMain
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jsMain.dependencies {
api(libs.kotlinx.coroutines.core)
}
jsTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
// Fachliches Domain-Modul: keine technischen Abhängigkeiten hier hinterlegen.
// Falls in Zukunft JVM-spezifische, fachlich neutrale Ergänzungen nötig sind,
// bitte bewusst und minimal hinzufügen.
}
jvmTest.dependencies {
// implementation(kotlin("test-junit5"))
implementation(libs.junit.jupiter.api)
implementation(libs.mockk)
implementation(projects.platform.platformTesting)
implementation(libs.bundles.testing.jvm)
}
// Serialization and date-time for commonMain
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jsMain.dependencies {
api(libs.kotlinx.coroutines.core)
}
jsTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
// Fachliches Domain-Modul: keine technischen Abhängigkeiten hier hinterlegen.
// Falls in Zukunft JVM-spezifische, fachlich neutrale Ergänzungen nötig sind,
// bitte bewusst und minimal hinzufügen.
}
jvmTest.dependencies {
// implementation(kotlin("test-junit5"))
implementation(libs.junit.jupiter.api)
implementation(libs.mockk)
implementation(projects.platform.platformTesting)
implementation(libs.bundles.testing.jvm)
}
}
}
tasks.named<Test>("jvmTest") {
useJUnitPlatform()
useJUnitPlatform()
}
@@ -1,11 +1,11 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
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.KotlinxInstantSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.Clock as KtClock
import kotlin.time.Instant
import kotlin.uuid.Uuid
@@ -13,73 +13,71 @@ import kotlin.uuid.Uuid
* Basis-Interface für alle Domain-Events im System.
* Ein Domain-Event beschreibt ein fachlich relevantes Ereignis, das stattgefunden hat.
*/
@OptIn(ExperimentalTime::class)
interface DomainEvent {
val eventId: EventId
val aggregateId: AggregateId
val eventType: EventType
val timestamp: Instant
val version: EventVersion
val correlationId: CorrelationId?
val causationId: CausationId?
val eventId: EventId
val aggregateId: AggregateId
val eventType: EventType
val timestamp: Instant
val version: EventVersion
val correlationId: CorrelationId?
val causationId: CausationId?
}
/**
* Abstrakte Basisklasse für Domain-Events, um Boilerplate zu reduzieren.
*/
@Serializable
@OptIn(ExperimentalTime::class)
abstract class BaseDomainEvent(
override val aggregateId: AggregateId,
override val eventType: EventType,
override val version: EventVersion,
override val eventId: EventId = EventId(Uuid.random()),
@Serializable(with = KotlinInstantSerializer::class)
override val timestamp: Instant,
override val correlationId: CorrelationId? = null,
override val causationId: CausationId? = null
override val aggregateId: AggregateId,
override val eventType: EventType,
override val version: EventVersion,
override val eventId: EventId = EventId(Uuid.random()),
@Serializable(with = KotlinxInstantSerializer::class)
override val timestamp: Instant,
override val correlationId: CorrelationId? = null,
override val causationId: CausationId? = null
) : DomainEvent {
constructor(
aggregateId: AggregateId,
eventType: EventType,
version: EventVersion,
eventId: EventId = EventId(Uuid.random()),
correlationId: CorrelationId? = null,
causationId: CausationId? = null
) : this(
aggregateId = aggregateId,
eventType = eventType,
version = version,
eventId = eventId,
timestamp = createTimestamp(),
correlationId = correlationId,
causationId = causationId
)
constructor(
aggregateId: AggregateId,
eventType: EventType,
version: EventVersion,
eventId: EventId = EventId(Uuid.random()),
correlationId: CorrelationId? = null,
causationId: CausationId? = null
) : this(
aggregateId = aggregateId,
eventType = eventType,
version = version,
eventId = eventId,
timestamp = createTimestamp(),
correlationId = correlationId,
causationId = causationId
)
companion object {
@OptIn(ExperimentalTime::class)
private fun createTimestamp(): Instant = Clock.System.now()
}
companion object {
private fun createTimestamp(): Instant = Instant.parse(KtClock.System.now().toString())
}
}
/**
* Schnittstelle für einen Publisher, der Domain-Events veröffentlichen kann.
*/
interface DomainEventPublisher {
suspend fun publish(event: DomainEvent)
suspend fun publishAll(events: List<DomainEvent>)
suspend fun publish(event: DomainEvent)
suspend fun publishAll(events: List<DomainEvent>)
}
/**
* Schnittstelle für einen Handler, der auf bestimmte Domain-Events reagieren kann.
*/
interface DomainEventHandler<T : DomainEvent> {
suspend fun handle(event: T)
fun canHandle(eventType: EventType): Boolean
suspend fun handle(event: T)
fun canHandle(eventType: EventType): Boolean
/**
* Rückwärtskompatible Methode für String-basierte Prüfung des Event-Typs.
*/
fun canHandle(eventType: String): Boolean = canHandle(EventType(eventType))
/**
* Rückwärtskompatible Methode für String-basierte Prüfung des Event-Typs.
*/
fun canHandle(eventType: String): Boolean = canHandle(EventType(eventType))
}
@@ -1,6 +1,6 @@
package at.mocode.core.domain.model
import at.mocode.core.domain.serialization.KotlinInstantSerializer
import at.mocode.core.domain.serialization.KotlinxInstantSerializer
import kotlinx.serialization.Serializable
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
@@ -15,15 +15,14 @@ interface BaseDto
* Basis-DTO für Domänen-Entitäten mit eindeutiger ID und Audit-Zeitstempeln.
*/
@Serializable
@OptIn(ExperimentalTime::class)
abstract class EntityDto : BaseDto {
abstract val id: EntityId
abstract val id: EntityId
@Serializable(with = KotlinInstantSerializer::class)
abstract val createdAt: Instant
@Serializable(with = KotlinxInstantSerializer::class)
abstract val createdAt: Instant
@Serializable(with = KotlinInstantSerializer::class)
abstract val updatedAt: Instant
@Serializable(with = KotlinxInstantSerializer::class)
abstract val updatedAt: Instant
}
/**
@@ -31,57 +30,63 @@ abstract class EntityDto : BaseDto {
*/
@Serializable
data class ErrorDto(
val code: ErrorCode,
val message: String,
val field: String? = null
val code: ErrorCode,
val message: String,
val field: String? = null
) : BaseDto
/**
* Standardisierte Hülle für API-Antworten mit einheitlicher Struktur.
*/
@Serializable
@OptIn(ExperimentalTime::class)
data class ApiResponse<T>(
val data: T?,
val success: Boolean,
val errors: List<ErrorDto> = emptyList(),
@Serializable(with = KotlinInstantSerializer::class)
val timestamp: Instant
val data: T?,
val success: Boolean,
val errors: List<ErrorDto> = emptyList(),
@Serializable(with = KotlinxInstantSerializer::class)
val timestamp: Instant
) {
companion object {
@OptIn(ExperimentalTime::class)
fun <T> success(data: T): ApiResponse<T> {
return ApiResponse(data = data, success = true, timestamp = Clock.System.now())
}
companion object {
@OptIn(ExperimentalTime::class)
fun <T> success(data: T): ApiResponse<T> =
ApiResponse(
data = data,
success = true,
timestamp = Instant.parse(Clock.System.now().toString())
)
@OptIn(ExperimentalTime::class)
fun <T> error(
code: ErrorCode,
message: String,
field: String? = null
): ApiResponse<T> {
return ApiResponse(
data = null,
success = false,
errors = listOf(ErrorDto(code = code, message = message, field = field)),
timestamp = Clock.System.now()
)
}
@OptIn(ExperimentalTime::class)
fun <T> error(
code: String,
message: String,
field: String? = null
): ApiResponse<T> {
return error(ErrorCode(code), message, field)
}
@OptIn(ExperimentalTime::class)
fun <T> error(errors: List<ErrorDto>): ApiResponse<T> {
return ApiResponse(data = null, success = false, errors = errors, timestamp = Clock.System.now())
}
@OptIn(ExperimentalTime::class)
fun <T> error(
code: ErrorCode,
message: String,
field: String? = null
): ApiResponse<T> {
return ApiResponse(
data = null,
success = false,
errors = listOf(ErrorDto(code = code, message = message, field = field)),
timestamp = Instant.parse(Clock.System.now().toString())
)
}
fun <T> error(
code: String,
message: String,
field: String? = null
): ApiResponse<T> {
return error(ErrorCode(code), message, field)
}
@OptIn(ExperimentalTime::class)
fun <T> error(errors: List<ErrorDto>): ApiResponse<T> {
return ApiResponse(
data = null,
success = false,
errors = errors,
timestamp = Instant.parse(Clock.System.now().toString())
)
}
}
}
/**
@@ -89,37 +94,37 @@ data class ApiResponse<T>(
*/
@Serializable
data class PagedResponse<T>(
val content: List<T>,
val page: PageNumber,
val size: PageSize,
val totalElements: Long,
val totalPages: Int,
val hasNext: Boolean,
val hasPrevious: Boolean
val content: List<T>,
val page: PageNumber,
val size: PageSize,
val totalElements: Long,
val totalPages: Int,
val hasNext: Boolean,
val hasPrevious: Boolean
) {
companion object {
/**
* Erzeugt eine PagedResponse mit Rückwärtskompatibilität für einfache Int-Werte.
* Nützlich, wenn Aufrufer noch keine PageNumber/PageSize verwenden.
*/
fun <T> create(
content: List<T>,
page: Int,
size: Int,
totalElements: Long,
totalPages: Int,
hasNext: Boolean,
hasPrevious: Boolean
): PagedResponse<T> {
return PagedResponse(
content = content,
page = PageNumber(page),
size = PageSize(size),
totalElements = totalElements,
totalPages = totalPages,
hasNext = hasNext,
hasPrevious = hasPrevious
)
}
companion object {
/**
* Erzeugt eine PagedResponse mit Rückwärtskompatibilität für einfache Int-Werte.
* Nützlich, wenn Aufrufer noch keine PageNumber/PageSize verwenden.
*/
fun <T> create(
content: List<T>,
page: Int,
size: Int,
totalElements: Long,
totalPages: Int,
hasNext: Boolean,
hasPrevious: Boolean
): PagedResponse<T> {
return PagedResponse(
content = content,
page = PageNumber(page),
size = PageSize(size),
totalElements = totalElements,
totalPages = totalPages,
hasNext = hasNext,
hasPrevious = hasPrevious
)
}
}
}
@@ -12,10 +12,10 @@ import kotlinx.serialization.Serializable
*/
@Serializable
enum class DatenQuelleE {
MANUELL,
IMPORT_ZNS,
SYSTEM_GENERATED,
IMPORT_API
MANUELL,
IMPORT_ZNS,
SYSTEM_GENERATED,
IMPORT_API
}
/**
@@ -23,11 +23,11 @@ enum class DatenQuelleE {
*/
@Serializable
enum class StatusE {
AKTIV,
INAKTIV,
ENTWURF,
ARCHIVIERT,
GELOESCHT
AKTIV,
INAKTIV,
ENTWURF,
ARCHIVIERT,
GELOESCHT
}
/**
@@ -35,10 +35,10 @@ enum class StatusE {
*/
@Serializable
enum class PrioritaetE {
NIEDRIG,
NORMAL,
HOCH,
KRITISCH
NIEDRIG,
NORMAL,
HOCH,
KRITISCH
}
/**
@@ -46,11 +46,11 @@ enum class PrioritaetE {
*/
@Serializable
enum class BenutzerRolleE {
ADMIN,
BENUTZER,
MODERATOR,
GAST,
SYSTEM
ADMIN,
BENUTZER,
MODERATOR,
GAST,
SYSTEM
}
/**
@@ -58,11 +58,11 @@ enum class BenutzerRolleE {
*/
@Serializable
enum class VerifikationsStatusE {
NICHT_VERIFIZIERT,
IN_PRUEFUNG,
VERIFIZIERT,
ABGELEHNT,
KORREKTUR_ERFORDERLICH
NICHT_VERIFIZIERT,
IN_PRUEFUNG,
VERIFIZIERT,
ABGELEHNT,
KORREKTUR_ERFORDERLICH
}
/**
@@ -70,10 +70,10 @@ enum class VerifikationsStatusE {
*/
@Serializable
enum class BearbeitungsStatusE {
OFFEN,
IN_BEARBEITUNG,
WARTEND,
ABGESCHLOSSEN,
ABGEBROCHEN,
FEHLER
OFFEN,
IN_BEARBEITUNG,
WARTEND,
ABGESCHLOSSEN,
ABGEBROCHEN,
FEHLER
}
@@ -0,0 +1,16 @@
package at.mocode.core.domain.model
/**
* Zentrale Sammlung der standardisierten Fehlercodes der Anwendung.
* Dient als Single-Source-of-Truth, um Inkonsistenzen zu vermeiden.
*/
object ErrorCodes {
val DUPLICATE_ENTRY = ErrorCode("DUPLICATE_ENTRY")
val CONSTRAINT_VIOLATION = ErrorCode("CONSTRAINT_VIOLATION")
val FOREIGN_KEY_VIOLATION = ErrorCode("FOREIGN_KEY_VIOLATION")
val CHECK_VIOLATION = ErrorCode("CHECK_VIOLATION")
val DATABASE_TIMEOUT = ErrorCode("DATABASE_TIMEOUT")
val DATABASE_ERROR = ErrorCode("DATABASE_ERROR")
val TRANSACTION_ERROR = ErrorCode("TRANSACTION_ERROR")
val VALIDATION_ERROR = ErrorCode("VALIDATION_ERROR")
}
@@ -8,38 +8,38 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class ValidationError(
val field: String,
val message: String,
val code: String
val field: String,
val message: String,
val code: String
) : BaseDto {
companion object {
/**
* Erzeugt einen Validierungsfehler für Pflichtfeld-Prüfungen.
*/
fun required(field: String): ValidationError {
return ValidationError(field, "$field ist erforderlich", "REQUIRED")
}
/**
* Erzeugt einen Validierungsfehler für ungültiges Format.
*/
fun invalidFormat(field: String, message: String = "Ungültiges Format"): ValidationError {
return ValidationError(field, message, "INVALID_FORMAT")
}
/**
* Erzeugt einen Validierungsfehler für Längenprüfungen.
*/
fun invalidLength(field: String, message: String): ValidationError {
return ValidationError(field, message, "INVALID_LENGTH")
}
/**
* Erzeugt einen Validierungsfehler für Bereichsprüfungen.
*/
fun invalidRange(field: String, message: String): ValidationError {
return ValidationError(field, message, "INVALID_RANGE")
}
companion object {
/**
* Erzeugt einen Validierungsfehler für Pflichtfeld-Prüfungen.
*/
fun required(field: String): ValidationError {
return ValidationError(field, "$field ist erforderlich", "REQUIRED")
}
/**
* Erzeugt einen Validierungsfehler für ungültiges Format.
*/
fun invalidFormat(field: String, message: String = "Ungültiges Format"): ValidationError {
return ValidationError(field, message, "INVALID_FORMAT")
}
/**
* Erzeugt einen Validierungsfehler für Längenprüfungen.
*/
fun invalidLength(field: String, message: String): ValidationError {
return ValidationError(field, message, "INVALID_LENGTH")
}
/**
* Erzeugt einen Validierungsfehler für Bereichsprüfungen.
*/
fun invalidRange(field: String, message: String): ValidationError {
return ValidationError(field, message, "INVALID_RANGE")
}
}
}
@@ -1,4 +1,5 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.core.domain.model
import at.mocode.core.domain.serialization.UuidSerializer
@@ -19,7 +20,7 @@ import kotlin.uuid.Uuid
@Serializable
@JvmInline
value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
companion object
companion object
}
/**
@@ -28,7 +29,7 @@ value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid
@Serializable
@JvmInline
value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
companion object
companion object
}
/**
@@ -37,7 +38,7 @@ value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid)
@Serializable
@JvmInline
value class AggregateId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
companion object
companion object
}
/**
@@ -46,7 +47,7 @@ value class AggregateId(@Serializable(with = UuidSerializer::class) val value: U
@Serializable
@JvmInline
value class CorrelationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
companion object
companion object
}
/**
@@ -55,7 +56,7 @@ value class CorrelationId(@Serializable(with = UuidSerializer::class) val value:
@Serializable
@JvmInline
value class CausationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
companion object
companion object
}
// === Domain Value Classes ===
@@ -66,14 +67,14 @@ value class CausationId(@Serializable(with = UuidSerializer::class) val value: U
@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"
}
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
override fun toString(): String = value
}
/**
@@ -82,13 +83,13 @@ value class EventType(val value: String) {
@Serializable
@JvmInline
value class EventVersion(val value: Long) : Comparable<EventVersion> {
init {
require(value >= 0) { "Event version must be non-negative" }
}
init {
require(value >= 0) { "Event version must be non-negative" }
}
override fun toString(): String = value.toString()
override fun toString(): String = value.toString()
override fun compareTo(other: EventVersion): Int = value.compareTo(other.value)
override fun compareTo(other: EventVersion): Int = value.compareTo(other.value)
}
/**
@@ -97,14 +98,14 @@ value class EventVersion(val value: Long) : Comparable<EventVersion> {
@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"
}
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
override fun toString(): String = value
}
/**
@@ -113,11 +114,11 @@ value class ErrorCode(val value: String) {
@Serializable
@JvmInline
value class PageNumber(val value: Int) {
init {
require(value >= 0) { "Page number must be non-negative" }
}
init {
require(value >= 0) { "Page number must be non-negative" }
}
override fun toString(): String = value.toString()
override fun toString(): String = value.toString()
}
/**
@@ -126,10 +127,10 @@ value class PageNumber(val value: Int) {
@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" }
}
init {
require(value > 0) { "Page size must be positive" }
require(value <= 1000) { "Page size cannot exceed 1000" }
}
override fun toString(): String = value.toString()
override fun toString(): String = value.toString()
}
@@ -14,19 +14,19 @@ import kotlinx.serialization.encoding.Encoder
* Serializes as ISO-8601 date string (yyyy-MM-dd).
*/
object KotlinLocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("KotlinLocalDate", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("KotlinLocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString())
}
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDate {
val text = decoder.decodeString()
return try {
LocalDate.parse(text)
} catch (e: Exception) {
throw SerializationException("Invalid LocalDate format: '$text'", e)
}
override fun deserialize(decoder: Decoder): LocalDate {
val text = decoder.decodeString()
return try {
LocalDate.parse(text)
} catch (e: Exception) {
throw SerializationException("Invalid LocalDate format: '$text'", e)
}
}
}
@@ -1,7 +1,6 @@
@file:OptIn(kotlin.time.ExperimentalTime::class)
package at.mocode.core.domain.serialization
import kotlinx.datetime.Instant
import kotlin.time.Instant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@@ -10,17 +9,17 @@ import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
/**
* Serializer for kotlinx.datetime.Instant.
* Serializer for kotlin.time.Instant.
* Uses ISO-8601 string representation.
*/
object KotlinxInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KotlinxInstant", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KotlinxInstant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
}
@@ -20,15 +20,15 @@ import kotlin.uuid.Uuid
*/
@OptIn(ExperimentalTime::class)
object KotlinInstantSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun serialize(encoder: Encoder, value: Instant) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
override fun deserialize(decoder: Decoder): Instant {
return Instant.parse(decoder.decodeString())
}
}
// Note: Serializer for kotlinx.datetime.Instant is defined in a separate file
@@ -39,15 +39,15 @@ object KotlinInstantSerializer : KSerializer<Instant> {
*/
@OptIn(ExperimentalUuidApi::class)
object UuidSerializer : KSerializer<Uuid> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: Uuid) {
encoder.encodeString(value.toString())
}
override fun serialize(encoder: Encoder, value: Uuid) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): Uuid {
return Uuid.parse(decoder.decodeString())
}
override fun deserialize(decoder: Decoder): Uuid {
return Uuid.parse(decoder.decodeString())
}
}
/**
@@ -55,15 +55,15 @@ object UuidSerializer : KSerializer<Uuid> {
* Konvertiert LocalDate zu/von ISO-8601 String-Repräsentation.
*/
object LocalDateSerializer : KSerializer<LocalDate> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDate", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString())
}
override fun serialize(encoder: Encoder, value: LocalDate) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDate {
return LocalDate.parse(decoder.decodeString())
}
override fun deserialize(decoder: Decoder): LocalDate {
return LocalDate.parse(decoder.decodeString())
}
}
/**
@@ -71,15 +71,15 @@ object LocalDateSerializer : KSerializer<LocalDate> {
* Konvertiert LocalDateTime zu/von ISO-8601 String-Repräsentation.
*/
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
override fun serialize(encoder: Encoder, value: LocalDateTime) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString())
}
override fun deserialize(decoder: Decoder): LocalDateTime {
return LocalDateTime.parse(decoder.decodeString())
}
}
/**
@@ -87,13 +87,13 @@ object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
* Konvertiert LocalTime zu/von ISO-8601 String-Repräsentation.
*/
object LocalTimeSerializer : KSerializer<LocalTime> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalTime", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("LocalTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: LocalTime) {
encoder.encodeString(value.toString())
}
override fun serialize(encoder: Encoder, value: LocalTime) {
encoder.encodeString(value.toString())
}
override fun deserialize(decoder: Decoder): LocalTime {
return LocalTime.parse(decoder.decodeString())
}
override fun deserialize(decoder: Decoder): LocalTime {
return LocalTime.parse(decoder.decodeString())
}
}
@@ -12,42 +12,42 @@ import kotlin.test.assertTrue
@OptIn(kotlin.time.ExperimentalTime::class)
class ApiResponseTest {
@Test
fun `success factory sets flags and timestamp`() {
val res = ApiResponse.success(data = 42)
assertTrue(res.success)
assertEquals(42, res.data)
assertTrue(res.errors.isEmpty())
assertNotNull(res.timestamp)
}
@Test
fun `success factory sets flags and timestamp`() {
val res = ApiResponse.success(data = 42)
assertTrue(res.success)
assertEquals(42, res.data)
assertTrue(res.errors.isEmpty())
assertNotNull(res.timestamp)
}
@Test
fun `error factory with code object`() {
val res = ApiResponse.error<Int>(ErrorCode("INVALID_INPUT"), "Fehlerhafte Eingabe", field = "name")
assertFalse(res.success)
assertNull(res.data)
assertEquals(1, res.errors.size)
assertEquals("INVALID_INPUT", res.errors.first().code.value)
assertEquals("Fehlerhafte Eingabe", res.errors.first().message)
assertEquals("name", res.errors.first().field)
assertNotNull(res.timestamp)
}
@Test
fun `error factory with code object`() {
val res = ApiResponse.error<Int>(ErrorCode("INVALID_INPUT"), "Fehlerhafte Eingabe", field = "name")
assertFalse(res.success)
assertNull(res.data)
assertEquals(1, res.errors.size)
assertEquals("INVALID_INPUT", res.errors.first().code.value)
assertEquals("Fehlerhafte Eingabe", res.errors.first().message)
assertEquals("name", res.errors.first().field)
assertNotNull(res.timestamp)
}
@Test
fun `error factory with code string`() {
val res = ApiResponse.error<Int>("NOT_FOUND", "Nicht gefunden")
assertFalse(res.success)
assertNull(res.data)
assertEquals(1, res.errors.size)
assertEquals("NOT_FOUND", res.errors.first().code.value)
}
@Test
fun `error factory with code string`() {
val res = ApiResponse.error<Int>("NOT_FOUND", "Nicht gefunden")
assertFalse(res.success)
assertNull(res.data)
assertEquals(1, res.errors.size)
assertEquals("NOT_FOUND", res.errors.first().code.value)
}
@Test
fun `error factory with list`() {
val res = ApiResponse.error<Int>(listOf())
assertFalse(res.success)
assertNull(res.data)
assertTrue(res.errors.isEmpty())
assertNotNull(res.timestamp)
}
@Test
fun `error factory with list`() {
val res = ApiResponse.error<Int>(listOf())
assertFalse(res.success)
assertNull(res.data)
assertTrue(res.errors.isEmpty())
assertNotNull(res.timestamp)
}
}
@@ -10,54 +10,54 @@ import kotlin.uuid.Uuid
@OptIn(kotlin.time.ExperimentalTime::class)
class BaseDomainEventTest {
@kotlinx.serialization.Serializable
data class TestEvent(
val name: String,
// Delegiert an BaseDomainEvent
private val base: BaseDomainEvent
) : BaseDomainEvent(
aggregateId = base.aggregateId,
eventType = base.eventType,
version = base.version,
eventId = base.eventId,
timestamp = base.timestamp,
correlationId = base.correlationId,
causationId = base.causationId
)
@kotlinx.serialization.Serializable
data class TestEvent(
val name: String,
// Delegiert an BaseDomainEvent
private val base: BaseDomainEvent
) : BaseDomainEvent(
aggregateId = base.aggregateId,
eventType = base.eventType,
version = base.version,
eventId = base.eventId,
timestamp = base.timestamp,
correlationId = base.correlationId,
causationId = base.causationId
)
@Test
fun `secondary constructor generates id and timestamp`() {
val aggId = AggregateId(Uuid.random())
val ev = object : BaseDomainEvent(
aggregateId = aggId,
eventType = EventType("TestEvent"),
version = EventVersion(1)
) {}
@Test
fun `secondary constructor generates id and timestamp`() {
val aggId = AggregateId(Uuid.random())
val ev = object : BaseDomainEvent(
aggregateId = aggId,
eventType = EventType("TestEvent"),
version = EventVersion(1)
) {}
assertNotNull(ev.eventId)
assertNotNull(ev.timestamp)
assertEquals(aggId, ev.aggregateId)
assertEquals(EventType("TestEvent"), ev.eventType)
assertEquals(EventVersion(1), ev.version)
}
assertNotNull(ev.eventId)
assertNotNull(ev.timestamp)
assertEquals(aggId, ev.aggregateId)
assertEquals(EventType("TestEvent"), ev.eventType)
assertEquals(EventVersion(1), ev.version)
}
@Test
fun `primary constructor uses provided id and timestamp`() {
val aggId = AggregateId(Uuid.random())
val eid = EventId(Uuid.random())
val ts = kotlin.time.Instant.parse("2025-01-01T00:00:00Z")
val base = object : BaseDomainEvent(
aggregateId = aggId,
eventType = EventType("TestEvent"),
version = EventVersion(2),
eventId = eid,
timestamp = ts,
correlationId = CorrelationId(Uuid.random()),
causationId = CausationId(Uuid.random())
) {}
@Test
fun `primary constructor uses provided id and timestamp`() {
val aggId = AggregateId(Uuid.random())
val eid = EventId(Uuid.random())
val ts = kotlin.time.Instant.parse("2025-01-01T00:00:00Z")
val base = object : BaseDomainEvent(
aggregateId = aggId,
eventType = EventType("TestEvent"),
version = EventVersion(2),
eventId = eid,
timestamp = ts,
correlationId = CorrelationId(Uuid.random()),
causationId = CausationId(Uuid.random())
) {}
assertEquals(eid, base.eventId)
assertEquals(ts, base.timestamp)
assertEquals(EventVersion(2), base.version)
}
assertEquals(eid, base.eventId)
assertEquals(ts, base.timestamp)
assertEquals(EventVersion(2), base.version)
}
}
@@ -12,43 +12,43 @@ import kotlin.uuid.Uuid
@OptIn(kotlin.time.ExperimentalTime::class)
class SerializersTest {
@Test
fun `Instant roundtrip`() {
val instant = kotlin.time.Instant.parse("2024-01-01T00:00:00Z")
val json = Json.encodeToString(KotlinInstantSerializer, instant)
val decoded = Json.decodeFromString(KotlinInstantSerializer, json)
assertEquals(instant, decoded)
}
@Test
fun `Instant roundtrip`() {
val instant = kotlin.time.Instant.parse("2024-01-01T00:00:00Z")
val json = Json.encodeToString(KotlinInstantSerializer, instant)
val decoded = Json.decodeFromString(KotlinInstantSerializer, json)
assertEquals(instant, decoded)
}
@Test
fun `UUID roundtrip`() {
val uuid = Uuid.random()
val json = Json.encodeToString(UuidSerializer, uuid)
val decoded = Json.decodeFromString(UuidSerializer, json)
assertEquals(uuid, decoded)
}
@Test
fun `UUID roundtrip`() {
val uuid = Uuid.random()
val json = Json.encodeToString(UuidSerializer, uuid)
val decoded = Json.decodeFromString(UuidSerializer, json)
assertEquals(uuid, decoded)
}
@Test
fun `LocalDate roundtrip`() {
val ld = LocalDate.parse("2024-06-15")
val json = Json.encodeToString(LocalDateSerializer, ld)
val decoded = Json.decodeFromString(LocalDateSerializer, json)
assertEquals(ld, decoded)
}
@Test
fun `LocalDate roundtrip`() {
val ld = LocalDate.parse("2024-06-15")
val json = Json.encodeToString(LocalDateSerializer, ld)
val decoded = Json.decodeFromString(LocalDateSerializer, json)
assertEquals(ld, decoded)
}
@Test
fun `LocalDateTime roundtrip`() {
val ldt = LocalDateTime.parse("2024-06-15T12:34:56")
val json = Json.encodeToString(LocalDateTimeSerializer, ldt)
val decoded = Json.decodeFromString(LocalDateTimeSerializer, json)
assertEquals(ldt, decoded)
}
@Test
fun `LocalDateTime roundtrip`() {
val ldt = LocalDateTime.parse("2024-06-15T12:34:56")
val json = Json.encodeToString(LocalDateTimeSerializer, ldt)
val decoded = Json.decodeFromString(LocalDateTimeSerializer, json)
assertEquals(ldt, decoded)
}
@Test
fun `LocalTime roundtrip`() {
val lt = LocalTime.parse("12:34:56")
val json = Json.encodeToString(LocalTimeSerializer, lt)
val decoded = Json.decodeFromString(LocalTimeSerializer, json)
assertEquals(lt, decoded)
}
@Test
fun `LocalTime roundtrip`() {
val lt = LocalTime.parse("12:34:56")
val json = Json.encodeToString(LocalTimeSerializer, lt)
val decoded = Json.decodeFromString(LocalTimeSerializer, json)
assertEquals(lt, decoded)
}
}
@@ -8,39 +8,39 @@ import kotlin.test.assertTrue
class ValueTypesTest {
@Test
fun `EventType validation works`() {
assertFailsWith<IllegalArgumentException> { EventType("") }
assertFailsWith<IllegalArgumentException> { EventType("1Bad") }
assertFailsWith<IllegalArgumentException> { EventType("bad-char!") }
assertEquals("OrderCreated", EventType("OrderCreated").toString())
}
@Test
fun `EventType validation works`() {
assertFailsWith<IllegalArgumentException> { EventType("") }
assertFailsWith<IllegalArgumentException> { EventType("1Bad") }
assertFailsWith<IllegalArgumentException> { EventType("bad-char!") }
assertEquals("OrderCreated", EventType("OrderCreated").toString())
}
@Test
fun `EventVersion must be non-negative and comparable`() {
assertFailsWith<IllegalArgumentException> { EventVersion(-1) }
assertEquals(0, EventVersion(0).compareTo(EventVersion(0)))
assertTrue(EventVersion(2) > EventVersion(1))
}
@Test
fun `EventVersion must be non-negative and comparable`() {
assertFailsWith<IllegalArgumentException> { EventVersion(-1) }
assertEquals(0, EventVersion(0).compareTo(EventVersion(0)))
assertTrue(EventVersion(2) > EventVersion(1))
}
@Test
fun `ErrorCode must be uppercase with allowed characters`() {
assertFailsWith<IllegalArgumentException> { ErrorCode("") }
assertFailsWith<IllegalArgumentException> { ErrorCode("abc") }
assertFailsWith<IllegalArgumentException> { ErrorCode("Bad_Code") }
assertEquals("VALID_CODE1", ErrorCode("VALID_CODE1").toString())
}
@Test
fun `ErrorCode must be uppercase with allowed characters`() {
assertFailsWith<IllegalArgumentException> { ErrorCode("") }
assertFailsWith<IllegalArgumentException> { ErrorCode("abc") }
assertFailsWith<IllegalArgumentException> { ErrorCode("Bad_Code") }
assertEquals("VALID_CODE1", ErrorCode("VALID_CODE1").toString())
}
@Test
fun `PageNumber must be non-negative`() {
assertFailsWith<IllegalArgumentException> { PageNumber(-1) }
assertEquals("0", PageNumber(0).toString())
}
@Test
fun `PageNumber must be non-negative`() {
assertFailsWith<IllegalArgumentException> { PageNumber(-1) }
assertEquals("0", PageNumber(0).toString())
}
@Test
fun `PageSize range is enforced`() {
assertFailsWith<IllegalArgumentException> { PageSize(0) }
assertFailsWith<IllegalArgumentException> { PageSize(1001) }
assertEquals("1000", PageSize(1000).toString())
}
@Test
fun `PageSize range is enforced`() {
assertFailsWith<IllegalArgumentException> { PageSize(0) }
assertFailsWith<IllegalArgumentException> { PageSize(1001) }
assertEquals("1000", PageSize(1000).toString())
}
}