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
-1
View File
@@ -1 +0,0 @@
# Core\n\nMinimal placeholder README. See docs/index.md for project documentation
+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())
}
}
+59 -59
View File
@@ -1,75 +1,75 @@
// Dieses Modul stellt gemeinsame technische Hilfsfunktionen bereit,
// wie z.B. Konfigurations-Management, Datenbank-Verbindungen und Service Discovery.
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
jvmToolchain(21)
jvmToolchain(21)
// Target platforms
jvm {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
// Target platforms
jvm {
compilerOptions {
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
}
}
js(IR) {
browser {
testTask {
enabled = false
}
}
}
sourceSets {
all {
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
}
js(IR) {
browser {
testTask {
enabled = false
}
}
commonMain.dependencies {
// Domain models and types (core-utils depends on core-domain, not vice versa)
api(projects.core.coreDomain)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
// Async support (available for all platforms)
api(libs.kotlinx.coroutines.core)
// Utilities (multiplatform compatible)
api(libs.bignum)
}
sourceSets {
all {
languageSettings.optIn("kotlin.uuid.ExperimentalUuidApi")
}
commonMain.dependencies {
// Domain models and types (core-utils depends on core-domain, not vice versa)
api(projects.core.coreDomain)
api(libs.kotlinx.serialization.json)
api(libs.kotlinx.datetime)
// Async support (available for all platforms)
api(libs.kotlinx.coroutines.core)
// Utilities (multiplatform compatible)
api(libs.bignum)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
// JVM-specific dependencies - access to central catalog
api(projects.platform.platformDependencies)
// Database Management (JVM-specific)
api(libs.bundles.exposed)
api(libs.bundles.flyway)
api(libs.hikari.cp)
// Service Discovery (JVM-specific)
api(libs.spring.cloud.starter.consul.discovery)
// Logging (JVM-specific)
api(libs.kotlin.logging.jvm)
// Jakarta Annotation API
api(libs.jakarta.annotation.api)
// JSON Processing
api(libs.jackson.module.kotlin)
api(libs.jackson.datatype.jsr310)
}
jvmTest.dependencies {
// Testing (JVM-specific)
implementation(projects.platform.platformTesting)
implementation(libs.bundles.testing.jvm)
runtimeOnly(libs.postgresql.driver)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
// JVM-specific dependencies - access to central catalog
api(projects.platform.platformDependencies)
// Database Management (JVM-specific)
api(libs.bundles.exposed)
api(libs.bundles.flyway)
api(libs.hikari.cp)
// Service Discovery (JVM-specific)
api(libs.spring.cloud.starter.consul.discovery)
// Logging (JVM-specific)
api(libs.kotlin.logging.jvm)
// Jakarta Annotation API
api(libs.jakarta.annotation.api)
// JSON Processing
api(libs.jackson.module.kotlin)
api(libs.jackson.datatype.jsr310)
}
jvmTest.dependencies {
// Testing (JVM-specific)
implementation(projects.platform.platformTesting)
implementation(libs.bundles.testing.jvm)
runtimeOnly(libs.postgresql.driver)
}
}
}
tasks.named<Test>("jvmTest") {
useJUnitPlatform()
useJUnitPlatform()
}
@@ -1,4 +1,5 @@
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
package at.mocode.core.utils
import at.mocode.core.domain.model.*
@@ -54,14 +55,14 @@ fun String.toErrorCode(): ErrorCode = ErrorCode(this)
* Prüft ob der String ein gültiger EventType-Name ist.
*/
fun String.isValidEventType(): Boolean {
return isNotBlank() && matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))
return isNotBlank() && matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))
}
/**
* Prüft ob der String ein gültiger ErrorCode ist.
*/
fun String.isValidErrorCode(): Boolean {
return isNotBlank() && matches(Regex("^[A-Z][A-Z0-9_]*$"))
return isNotBlank() && matches(Regex("^[A-Z][A-Z0-9_]*$"))
}
// === Collection Extensions ===
@@ -70,22 +71,22 @@ fun String.isValidErrorCode(): Boolean {
* Erstellt eine PagedResponse aus einer Liste mit Standard-Paginierung.
*/
fun <T> List<T>.toPagedResponse(
page: Int = 0,
size: Int = 20
page: Int = 0,
size: Int = 20
): PagedResponse<T> {
val startIndex = page * size
val endIndex = minOf(startIndex + size, this.size)
val content = if (startIndex < this.size) this.subList(startIndex, endIndex) else emptyList()
val startIndex = page * size
val endIndex = minOf(startIndex + size, this.size)
val content = if (startIndex < this.size) this.subList(startIndex, endIndex) else emptyList()
return PagedResponse.create(
content = content,
page = page,
size = size,
totalElements = this.size.toLong(),
totalPages = (this.size + size - 1) / size,
hasNext = endIndex < this.size,
hasPrevious = page > 0
)
return PagedResponse.create(
content = content,
page = page,
size = size,
totalElements = this.size.toLong(),
totalPages = (this.size + size - 1) / size,
hasNext = endIndex < this.size,
hasPrevious = page > 0
)
}
// === Validation Extensions ===
@@ -94,7 +95,7 @@ fun <T> List<T>.toPagedResponse(
* Erstellt eine Liste von ValidationError aus einer Map von Fehlern.
*/
fun Map<String, String>.toValidationErrors(): List<ValidationError> {
return this.map { (field, message) -> ValidationError(field, message, "VALIDATION_ERROR") }
return this.map { (field, message) -> ValidationError(field, message, "VALIDATION_ERROR") }
}
/**
@@ -106,7 +107,7 @@ fun List<ValidationError>.hasErrors(): Boolean = this.isNotEmpty()
* Konvertiert eine Liste von ValidationError zu ErrorDto.
*/
fun List<ValidationError>.toErrorDtos(): List<ErrorDto> {
return this.map { ErrorDto(ErrorCode(it.code), it.message, it.field) }
return this.map { ErrorDto(ErrorCode(it.code), it.message, it.field) }
}
// === Time Extensions ===
@@ -9,315 +9,333 @@ import kotlin.jvm.JvmName
* Bietet einen funktionalen Ansatz zur Fehlerbehandlung ohne Exceptions.
*/
sealed class Result<out T> {
/**
* Represents a successful operation with a value.
*/
data class Success<T>(val value: T) : Result<T>()
/**
* Represents a successful operation with a value.
*/
data class Success<T>(val value: T) : Result<T>()
/**
* Represents a failed operation with error messages.
*/
data class Failure(val errors: List<ErrorDto>) : Result<Nothing>()
/**
* Represents a failed operation with error messages.
*/
data class Failure(val errors: List<ErrorDto>) : Result<Nothing>()
/**
* Checks if the Result is a success.
*/
val isSuccess: Boolean get() = this is Success
/**
* Checks if the Result is a success.
*/
val isSuccess: Boolean get() = this is Success
/**
* Checks if the Result is a failure.
*/
val isFailure: Boolean get() = this is Failure
/**
* Checks if the Result is a failure.
*/
val isFailure: Boolean get() = this is Failure
/**
* Gets the value if it's a success, otherwise null.
*
* @return the value if this is a Success, or null if this is a Failure
*/
fun getOrNull(): T? = when (this) {
is Success -> value
is Failure -> null
}
/**
* Gets the value if it's a success, otherwise the default value.
*
* @param defaultValue the value to return if this is a Failure
* @return the value if this is a Success, or the default value if this is a Failure
*/
fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) {
is Success -> value
is Failure -> defaultValue
}
/**
* Gets the errors if it's a failure, otherwise an empty list.
*
* @return the list of errors if this is a Failure, or an empty list if this is a Success
*/
@JvmName("retrieveErrors")
fun getErrors(): List<ErrorDto> = when (this) {
is Success -> emptyList()
is Failure -> errors
}
/**
* Transforms the value if it's a success.
*
* @param transform function to apply to the success value
* @return a new Success with the transformed value if this is a Success, or this unchanged Failure
*/
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
is Success -> Success(transform(value))
is Failure -> this
}
/**
* Transforms the Result flatly (for nested Results).
* Unlike map, which wraps the transformed value in a new Success, flatMap uses the Result returned by the transform function.
*
* @param transform function that returns a Result
* @return the Result returned by the transform function if this is a Success, or this unchanged Failure
*/
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
is Success -> transform(value)
is Failure -> this
}
/**
* Executes an action if it's a success.
*
* @param action the function to execute with the success value
* @return this Result, unchanged, to allow for chaining
*/
inline fun onSuccess(action: (T) -> Unit): Result<T> {
if (this is Success) action(value)
return this
}
/**
* Executes an action if it's a failure.
*
* @param action the function to execute with the list of errors
* @return this Result, unchanged, to allow for chaining
*/
inline fun onFailure(action: (List<ErrorDto>) -> Unit): Result<T> {
if (this is Failure) action(errors)
return this
}
/**
* Transforms the Result by applying one of two functions depending on whether it's a success or failure.
*
* @param onSuccess function to apply if this is a success
* @param onFailure function to apply if this is a failure
* @return the result of applying the appropriate function
*/
inline fun <R> fold(
onSuccess: (T) -> R,
onFailure: (List<ErrorDto>) -> R
): R = when (this) {
is Success -> onSuccess(value)
is Failure -> onFailure(errors)
}
/**
* Attempts to recover from a failure by applying the specified function to the error list.
* If this is already a success, it is returned unchanged.
*
* @param transform function to apply to the error list to recover
* @return a new Success if recovery was successful, or this unchanged Result if already a success
*/
inline fun recover(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
is Success -> this
is Failure -> Success(transform(errors))
}
/**
* Attempts to recover from a failure by applying the specified function to the error list.
* If this is already a success, it is returned unchanged.
* If an exception occurs during recovery, it is converted to a new failure.
*
* @param transform function to apply to the error list to recover
* @return a new Success if recovery was successful, a new Failure if recovery threw an exception,
* or this unchanged Result if already a success
*/
inline fun recoverCatching(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
is Success -> this
is Failure -> try {
Success(transform(errors))
} catch (e: Exception) {
Failure(
listOf(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("RECOVERY_FAILED"),
message = e.message ?: "Recovery failed with an unknown error"
)
)
)
}
}
/**
* Combines this Result with another Result, creating a pair of their values if both are successful.
* If either Result is a failure, the combined Result will be a failure containing all errors.
*
* @param other the Result to combine with this one
* @return a Result containing a Pair of values if both are successful, or a Failure with all errors
*/
fun <R> zip(other: Result<R>): Result<Pair<T, R>> = when {
this is Success && other is Success -> Success(Pair(this.value, other.value))
this is Success && other is Failure -> Failure(other.errors)
this is Failure && other is Success -> Failure(this.errors)
this is Failure && other is Failure -> {
val allErrors = this.errors + other.errors
Failure(allErrors)
}
// This branch should never be reached due to sealed class, but included for completeness
else -> throw IllegalStateException("Unreachable code - Result should be either Success or Failure")
}
/**
* Safely attempts to get the value, throwing a custom exception if this is a failure.
*
* @param errorHandler function that converts the list of errors to an exception
* @return the value if this is a Success
* @throws E if this is a Failure, as created by the errorHandler
*/
inline fun <E : Throwable> getOrThrow(errorHandler: (List<ErrorDto>) -> E): T = when (this) {
is Success -> value
is Failure -> throw errorHandler(errors)
}
/**
* Gets the value if it's a success, or throws an IllegalStateException with a message constructed from the errors.
*
* @return the value if this is a Success
* @throws IllegalStateException if this is a Failure, with a message containing the error details
*/
fun getOrThrow(): T = getOrThrow { errors ->
IllegalStateException("Result is a Failure with errors: ${errors.joinToString { it.message }}")
}
companion object {
/**
* Gets the value if it's a success, otherwise null.
* Creates a successful Result.
*
* @return the value if this is a Success, or null if this is a Failure
* @param value the value to wrap in a Success
* @return a new Success containing the provided value
*/
fun getOrNull(): T? = when (this) {
is Success -> value
is Failure -> null
fun <T> success(value: T): Result<T> = Success(value)
/**
* Creates a failure Result with a single error.
*
* @param error the error to include in the Failure
* @return a new Failure containing the provided error
*/
fun <T> failure(error: ErrorDto): Result<T> = Failure(listOf(error))
/**
* Creates a failure Result with multiple errors.
*
* @param errors the list of errors to include in the Failure
* @return a new Failure containing the provided errors
*/
fun <T> failure(errors: List<ErrorDto>): Result<T> = Failure(errors)
/**
* Creates a failure Result from ValidationErrors.
* Converts the ValidationErrors to ErrorDtos internally.
*
* @param validationErrors the list of validation errors to convert and include in the Failure
* @return a new Failure containing ErrorDtos converted from the provided ValidationErrors
*/
@JvmName("failureFromValidationErrors")
fun <T> failure(validationErrors: List<ValidationError>): Result<T> =
Failure(validationErrors.toErrorDtos())
/**
* Executes an operation that returns a Result and catches exceptions.
* Provides more specific error codes based on the type of exception caught.
*
* @param operation the operation to execute
* @return a Success with the operation result, or a Failure with error details if an exception occurred
*/
inline fun <T> runCatching(operation: () -> T): Result<T> = try {
success(operation())
} catch (e: IllegalArgumentException) {
failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("INVALID_ARGUMENT"),
message = e.message ?: "Invalid argument provided"
)
)
} catch (e: IllegalStateException) {
failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("INVALID_STATE"),
message = e.message ?: "Operation called in invalid state"
)
)
} catch (e: UnsupportedOperationException) {
failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("UNSUPPORTED_OPERATION"),
message = e.message ?: "Operation not supported"
)
)
} catch (e: IndexOutOfBoundsException) {
failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("INDEX_OUT_OF_BOUNDS"),
message = e.message ?: "Index out of bounds"
)
)
} catch (e: NullPointerException) {
failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("NULL_REFERENCE"),
message = e.message ?: "Unexpected null reference"
)
)
} catch (e: ClassCastException) {
failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("TYPE_MISMATCH"),
message = e.message ?: "Type mismatch occurred"
)
)
} catch (e: Exception) {
// Fallback for any other exception type
failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("OPERATION_FAILED"),
message = e.message ?: "Unknown error occurred"
)
)
}
/**
* Gets the value if it's a success, otherwise the default value.
* Combines multiple Results into a single Result with a list.
* Optimized for performance with large collections.
*
* @param defaultValue the value to return if this is a Failure
* @return the value if this is a Success, or the default value if this is a Failure
* @param results a list of Results to combine
* @return a Success containing a list of all success values if all Results are successful,
* or a Failure containing all error messages if any Results are failures
*/
fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) {
is Success -> value
is Failure -> defaultValue
}
/**
* Gets the errors if it's a failure, otherwise an empty list.
*
* @return the list of errors if this is a Failure, or an empty list if this is a Success
*/
@JvmName("retrieveErrors")
fun getErrors(): List<ErrorDto> = when (this) {
is Success -> emptyList()
is Failure -> errors
}
/**
* Transforms the value if it's a success.
*
* @param transform function to apply to the success value
* @return a new Success with the transformed value if this is a Success, or this unchanged Failure
*/
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
is Success -> Success(transform(value))
is Failure -> this
}
/**
* Transforms the Result flatly (for nested Results).
* Unlike map, which wraps the transformed value in a new Success, flatMap uses the Result returned by the transform function.
*
* @param transform function that returns a Result
* @return the Result returned by the transform function if this is a Success, or this unchanged Failure
*/
inline fun <R> flatMap(transform: (T) -> Result<R>): Result<R> = when (this) {
is Success -> transform(value)
is Failure -> this
}
/**
* Executes an action if it's a success.
*
* @param action the function to execute with the success value
* @return this Result, unchanged, to allow for chaining
*/
inline fun onSuccess(action: (T) -> Unit): Result<T> {
if (this is Success) action(value)
return this
}
/**
* Executes an action if it's a failure.
*
* @param action the function to execute with the list of errors
* @return this Result, unchanged, to allow for chaining
*/
inline fun onFailure(action: (List<ErrorDto>) -> Unit): Result<T> {
if (this is Failure) action(errors)
return this
}
/**
* Transforms the Result by applying one of two functions depending on whether it's a success or failure.
*
* @param onSuccess function to apply if this is a success
* @param onFailure function to apply if this is a failure
* @return the result of applying the appropriate function
*/
inline fun <R> fold(
onSuccess: (T) -> R,
onFailure: (List<ErrorDto>) -> R
): R = when (this) {
is Success -> onSuccess(value)
is Failure -> onFailure(errors)
}
/**
* Attempts to recover from a failure by applying the specified function to the error list.
* If this is already a success, it is returned unchanged.
*
* @param transform function to apply to the error list to recover
* @return a new Success if recovery was successful, or this unchanged Result if already a success
*/
inline fun recover(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
is Success -> this
is Failure -> Success(transform(errors))
}
/**
* Attempts to recover from a failure by applying the specified function to the error list.
* If this is already a success, it is returned unchanged.
* If an exception occurs during recovery, it is converted to a new failure.
*
* @param transform function to apply to the error list to recover
* @return a new Success if recovery was successful, a new Failure if recovery threw an exception,
* or this unchanged Result if already a success
*/
inline fun recoverCatching(transform: (List<ErrorDto>) -> @UnsafeVariance T): Result<T> = when (this) {
is Success -> this
is Failure -> try {
Success(transform(errors))
} catch (e: Exception) {
Failure(listOf(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("RECOVERY_FAILED"),
message = e.message ?: "Recovery failed with an unknown error"
)))
}
}
/**
* Combines this Result with another Result, creating a pair of their values if both are successful.
* If either Result is a failure, the combined Result will be a failure containing all errors.
*
* @param other the Result to combine with this one
* @return a Result containing a Pair of values if both are successful, or a Failure with all errors
*/
fun <R> zip(other: Result<R>): Result<Pair<T, R>> = when {
this is Success && other is Success -> Success(Pair(this.value, other.value))
this is Success && other is Failure -> Failure(other.errors)
this is Failure && other is Success -> Failure(this.errors)
this is Failure && other is Failure -> {
val allErrors = this.errors + other.errors
Failure(allErrors)
}
// This branch should never be reached due to sealed class, but included for completeness
else -> throw IllegalStateException("Unreachable code - Result should be either Success or Failure")
}
/**
* Safely attempts to get the value, throwing a custom exception if this is a failure.
*
* @param errorHandler function that converts the list of errors to an exception
* @return the value if this is a Success
* @throws E if this is a Failure, as created by the errorHandler
*/
inline fun <E : Throwable> getOrThrow(errorHandler: (List<ErrorDto>) -> E): T = when (this) {
is Success -> value
is Failure -> throw errorHandler(errors)
}
/**
* Gets the value if it's a success, or throws an IllegalStateException with a message constructed from the errors.
*
* @return the value if this is a Success
* @throws IllegalStateException if this is a Failure, with a message containing the error details
*/
fun getOrThrow(): T = getOrThrow { errors ->
IllegalStateException("Result is a Failure with errors: ${errors.joinToString { it.message }}")
}
companion object {
/**
* Creates a successful Result.
*
* @param value the value to wrap in a Success
* @return a new Success containing the provided value
*/
fun <T> success(value: T): Result<T> = Success(value)
/**
* Creates a failure Result with a single error.
*
* @param error the error to include in the Failure
* @return a new Failure containing the provided error
*/
fun <T> failure(error: ErrorDto): Result<T> = Failure(listOf(error))
/**
* Creates a failure Result with multiple errors.
*
* @param errors the list of errors to include in the Failure
* @return a new Failure containing the provided errors
*/
fun <T> failure(errors: List<ErrorDto>): Result<T> = Failure(errors)
/**
* Creates a failure Result from ValidationErrors.
* Converts the ValidationErrors to ErrorDtos internally.
*
* @param validationErrors the list of validation errors to convert and include in the Failure
* @return a new Failure containing ErrorDtos converted from the provided ValidationErrors
*/
@JvmName("failureFromValidationErrors")
fun <T> failure(validationErrors: List<ValidationError>): Result<T> =
Failure(validationErrors.toErrorDtos())
/**
* Executes an operation that returns a Result and catches exceptions.
* Provides more specific error codes based on the type of exception caught.
*
* @param operation the operation to execute
* @return a Success with the operation result, or a Failure with error details if an exception occurred
*/
inline fun <T> runCatching(operation: () -> T): Result<T> = try {
success(operation())
} catch (e: IllegalArgumentException) {
failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("INVALID_ARGUMENT"),
message = e.message ?: "Invalid argument provided"
))
} catch (e: IllegalStateException) {
failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("INVALID_STATE"),
message = e.message ?: "Operation called in invalid state"
))
} catch (e: UnsupportedOperationException) {
failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("UNSUPPORTED_OPERATION"),
message = e.message ?: "Operation not supported"
))
} catch (e: IndexOutOfBoundsException) {
failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("INDEX_OUT_OF_BOUNDS"),
message = e.message ?: "Index out of bounds"
))
} catch (e: NullPointerException) {
failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("NULL_REFERENCE"),
message = e.message ?: "Unexpected null reference"
))
} catch (e: ClassCastException) {
failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("TYPE_MISMATCH"),
message = e.message ?: "Type mismatch occurred"
))
} catch (e: Exception) {
// Fallback for any other exception type
failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("OPERATION_FAILED"),
message = e.message ?: "Unknown error occurred"
))
}
/**
* Combines multiple Results into a single Result with a list.
* Optimized for performance with large collections.
*
* @param results a list of Results to combine
* @return a Success containing a list of all success values if all Results are successful,
* or a Failure containing all error messages if any Results are failures
*/
fun <T> combine(results: List<Result<T>>): Result<List<T>> {
// Fast path for empty list
if (results.isEmpty()) {
return success(emptyList())
}
// Fast path for single result
if (results.size == 1) {
return results.first().map { listOf(it) }
}
// Check if there are any failures
val anyFailure = results.any { it.isFailure }
// If no failures, we can optimize by directly mapping to values
if (!anyFailure) {
return success(results.map { (it as Success).value })
}
// If there are failures, collect all errors
val errors = results
.filterIsInstance<Failure>()
.flatMap { it.errors }
// If empty results list contained no failures, return empty success
if (errors.isEmpty()) {
return success(emptyList())
}
return failure(errors)
}
fun <T> combine(results: List<Result<T>>): Result<List<T>> {
// Fast path for empty list
if (results.isEmpty()) {
return success(emptyList())
}
// Fast path for single result
if (results.size == 1) {
return results.first().map { listOf(it) }
}
// Check if there are any failures
val anyFailure = results.any { it.isFailure }
// If no failures, we can optimize by directly mapping to values
if (!anyFailure) {
return success(results.map { (it as Success).value })
}
// If there are failures, collect all errors
val errors = results
.filterIsInstance<Failure>()
.flatMap { it.errors }
// If empty results list contained no failures, return empty success
if (errors.isEmpty()) {
return success(emptyList())
}
return failure(errors)
}
}
}
/**
@@ -328,11 +346,13 @@ sealed class Result<out T> {
* @return a Success containing the non-null value, or a Failure if the value is null
*/
fun <T> T?.toResult(errorMessage: String = "Value is null"): Result<T> =
if (this != null) {
Result.success(this)
} else {
Result.failure(ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("NULL_VALUE"),
message = errorMessage
))
}
if (this != null) {
Result.success(this)
} else {
Result.failure(
ErrorDto(
code = at.mocode.core.domain.model.ErrorCode("NULL_VALUE"),
message = errorMessage
)
)
}
@@ -11,63 +11,63 @@ import at.mocode.core.domain.model.ValidationError
* Builder-Klasse für die Erstellung von Validierungsregeln.
*/
class ValidationBuilder {
private val errors = mutableListOf<ValidationError>()
private val errors = mutableListOf<ValidationError>()
/**
* Validiert ein Feld gegen mehrere Regeln.
*/
fun <T> field(name: String, value: T, vararg rules: ValidationRule<T>): ValidationBuilder {
rules.forEach { rule ->
rule.validate(name, value)?.let { error ->
errors.add(error)
}
}
return this
/**
* Validiert ein Feld gegen mehrere Regeln.
*/
fun <T> field(name: String, value: T, vararg rules: ValidationRule<T>): ValidationBuilder {
rules.forEach { rule ->
rule.validate(name, value)?.let { error ->
errors.add(error)
}
}
return this
}
/**
* Fügt einen benutzerdefinierten Validierungsfehler hinzu.
*/
fun addError(field: String, message: String, code: String = "VALIDATION_ERROR"): ValidationBuilder {
errors.add(ValidationError(field, message, code))
return this
/**
* Fügt einen benutzerdefinierten Validierungsfehler hinzu.
*/
fun addError(field: String, message: String, code: String = "VALIDATION_ERROR"): ValidationBuilder {
errors.add(ValidationError(field, message, code))
return this
}
/**
* Führt eine benutzerdefinierten Validierung aus.
*/
fun custom(validation: () -> ValidationError?): ValidationBuilder {
validation()?.let { error ->
errors.add(error)
}
return this
}
/**
* Führt eine benutzerdefinierten Validierung aus.
*/
fun custom(validation: () -> ValidationError?): ValidationBuilder {
validation()?.let { error ->
errors.add(error)
}
return this
/**
* Erstellt das finale Validierungsergebnis.
*/
fun build(): Result<Unit> {
return if (errors.isEmpty()) {
Result.success(Unit)
} else {
Result.failure(errors)
}
}
/**
* Erstellt das finale Validierungsergebnis.
*/
fun build(): Result<Unit> {
return if (errors.isEmpty()) {
Result.success(Unit)
} else {
Result.failure(errors)
}
}
/**
* Gibt die gesammelten Fehler zurück.
*/
fun getErrors(): List<ValidationError> = errors.toList()
/**
* Gibt die gesammelten Fehler zurück.
*/
fun getErrors(): List<ValidationError> = errors.toList()
}
/**
* Interface für Validierungsregeln.
*/
fun interface ValidationRule<T> {
/**
* Validiert einen Wert und gibt einen Fehler zurück, wenn die Validierung fehlschlägt.
*/
fun validate(fieldName: String, value: T): ValidationError?
/**
* Validiert einen Wert und gibt einen Fehler zurück, wenn die Validierung fehlschlägt.
*/
fun validate(fieldName: String, value: T): ValidationError?
}
/**
@@ -75,150 +75,150 @@ fun interface ValidationRule<T> {
*/
object ValidationRules {
// === String-Validierungen ===
// === String-Validierungen ===
/**
* Prüft ob ein String nicht leer ist.
*/
fun notBlank(): ValidationRule<String> = ValidationRule { fieldName, value ->
if (value.isBlank()) ValidationError.required(fieldName) else null
}
/**
* Prüft ob ein String nicht leer ist.
*/
fun notBlank(): ValidationRule<String> = ValidationRule { fieldName, value ->
if (value.isBlank()) ValidationError.required(fieldName) else null
}
/**
* Prüft die Mindestlänge eines Strings.
*/
fun minLength(min: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
if (value.length < min) {
ValidationError.invalidLength(fieldName, "$fieldName must be at least $min characters long")
} else null
}
/**
* Prüft die Mindestlänge eines Strings.
*/
fun minLength(min: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
if (value.length < min) {
ValidationError.invalidLength(fieldName, "$fieldName muss mindestens $min Zeichen lang sein")
} else null
}
/**
* Prüft die Maximallänge eines Strings.
*/
fun maxLength(max: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
if (value.length > max) {
ValidationError.invalidLength(fieldName, "$fieldName must not exceed $max characters")
} else null
}
/**
* Prüft die Maximallänge eines Strings.
*/
fun maxLength(max: Int): ValidationRule<String> = ValidationRule { fieldName, value ->
if (value.length > max) {
ValidationError.invalidLength(fieldName, "$fieldName darf $max Zeichen nicht überschreiten")
} else null
}
/**
* Prüft ob ein String einem RegEx-Pattern entspricht.
*/
fun matches(pattern: Regex, message: String): ValidationRule<String> = ValidationRule { fieldName, value ->
if (!value.matches(pattern)) {
ValidationError.invalidFormat(fieldName, message)
} else null
}
/**
* Prüft ob ein String einem RegEx-Pattern entspricht.
*/
fun matches(pattern: Regex, message: String): ValidationRule<String> = ValidationRule { fieldName, value ->
if (!value.matches(pattern)) {
ValidationError.invalidFormat(fieldName, message)
} else null
}
/**
* Prüft ob ein String eine gültige E-Mail-Adresse ist.
*/
fun email(): ValidationRule<String> = ValidationRule { fieldName, value ->
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
if (!value.matches(emailRegex)) {
ValidationError.invalidFormat(fieldName, "$fieldName must be a valid email address")
} else null
}
/**
* Prüft ob ein String eine gültige E-Mail-Adresse ist.
*/
fun email(): ValidationRule<String> = ValidationRule { fieldName, value ->
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
if (!value.matches(emailRegex)) {
ValidationError.invalidFormat(fieldName, "$fieldName muss eine gültige E-Mail-Adresse sein")
} else null
}
// === Numerische Validierungen ===
// === Numerische Validierungen ===
/**
* Prüft den Mindestwert einer Zahl.
*/
fun <T : Comparable<T>> min(minValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
if (value < minValue) {
ValidationError.invalidRange(fieldName, "$fieldName must be at least $minValue")
} else null
}
/**
* Prüft den Mindestwert einer Zahl.
*/
fun <T : Comparable<T>> min(minValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
if (value < minValue) {
ValidationError.invalidRange(fieldName, "$fieldName muss mindestens $minValue sein")
} else null
}
/**
* Prüft den Maximalwert einer Zahl.
*/
fun <T : Comparable<T>> max(maxValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
if (value > maxValue) {
ValidationError.invalidRange(fieldName, "$fieldName must not exceed $maxValue")
} else null
}
/**
* Prüft den Maximalwert einer Zahl.
*/
fun <T : Comparable<T>> max(maxValue: T): ValidationRule<T> = ValidationRule { fieldName, value ->
if (value > maxValue) {
ValidationError.invalidRange(fieldName, "$fieldName darf $maxValue nicht überschreiten")
} else null
}
/**
* Prüft ob eine Zahl positiv ist.
*/
fun positive(): ValidationRule<Number> = ValidationRule { fieldName, value ->
if (value.toDouble() <= 0) {
ValidationError.invalidRange(fieldName, "$fieldName must be positive")
} else null
}
/**
* Prüft ob eine Zahl positiv ist.
*/
fun positive(): ValidationRule<Number> = ValidationRule { fieldName, value ->
if (value.toDouble() <= 0) {
ValidationError.invalidRange(fieldName, "$fieldName muss positiv sein")
} else null
}
/**
* Prüft ob eine Zahl nicht negativ ist.
*/
fun nonNegative(): ValidationRule<Number> = ValidationRule { fieldName, value ->
if (value.toDouble() < 0) {
ValidationError.invalidRange(fieldName, "$fieldName must not be negative")
} else null
}
/**
* Prüft ob eine Zahl nicht negativ ist.
*/
fun nonNegative(): ValidationRule<Number> = ValidationRule { fieldName, value ->
if (value.toDouble() < 0) {
ValidationError.invalidRange(fieldName, "$fieldName darf nicht negativ sein")
} else null
}
// === Collection-Validierungen ===
// === Collection-Validierungen ===
/**
* Prüft ob eine Collection nicht leer ist.
*/
fun <T> notEmpty(): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
if (value.isEmpty()) {
ValidationError.required(fieldName)
} else null
}
/**
* Prüft ob eine Collection nicht leer ist.
*/
fun <T> notEmpty(): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
if (value.isEmpty()) {
ValidationError.required(fieldName)
} else null
}
/**
* Prüft die Mindestgröße einer Collection.
*/
fun <T> minSize(min: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
if (value.size < min) {
ValidationError.invalidLength(fieldName, "$fieldName must contain at least $min items")
} else null
}
/**
* Prüft die Mindestgröße einer Collection.
*/
fun <T> minSize(min: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
if (value.size < min) {
ValidationError.invalidLength(fieldName, "$fieldName muss mindestens $min Elemente enthalten")
} else null
}
/**
* Prüft die Maximalgröße einer Collection.
*/
fun <T> maxSize(max: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
if (value.size > max) {
ValidationError.invalidLength(fieldName, "$fieldName must not contain more than $max items")
} else null
}
/**
* Prüft die Maximalgröße einer Collection.
*/
fun <T> maxSize(max: Int): ValidationRule<Collection<T>> = ValidationRule { fieldName, value ->
if (value.size > max) {
ValidationError.invalidLength(fieldName, "$fieldName darf nicht mehr als $max Elemente enthalten")
} else null
}
// === Null-Validierungen ===
// === Null-Validierungen ===
/**
* Prüft ob ein Wert nicht null ist.
*/
fun <T> notNull(): ValidationRule<T?> = ValidationRule { fieldName, value ->
if (value == null) ValidationError.required(fieldName) else null
}
/**
* Prüft ob ein Wert nicht null ist.
*/
fun <T> notNull(): ValidationRule<T?> = ValidationRule { fieldName, value ->
if (value == null) ValidationError.required(fieldName) else null
}
}
/**
* DSL-Funktion für die Erstellung von Validierungen.
*/
inline fun validate(builder: ValidationBuilder.() -> Unit): Result<Unit> {
return ValidationBuilder().apply(builder).build()
return ValidationBuilder().apply(builder).build()
}
/**
* Extension-Funktion für einfache String-Validierung.
*/
fun String?.validateNotBlank(fieldName: String): ValidationError? {
return if (this.isNullOrBlank()) ValidationError.required(fieldName) else null
return if (this.isNullOrBlank()) ValidationError.required(fieldName) else null
}
/**
* Extension-Funktion für einfache E-Mail-Validierung.
*/
fun String?.validateEmail(fieldName: String): ValidationError? {
if (this.isNullOrBlank()) return ValidationError.required(fieldName)
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
return if (!this.matches(emailRegex)) {
ValidationError.invalidFormat(fieldName, "$fieldName must be a valid email address")
} else null
if (this.isNullOrBlank()) return ValidationError.required(fieldName)
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
return if (!this.matches(emailRegex)) {
ValidationError.invalidFormat(fieldName, "$fieldName muss eine gültige E-Mail-Adresse sein")
} else null
}
@@ -9,28 +9,28 @@ import kotlin.test.assertTrue
class ExtensionsPagedResponseTest {
@Test
fun `toPagedResponse basic pagination`() {
val list = (1..50).toList()
@Test
fun `toPagedResponse basic pagination`() {
val list = (1..50).toList()
val page0 = list.toPagedResponse(page = 0, size = 10)
assertEquals(10, page0.content.size)
assertEquals(PageNumber(0), page0.page)
assertEquals(PageSize(10), page0.size)
assertEquals(50L, page0.totalElements)
assertEquals(5, page0.totalPages)
assertTrue(page0.hasNext)
assertFalse(page0.hasPrevious)
val page0 = list.toPagedResponse(page = 0, size = 10)
assertEquals(10, page0.content.size)
assertEquals(PageNumber(0), page0.page)
assertEquals(PageSize(10), page0.size)
assertEquals(50L, page0.totalElements)
assertEquals(5, page0.totalPages)
assertTrue(page0.hasNext)
assertFalse(page0.hasPrevious)
val page4 = list.toPagedResponse(page = 4, size = 10)
assertEquals((41..50).toList(), page4.content)
assertFalse(page4.hasNext)
assertTrue(page4.hasPrevious)
val page4 = list.toPagedResponse(page = 4, size = 10)
assertEquals((41..50).toList(), page4.content)
assertFalse(page4.hasNext)
assertTrue(page4.hasPrevious)
val emptyPage = list.toPagedResponse(page = 6, size = 10)
assertTrue(emptyPage.content.isEmpty())
assertEquals(5, emptyPage.totalPages)
assertFalse(emptyPage.hasNext)
assertTrue(emptyPage.hasPrevious)
}
val emptyPage = list.toPagedResponse(page = 6, size = 10)
assertTrue(emptyPage.content.isEmpty())
assertEquals(5, emptyPage.totalPages)
assertFalse(emptyPage.hasNext)
assertTrue(emptyPage.hasPrevious)
}
}
@@ -7,101 +7,103 @@ import kotlin.test.*
class ResultTest {
@Test
fun `success and failure flags`() {
val s = Result.success(1)
assertTrue(s.isSuccess)
assertFalse(s.isFailure)
@Test
fun `success and failure flags`() {
val s = Result.success(1)
assertTrue(s.isSuccess)
assertFalse(s.isFailure)
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
assertTrue(f.isFailure)
assertFalse(f.isSuccess)
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
assertTrue(f.isFailure)
assertFalse(f.isSuccess)
}
@Test
fun `map flatMap fold`() {
val s = Result.success(2).map { it * 2 }
assertEquals(4, (s as Result.Success).value)
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
assertTrue(f.map { it + 1 } is Result.Failure)
val flat = Result.success(2).flatMap { Result.success(it.toString()) }
assertEquals("2", (flat as Result.Success).value)
val folded = flat.fold({ it.length }, { -1 })
assertEquals(1, folded)
}
@Test
fun `zip and combine`() {
val a = Result.success(1)
val b = Result.success("x")
val zipped = a.zip(b)
assertTrue(zipped is Result.Success)
assertEquals(Pair(1, "x"), (zipped as Result.Success).value)
val f1: Result<Int> = Result.failure(ErrorDto(ErrorCode("E1"), ""))
val f2: Result<String> = Result.failure(ErrorDto(ErrorCode("E2"), ""))
val z2 = f1.zip(b)
assertTrue(z2 is Result.Failure)
val combined = Result.combine(listOf(Result.success(1), Result.success(2)))
assertTrue(combined is Result.Success)
assertEquals(listOf(1, 2), (combined as Result.Success).value)
val combinedFail =
Result.combine(listOf(f1 as Result<Int>, Result.success(3), Result.failure(ErrorDto(ErrorCode("E3"), ""))))
assertTrue(combinedFail is Result.Failure)
assertEquals(2, (combinedFail as Result.Failure).errors.size)
}
@Test
fun `runCatching failure conversion failureFromValidation and recovery`() {
val ok = Result.runCatching { "ok" }
assertTrue(ok is Result.Success)
val iae = Result.runCatching<String> { throw IllegalArgumentException("bad") }
assertTrue(iae is Result.Failure)
assertEquals("INVALID_ARGUMENT", (iae as Result.Failure).errors.first().code.value)
val generic = Result.runCatching<String> { throw Exception("x") }
assertTrue(generic is Result.Failure)
val verrs = listOf(ValidationError.required("name"), ValidationError.invalidFormat("email"))
val fromVal: Result<Unit> = Result.failure(verrs)
assertTrue(fromVal is Result.Failure)
assertEquals("REQUIRED", (fromVal as Result.Failure).errors.first().code.value)
val rec = Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recover { _ -> "fallback" }
assertTrue(rec is Result.Success)
val recFail =
Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recoverCatching { _ -> throw IllegalStateException("boom") }
assertTrue(recFail is Result.Failure)
assertEquals("RECOVERY_FAILED", (recFail as Result.Failure).errors.first().code.value)
}
@Test
fun `getOrNull default throw and toResult`() {
val s = Result.success(5)
assertEquals(5, s.getOrNull())
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), ""))
assertNull(f.getOrNull())
assertEquals(7, f.getOrDefault(7))
assertEquals(5, s.getOrThrow())
try {
f.getOrThrow()
fail("should throw")
} catch (e: IllegalStateException) {
// ok
}
@Test
fun `map flatMap fold`() {
val s = Result.success(2).map { it * 2 }
assertEquals(4, (s as Result.Success).value)
val nullable: Int? = null
val r = nullable.toResult("ist leer")
assertTrue(r is Result.Failure)
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), "m"))
assertTrue(f.map { it + 1 } is Result.Failure)
val flat = Result.success(2).flatMap { Result.success(it.toString()) }
assertEquals("2", (flat as Result.Success).value)
val folded = flat.fold({ it.length }, { -1 })
assertEquals(1, folded)
}
@Test
fun `zip and combine`() {
val a = Result.success(1)
val b = Result.success("x")
val zipped = a.zip(b)
assertTrue(zipped is Result.Success)
assertEquals(Pair(1, "x"), (zipped as Result.Success).value)
val f1: Result<Int> = Result.failure(ErrorDto(ErrorCode("E1"), ""))
val f2: Result<String> = Result.failure(ErrorDto(ErrorCode("E2"), ""))
val z2 = f1.zip(b)
assertTrue(z2 is Result.Failure)
val combined = Result.combine(listOf(Result.success(1), Result.success(2)))
assertTrue(combined is Result.Success)
assertEquals(listOf(1, 2), (combined as Result.Success).value)
val combinedFail = Result.combine(listOf(f1 as Result<Int>, Result.success(3), Result.failure(ErrorDto(ErrorCode("E3"), ""))))
assertTrue(combinedFail is Result.Failure)
assertEquals(2, (combinedFail as Result.Failure).errors.size)
}
@Test
fun `runCatching failure conversion failureFromValidation and recovery`() {
val ok = Result.runCatching { "ok" }
assertTrue(ok is Result.Success)
val iae = Result.runCatching<String> { throw IllegalArgumentException("bad") }
assertTrue(iae is Result.Failure)
assertEquals("INVALID_ARGUMENT", (iae as Result.Failure).errors.first().code.value)
val generic = Result.runCatching<String> { throw Exception("x") }
assertTrue(generic is Result.Failure)
val verrs = listOf(ValidationError.required("name"), ValidationError.invalidFormat("email"))
val fromVal: Result<Unit> = Result.failure(verrs)
assertTrue(fromVal is Result.Failure)
assertEquals("REQUIRED", (fromVal as Result.Failure).errors.first().code.value)
val rec = Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recover { _ -> "fallback" }
assertTrue(rec is Result.Success)
val recFail = Result.failure<String>(ErrorDto(ErrorCode("E"), "")).recoverCatching { _ -> throw IllegalStateException("boom") }
assertTrue(recFail is Result.Failure)
assertEquals("RECOVERY_FAILED", (recFail as Result.Failure).errors.first().code.value)
}
@Test
fun `getOrNull default throw and toResult`() {
val s = Result.success(5)
assertEquals(5, s.getOrNull())
val f: Result<Int> = Result.failure(ErrorDto(ErrorCode("E"), ""))
assertNull(f.getOrNull())
assertEquals(7, f.getOrDefault(7))
assertEquals(5, s.getOrThrow())
try {
f.getOrThrow()
fail("should throw")
} catch (e: IllegalStateException) {
// ok
}
val nullable: Int? = null
val r = nullable.toResult("ist leer")
assertTrue(r is Result.Failure)
val r2 = 3.toResult()
assertTrue(r2 is Result.Success)
}
val r2 = 3.toResult()
assertTrue(r2 is Result.Success)
}
}
@@ -1,12 +1,13 @@
package at.mocode.core.utils
import at.mocode.core.domain.model.ErrorCode
import at.mocode.core.domain.model.ErrorCodes
import at.mocode.core.domain.model.ErrorDto
import at.mocode.core.domain.model.PagedResponse
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.BatchInsertStatement
import org.jetbrains.exposed.sql.transactions.transaction
import java.sql.SQLException
import java.sql.SQLTimeoutException
/**
* JVM-specific database utilities for the Core module.
@@ -22,61 +23,71 @@ import java.sql.SQLException
* @return A Result containing either the operation result or error information
*/
inline fun <T> transactionResult(
database: Database? = null,
crossinline block: Transaction.() -> T
database: Database? = null,
crossinline block: Transaction.() -> T
): Result<T> {
return try {
val result = transaction(database) { block() }
Result.success(result)
} catch (e: SQLException) {
// Handle specific SQL exceptions
val errorCode = when {
e.message?.contains("constraint", ignoreCase = true) == true -> "CONSTRAINT_VIOLATION"
e.message?.contains("duplicate", ignoreCase = true) == true -> "DUPLICATE_ENTRY"
e.message?.contains("timeout", ignoreCase = true) == true -> "DATABASE_TIMEOUT"
else -> "DATABASE_ERROR"
}
Result.failure(
ErrorDto(
code = ErrorCode(errorCode),
message = "Database operation failed: ${e.message}"
)
)
} catch (e: Exception) {
Result.failure(
ErrorDto(
code = ErrorCode("TRANSACTION_ERROR"),
message = "Transaction failed: ${e.message ?: "Unknown error"}"
)
)
return try {
val result = transaction(database) { block() }
Result.success(result)
} catch (e: SQLTimeoutException) {
Result.failure(
ErrorDto(
code = ErrorCodes.DATABASE_TIMEOUT,
message = "Datenbank-Operation wegen Timeout fehlgeschlagen"
)
)
} catch (e: SQLException) {
// Robustere Fehlerbehandlung über SQLSTATE (Postgres)
val mapped = when (e.sqlState) {
// unique_violation
"23505" -> ErrorCodes.DUPLICATE_ENTRY
// foreign_key_violation
"23503" -> ErrorCodes.FOREIGN_KEY_VIOLATION
// check_violation
"23514" -> ErrorCodes.CHECK_VIOLATION
else -> ErrorCodes.DATABASE_ERROR
}
Result.failure(
ErrorDto(
code = mapped,
message = "Datenbank-Operation fehlgeschlagen"
)
)
} catch (e: Exception) {
Result.failure(
ErrorDto(
code = ErrorCodes.TRANSACTION_ERROR,
message = "Transaktion fehlgeschlagen"
)
)
}
}
/**
* Executes a write database operation.
*/
inline fun <T> writeTransaction(
database: Database? = null,
crossinline block: Transaction.() -> T
database: Database? = null,
crossinline block: Transaction.() -> T
): Result<T> = transactionResult(database, block)
/**
* Executes a read database operation.
*/
inline fun <T> readTransaction(
database: Database? = null,
crossinline block: Transaction.() -> T
database: Database? = null,
crossinline block: Transaction.() -> T
): Result<T> = transactionResult(database, block)
/**
* Extension function for Query-Builder to add pagination.
*/
fun Query.paginate(page: Int, size: Int): Query {
require(page >= 0) { "Page number must be non-negative" }
require(size > 0) { "Page size must be positive" }
require(page >= 0) { "Page number must be non-negative" }
require(size > 0) { "Page size must be positive" }
return limit(size).offset(start = (page * size).toLong())
return limit(size).offset(start = (page * size).toLong())
}
/**
@@ -89,48 +100,48 @@ fun Query.paginate(page: Int, size: Int): Query {
* @return A PagedResponse containing the paginated and transformed data
*/
fun <T> Query.toPagedResponse(
page: Int,
size: Int,
transform: (ResultRow) -> T
page: Int,
size: Int,
transform: (ResultRow) -> T
): PagedResponse<T> {
// Validate input parameters
require(page >= 0) { "Page number must be non-negative" }
require(size > 0) { "Page size must be positive" }
// Validate input parameters
require(page >= 0) { "Page number must be non-negative" }
require(size > 0) { "Page size must be positive" }
// Calculate the total count first (executes a COUNT query)
val totalCount = this.count()
// If there are no results, return an empty page
if (totalCount == 0L) {
return PagedResponse.create(
content = emptyList(),
page = page,
size = size,
totalElements = 0,
totalPages = 0,
hasNext = false,
hasPrevious = page > 0
)
}
// Calculate total pages - use ceil division to ensure we round up
val totalPages = ((totalCount + size - 1) / size).toInt()
// Ensure the requested page exists (if page is beyond available pages, return the last page)
val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page
// Then apply pagination and transform results
val content = this.paginate(adjustedPage, size).map(transform)
// Calculate the total count first (executes a COUNT query)
val totalCount = this.count()
// If there are no results, return an empty page
if (totalCount == 0L) {
return PagedResponse.create(
content = content,
page = adjustedPage,
size = size,
totalElements = totalCount,
totalPages = totalPages,
hasNext = adjustedPage < totalPages - 1,
hasPrevious = adjustedPage > 0
content = emptyList(),
page = page,
size = size,
totalElements = 0,
totalPages = 0,
hasNext = false,
hasPrevious = page > 0
)
}
// Calculate total pages - use ceil division to ensure we round up
val totalPages = ((totalCount + size - 1) / size).toInt()
// Ensure the requested page exists (if page is beyond available pages, return the last page)
val adjustedPage = if (page >= totalPages) (totalPages - 1).coerceAtLeast(0) else page
// Then apply pagination and transform results
val content = this.paginate(adjustedPage, size).map(transform)
return PagedResponse.create(
content = content,
page = adjustedPage,
size = size,
totalElements = totalCount,
totalPages = totalPages,
hasNext = adjustedPage < totalPages - 1,
hasPrevious = adjustedPage > 0
)
}
/**
@@ -138,64 +149,93 @@ fun <T> Query.toPagedResponse(
*/
object DatabaseUtils {
/**
* Checks if a table exists.
* Uses a safe query approach to verify table existence.
*/
fun tableExists(tableName: String, database: Database? = null): Boolean {
return try {
transaction(database) {
// Execute a safer SQL statement to check if table exists
val result = exec("SELECT 1 FROM information_schema.tables WHERE table_name = '$tableName' LIMIT 1")
// If the query returns a result, the table exists
result != null
}
} catch (e: Exception) {
false
}
/**
* Checks if a table exists.
* Uses a safe query approach to verify table existence.
*/
fun tableExists(tableName: String, database: Database? = null): Boolean {
return try {
transaction(database) {
// Postgres-spezifischer, robuster Ansatz über to_regclass
val valid = tableName.trim()
if (!valid.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) return@transaction false
exec("SELECT to_regclass('$valid')") { rs ->
if (rs.next()) rs.getString(1) else null
} != null
}
} catch (e: Exception) {
false
}
}
/**
* Creates an index if it doesn't exist.
*/
fun createIndexIfNotExists(
tableName: String,
indexName: String,
columns: Array<String>,
unique: Boolean = false,
database: Database? = null
): Result<Unit> {
return transactionResult(database) {
val uniqueStr = if (unique) "UNIQUE" else ""
val columnsStr = columns.joinToString(", ")
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $indexName ON $tableName ($columnsStr)"
exec(sql)
}
}
/**
* Creates an index if it doesn't exist.
*/
@JvmName("createIndexIfNotExistsArray")
fun createIndexIfNotExists(
tableName: String,
indexName: String,
columns: Array<String>,
unique: Boolean = false,
database: Database? = null
): Result<Unit> = createIndexIfNotExists(tableName, indexName, *columns, unique = unique, database = database)
/**
* Executes a raw SQL query and returns the number of affected rows.
*/
fun executeRawSql(sql: String, database: Database? = null): Result<Int> {
return transactionResult(database) {
(exec(sql) ?: 0) as Int
}
}
@JvmName("createIndexIfNotExistsVararg")
fun createIndexIfNotExists(
tableName: String,
indexName: String,
vararg columns: String,
unique: Boolean = false,
database: Database? = null
): Result<Unit> {
return transactionResult(database) {
// Einfache Sanitization + Quoting der Identifier
fun quoteIdent(name: String): String {
require(name.matches(Regex("^[A-Za-z_][A-Za-z0-9_]*$"))) { "Ungültiger Identifier: $name" }
return "\"$name\""
}
/**
* Helper function for batch inserts.
*/
inline fun <T> batchInsert(
table: Table,
data: Iterable<T>,
crossinline body: BatchInsertStatement.(T) -> Unit
): Result<List<ResultRow>> {
return transactionResult {
table.batchInsert(data) { item ->
body(item)
}
}
val uniqueStr = if (unique) "UNIQUE" else ""
val qTable = quoteIdent(tableName)
val qIndex = quoteIdent(indexName)
val cols = columns.map { quoteIdent(it) }.joinToString(", ")
val sql = "CREATE $uniqueStr INDEX IF NOT EXISTS $qIndex ON $qTable ($cols)"
exec(sql)
Unit
}
}
/**
* Führt ein beliebiges SQL-Statement aus (DDL/DML). Liefert keinen Update-Count zurück.
*/
fun executeRawSql(sql: String, database: Database? = null): Result<Unit> = transactionResult(database) {
exec(sql)
Unit
}
/**
* Executes a raw SQL update statement and returns affected rows.
*/
fun executeUpdate(sql: String, database: Database? = null): Result<Int> = transactionResult(database) {
// Nutzt Exposed PreparedStatementApi, kein AutoCloseable
val ps = this.connection.prepareStatement(sql, false)
ps.executeUpdate()
}
/**
* Helper function for batch inserts.
*/
inline fun <T> batchInsert(
table: Table,
data: Iterable<T>,
crossinline body: BatchInsertStatement.(T) -> Unit
): Result<List<ResultRow>> {
return transactionResult {
table.batchInsert(data) { item ->
body(item)
}
}
}
}
/**
@@ -206,11 +246,11 @@ object DatabaseUtils {
* Safely gets a value from a ResultRow.
*/
fun <T> ResultRow.getOrNull(column: Column<T>): T? {
return try {
this[column]
} catch (e: Exception) {
null
}
return try {
this[column]
} catch (e: Exception) {
null
}
}
/**
@@ -218,17 +258,17 @@ fun <T> ResultRow.getOrNull(column: Column<T>): T? {
* Safely handles any exceptions during the conversion process.
*/
fun ResultRow.toMap(): Map<String, Any?> {
val result = mutableMapOf<String, Any?>()
this.fieldIndex.forEach { (expression, _) ->
try {
when (expression) {
is Column<*> -> result[expression.name] = this[expression]
else -> result[expression.toString()] = this[expression]
}
} catch (e: Exception) {
// Ignore columns that can't be read and log the error if needed
// You could add logging here in a production environment
}
val result = mutableMapOf<String, Any?>()
this.fieldIndex.forEach { (expression, _) ->
try {
when (expression) {
is Column<*> -> result[expression.name] = this[expression]
else -> result[expression.toString()] = this[expression]
}
} catch (e: Exception) {
// Ignore columns that can't be read and log the error if needed
// You could add logging here in a production environment
}
return result
}
return result
}