feat(Tracer Bullet)
This commit is contained in:
@@ -1,27 +1,51 @@
|
||||
// Dieses Modul definiert die Kern-Domänenobjekte des Shared Kernels.
|
||||
// Es enthält keine Implementierungsdetails, nur reine Datenklassen und Enums.
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime")
|
||||
// Target platforms
|
||||
jvm {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime")
|
||||
}
|
||||
}
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
// Kern-Abhängigkeiten für das Domänen-Modul (common for all platforms)
|
||||
api(libs.uuid)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.kotlinx.datetime)
|
||||
}
|
||||
}
|
||||
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
|
||||
// definierten Bibliotheken hat (JVM-specific)
|
||||
api(projects.platform.platformDependencies)
|
||||
}
|
||||
}
|
||||
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
// Stellt die Test-Bibliotheken bereit (JVM-specific)
|
||||
implementation(projects.platform.platformTesting)
|
||||
implementation(libs.bundles.testing.jvm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Stellt sicher, dass dieses Modul Zugriff auf die im zentralen Katalog
|
||||
// definierten Bibliotheken hat.
|
||||
api(projects.platform.platformDependencies)
|
||||
|
||||
// Kern-Abhängigkeiten für das Domänen-Modul.
|
||||
api(libs.uuid)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
api(libs.kotlinx.datetime)
|
||||
|
||||
// Stellt die Test-Bibliotheken bereit.
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
}
|
||||
|
||||
+17
-16
@@ -1,5 +1,6 @@
|
||||
package at.mocode.core.domain.event
|
||||
|
||||
import at.mocode.core.domain.model.*
|
||||
import at.mocode.core.domain.serialization.KotlinInstantSerializer
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
@@ -7,38 +8,38 @@ import com.benasher44.uuid.uuid4
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.ExperimentalTime
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
|
||||
/**
|
||||
* Basis-Interface für alle Domänen-Events im System.
|
||||
* Ein Domänen-Event repräsentiert etwas fachlich Bedeutsames, das passiert ist.
|
||||
*/
|
||||
interface DomainEvent {
|
||||
val eventId: Uuid
|
||||
val aggregateId: Uuid
|
||||
val eventType: String
|
||||
val eventId: EventId
|
||||
val aggregateId: AggregateId
|
||||
val eventType: EventType
|
||||
val timestamp: Instant
|
||||
val version: Long
|
||||
val correlationId: Uuid?
|
||||
val causationId: Uuid?
|
||||
val version: EventVersion
|
||||
val correlationId: CorrelationId?
|
||||
val causationId: CausationId?
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstrakte Basisklasse für Domänen-Events, um Boilerplate-Code zu reduzieren.
|
||||
*/
|
||||
@Serializable
|
||||
@OptIn(ExperimentalTime::class)
|
||||
abstract class BaseDomainEvent(
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
override val aggregateId: Uuid,
|
||||
override val eventType: String,
|
||||
override val version: Long,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
override val eventId: Uuid = uuid4(),
|
||||
override val aggregateId: AggregateId,
|
||||
override val eventType: EventType,
|
||||
override val version: EventVersion,
|
||||
override val eventId: EventId = EventId(uuid4()),
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
override val timestamp: Instant = Clock.System.now(),
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
override val correlationId: Uuid? = null,
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
override val causationId: Uuid? = null
|
||||
override val correlationId: CorrelationId? = null,
|
||||
override val causationId: CausationId? = null
|
||||
) : DomainEvent
|
||||
|
||||
/**
|
||||
+7
-2
@@ -5,6 +5,7 @@ import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
@@ -16,9 +17,9 @@ interface BaseDto
|
||||
* Base DTO for domain entities that have unique ID and audit timestamps.
|
||||
*/
|
||||
@Serializable
|
||||
@OptIn(ExperimentalTime::class)
|
||||
abstract class EntityDto : BaseDto {
|
||||
@Serializable(with = UuidSerializer::class)
|
||||
abstract val id: Uuid
|
||||
abstract val id: EntityId
|
||||
|
||||
@Serializable(with = KotlinInstantSerializer::class)
|
||||
abstract val createdAt: Instant
|
||||
@@ -41,6 +42,7 @@ data class ErrorDto(
|
||||
* A standardized and consistent wrapper for all API responses.
|
||||
*/
|
||||
@Serializable
|
||||
@OptIn(ExperimentalTime::class)
|
||||
data class ApiResponse<T>(
|
||||
val data: T?,
|
||||
val success: Boolean,
|
||||
@@ -49,10 +51,12 @@ data class ApiResponse<T>(
|
||||
val timestamp: Instant = Clock.System.now()
|
||||
) {
|
||||
companion object {
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun <T> success(data: T): ApiResponse<T> {
|
||||
return ApiResponse(data = data, success = true)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun <T> error(
|
||||
code: String,
|
||||
message: String,
|
||||
@@ -65,6 +69,7 @@ data class ApiResponse<T>(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun <T> error(errors: List<ErrorDto>): ApiResponse<T> {
|
||||
return ApiResponse(data = null, success = false, errors = errors)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package at.mocode.core.domain.model
|
||||
|
||||
import at.mocode.core.domain.serialization.UuidSerializer
|
||||
import com.benasher44.uuid.Uuid
|
||||
import kotlin.jvm.JvmInline
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Value classes for strongly typed IDs and domain values.
|
||||
* These provide compile-time type safety without runtime overhead.
|
||||
*/
|
||||
|
||||
// === ID Value Classes ===
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for entity IDs.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class EntityId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for event IDs.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class EventId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for aggregate IDs.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class AggregateId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for correlation IDs used in event tracing.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class CorrelationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for causation IDs used in event tracing.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class CausationId(@Serializable(with = UuidSerializer::class) val value: Uuid) {
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
// === Domain Value Classes ===
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for event types.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class EventType(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Event type cannot be blank" }
|
||||
require(value.matches(Regex("^[A-Za-z][A-Za-z0-9]*$"))) {
|
||||
"Event type must start with a letter and contain only alphanumeric characters"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for event version numbers.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class EventVersion(val value: Long) : Comparable<EventVersion> {
|
||||
init {
|
||||
require(value >= 0) { "Event version must be non-negative" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
|
||||
override fun compareTo(other: EventVersion): Int = value.compareTo(other.value)
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for error codes.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class ErrorCode(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Error code cannot be blank" }
|
||||
require(value.matches(Regex("^[A-Z][A-Z0-9_]*$"))) {
|
||||
"Error code must be uppercase and contain only letters, numbers, and underscores"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for page numbers in pagination.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PageNumber(val value: Int) {
|
||||
init {
|
||||
require(value >= 0) { "Page number must be non-negative" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for page sizes in pagination.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PageSize(val value: Int) {
|
||||
init {
|
||||
require(value > 0) { "Page size must be positive" }
|
||||
require(value <= 1000) { "Page size cannot exceed 1000" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
+2
@@ -3,6 +3,7 @@ package at.mocode.core.domain.serialization
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuidFrom
|
||||
import kotlin.time.Instant // KORRIGIERT: Finaler Wechsel zu kotlin.time
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.LocalTime
|
||||
@@ -19,6 +20,7 @@ object UuidSerializer : KSerializer<Uuid> {
|
||||
override fun deserialize(decoder: Decoder): Uuid = uuidFrom(decoder.decodeString())
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
object KotlinInstantSerializer : KSerializer<Instant> {
|
||||
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.STRING)
|
||||
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString())
|
||||
@@ -1,6 +1,7 @@
|
||||
package at.mocode.core.domain
|
||||
|
||||
import at.mocode.core.domain.event.BaseDomainEvent
|
||||
import at.mocode.core.domain.model.*
|
||||
import com.benasher44.uuid.Uuid
|
||||
import com.benasher44.uuid.uuid4
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -22,21 +23,21 @@ class DomainEventTest {
|
||||
@Serializable
|
||||
data class TestEvent(
|
||||
@Transient
|
||||
override val aggregateId: Uuid = uuid4(),
|
||||
override val aggregateId: AggregateId = AggregateId(uuid4()),
|
||||
@Transient
|
||||
override val version: Long = 1L,
|
||||
override val version: EventVersion = EventVersion(1L),
|
||||
val testPayload: String = "Test"
|
||||
) : BaseDomainEvent(
|
||||
aggregateId = aggregateId,
|
||||
eventType = "TestEventOccurred", // Ein klar definierter Event-Typ
|
||||
eventType = EventType("TestEventOccurred"), // Ein klar definierter Event-Typ
|
||||
version = version
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `BaseDomainEvent should auto-generate eventId and timestamp upon creation`() {
|
||||
// Arrange
|
||||
val aggregateId = uuid4()
|
||||
val version = 1L
|
||||
val aggregateId = AggregateId(uuid4())
|
||||
val version = EventVersion(1L)
|
||||
|
||||
// Act
|
||||
val event = TestEvent(aggregateId, version)
|
||||
@@ -46,6 +47,6 @@ class DomainEventTest {
|
||||
assertNotNull(event.timestamp, "timestamp should be automatically generated and not null")
|
||||
assertEquals(aggregateId, event.aggregateId, "aggregateId should be set correctly")
|
||||
assertEquals(version, event.version, "version should be set correctly")
|
||||
assertEquals("TestEventOccurred", event.eventType, "eventType should be set correctly")
|
||||
assertEquals(EventType("TestEventOccurred"), event.eventType, "eventType should be set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,44 +1,70 @@
|
||||
// Dieses Modul stellt gemeinsame technische Hilfsfunktionen bereit,
|
||||
// wie z.B. Konfigurations-Management, Datenbank-Verbindungen und Service Discovery.
|
||||
plugins {
|
||||
alias(libs.plugins.kotlin.jvm)
|
||||
alias(libs.plugins.kotlin.multiplatform)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
// Target platforms
|
||||
jvm {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
}
|
||||
}
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
// Abhängigkeit zum core-domain-Modul, um dessen Typen zu verwenden
|
||||
api(projects.core.coreDomain)
|
||||
|
||||
// Asynchronität (available for all platforms) - explicit version to avoid BOM issues
|
||||
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
|
||||
|
||||
// Utilities (multiplatform compatible)
|
||||
api(libs.bignum)
|
||||
}
|
||||
}
|
||||
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
// Abhängigkeit zum platform-Modul für zentrale Versionsverwaltung
|
||||
api(projects.platform.platformDependencies)
|
||||
|
||||
// Datenbank-Management (JVM-specific)
|
||||
// OPTIMIERUNG: Verwendung von Bundles für Exposed und Flyway
|
||||
api(libs.bundles.exposed)
|
||||
api(libs.bundles.flyway)
|
||||
api(libs.hikari.cp)
|
||||
|
||||
// Service Discovery (JVM-specific)
|
||||
// api(libs.consul.client) wird getauscht mir spring-cloud-starter-consul-discovery
|
||||
api(libs.spring.cloud.starter.consul.discovery)
|
||||
|
||||
// Logging (JVM-specific)
|
||||
api(libs.kotlin.logging.jvm)
|
||||
|
||||
// JVM-specific utilities
|
||||
implementation(libs.room.common.jvm) // Für BigDecimal Serialisierung
|
||||
}
|
||||
}
|
||||
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(libs.kotlin.test)
|
||||
}
|
||||
}
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
// Testing (JVM-specific)
|
||||
implementation(projects.platform.platformTesting)
|
||||
implementation(libs.bundles.testing.jvm)
|
||||
runtimeOnly(libs.postgresql.driver)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Abhängigkeit zum platform-Modul für zentrale Versionsverwaltung
|
||||
api(projects.platform.platformDependencies)
|
||||
// Abhängigkeit zum core-domain-Modul, um dessen Typen zu verwenden
|
||||
api(projects.core.coreDomain)
|
||||
|
||||
// Asynchronität
|
||||
api(libs.kotlinx.coroutines.core)
|
||||
|
||||
// Datenbank-Management
|
||||
// OPTIMIERUNG: Verwendung von Bundles für Exposed und Flyway
|
||||
api(libs.bundles.exposed)
|
||||
api(libs.bundles.flyway)
|
||||
api(libs.hikari.cp)
|
||||
|
||||
// Service Discovery
|
||||
// api(libs.consul.client) wird getauscht mir spring-cloud-starter-consul-discovery
|
||||
api(libs.spring.cloud.starter.consul.discovery)
|
||||
|
||||
// Logging
|
||||
api(libs.kotlin.logging.jvm)
|
||||
|
||||
// Utilities
|
||||
api(libs.bignum)
|
||||
implementation(libs.room.common.jvm) // Für BigDecimal Serialisierung
|
||||
|
||||
// Testing
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
testImplementation(libs.bundles.testing.jvm)
|
||||
testImplementation(libs.kotlin.test)
|
||||
testRuntimeOnly(libs.postgresql.driver)
|
||||
}
|
||||
|
||||
@@ -15,43 +15,55 @@ data class AppConfig(
|
||||
val rateLimit: RateLimitConfig
|
||||
)
|
||||
|
||||
data class AppInfoConfig(val name: String, val version: String, val description: String)
|
||||
data class AppInfoConfig(
|
||||
val name: ApplicationName,
|
||||
val version: ApplicationVersion,
|
||||
val description: String
|
||||
)
|
||||
|
||||
data class ServerConfig(
|
||||
val port: Int,
|
||||
val host: String,
|
||||
val advertisedHost: String,
|
||||
val workers: Int,
|
||||
val port: Port,
|
||||
val host: Host,
|
||||
val advertisedHost: Host,
|
||||
val workers: WorkerCount,
|
||||
val cors: CorsConfig
|
||||
) {
|
||||
data class CorsConfig(val enabled: Boolean, val allowedOrigins: List<String>)
|
||||
}
|
||||
|
||||
data class DatabaseConfig(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
val name: String,
|
||||
val jdbcUrl: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val host: Host,
|
||||
val port: Port,
|
||||
val name: DatabaseName,
|
||||
val jdbcUrl: JdbcUrl,
|
||||
val username: DatabaseUsername,
|
||||
val password: DatabasePassword,
|
||||
val driverClassName: String,
|
||||
val maxPoolSize: Int,
|
||||
val minPoolSize: Int,
|
||||
val maxPoolSize: PoolSize,
|
||||
val minPoolSize: PoolSize,
|
||||
val autoMigrate: Boolean
|
||||
)
|
||||
|
||||
data class ServiceDiscoveryConfig(val enabled: Boolean, val consulHost: String, val consulPort: Int)
|
||||
data class ServiceDiscoveryConfig(
|
||||
val enabled: Boolean,
|
||||
val consulHost: Host,
|
||||
val consulPort: Port
|
||||
)
|
||||
|
||||
data class SecurityConfig(val jwt: JwtConfig, val apiKey: String?) {
|
||||
data class SecurityConfig(val jwt: JwtConfig, val apiKey: ApiKey?) {
|
||||
data class JwtConfig(
|
||||
val secret: String,
|
||||
val issuer: String,
|
||||
val audience: String,
|
||||
val realm: String,
|
||||
val secret: JwtSecret,
|
||||
val issuer: JwtIssuer,
|
||||
val audience: JwtAudience,
|
||||
val realm: JwtRealm,
|
||||
val expirationInMinutes: Long
|
||||
)
|
||||
}
|
||||
|
||||
data class LoggingConfig(val level: String, val logRequests: Boolean, val logResponses: Boolean)
|
||||
|
||||
data class RateLimitConfig(val enabled: Boolean, val globalLimit: Int, val globalPeriodMinutes: Int)
|
||||
data class RateLimitConfig(
|
||||
val enabled: Boolean,
|
||||
val globalLimit: RateLimit,
|
||||
val globalPeriodMinutes: PeriodMinutes
|
||||
)
|
||||
|
||||
@@ -53,8 +53,8 @@ class ConfigLoader(private val configPath: String = "config") {
|
||||
|
||||
// Die Konfigurations-Erstellungslogik ist hierher verschoben
|
||||
private fun createAppInfoConfig(props: Properties) = AppInfoConfig(
|
||||
name = props.getProperty("app.name", "Meldestelle"),
|
||||
version = props.getProperty("app.version", "1.0.0"),
|
||||
name = ApplicationName(props.getProperty("app.name", "Meldestelle")),
|
||||
version = ApplicationVersion(props.getProperty("app.version", "1.0.0")),
|
||||
description = props.getProperty("app.description", "Pferdesport Meldestelle System")
|
||||
)
|
||||
|
||||
@@ -65,10 +65,10 @@ class ConfigLoader(private val configPath: String = "config") {
|
||||
"127.0.0.1"
|
||||
}
|
||||
return ServerConfig(
|
||||
port = props.getIntProperty("server.port", "API_PORT", 8081),
|
||||
host = props.getStringProperty("server.host", "API_HOST", "0.0.0.0"),
|
||||
advertisedHost = props.getStringProperty("server.advertisedHost", "API_HOST_ADVERTISED", defaultHost),
|
||||
workers = props.getIntProperty("server.workers", "API_WORKERS", Runtime.getRuntime().availableProcessors()),
|
||||
port = Port(props.getIntProperty("server.port", "API_PORT", 8081)),
|
||||
host = Host(props.getStringProperty("server.host", "API_HOST", "0.0.0.0")),
|
||||
advertisedHost = Host(props.getStringProperty("server.advertisedHost", "API_HOST_ADVERTISED", defaultHost)),
|
||||
workers = WorkerCount(props.getIntProperty("server.workers", "API_WORKERS", Runtime.getRuntime().availableProcessors())),
|
||||
cors = ServerConfig.CorsConfig(
|
||||
enabled = props.getBooleanProperty("server.cors.enabled", "API_CORS_ENABLED", true),
|
||||
allowedOrigins = props.getProperty("server.cors.allowedOrigins")?.split(",")?.map { it.trim() }
|
||||
@@ -82,15 +82,15 @@ class ConfigLoader(private val configPath: String = "config") {
|
||||
val port = props.getIntProperty("database.port", "DB_PORT", 5432)
|
||||
val name = props.getStringProperty("database.name", "DB_NAME", "meldestelle_db")
|
||||
return DatabaseConfig(
|
||||
host = host,
|
||||
port = port,
|
||||
name = name,
|
||||
jdbcUrl = "jdbc:postgresql://$host:$port/$name",
|
||||
username = props.getStringProperty("database.username", "DB_USER", "meldestelle_user"),
|
||||
password = props.getStringProperty("database.password", "DB_PASSWORD", "secure_password_change_me"),
|
||||
host = Host(host),
|
||||
port = Port(port),
|
||||
name = DatabaseName(name),
|
||||
jdbcUrl = JdbcUrl("jdbc:postgresql://$host:$port/$name"),
|
||||
username = DatabaseUsername(props.getStringProperty("database.username", "DB_USER", "meldestelle_user")),
|
||||
password = DatabasePassword(props.getStringProperty("database.password", "DB_PASSWORD", "secure_password_change_me")),
|
||||
driverClassName = "org.postgresql.Driver",
|
||||
maxPoolSize = props.getIntProperty("database.maxPoolSize", "DB_MAX_POOL_SIZE", 10),
|
||||
minPoolSize = props.getIntProperty("database.minPoolSize", "DB_MIN_POOL_SIZE", 5),
|
||||
maxPoolSize = PoolSize(props.getIntProperty("database.maxPoolSize", "DB_MAX_POOL_SIZE", 10)),
|
||||
minPoolSize = PoolSize(props.getIntProperty("database.minPoolSize", "DB_MIN_POOL_SIZE", 5)),
|
||||
autoMigrate = props.getBooleanProperty("database.autoMigrate", "DB_AUTO_MIGRATE", true)
|
||||
)
|
||||
}
|
||||
@@ -99,27 +99,27 @@ class ConfigLoader(private val configPath: String = "config") {
|
||||
// analog zu den 'fromProperties' Methoden aus der alten AppConfig.
|
||||
private fun createServiceDiscoveryConfig(props: Properties) = ServiceDiscoveryConfig(
|
||||
enabled = props.getBooleanProperty("service-discovery.enabled", "CONSUL_ENABLED", true),
|
||||
consulHost = props.getStringProperty("service-discovery.consul.host", "CONSUL_HOST", "consul"),
|
||||
consulPort = props.getIntProperty("service-discovery.consul.port", "CONSUL_PORT", 8500)
|
||||
consulHost = Host(props.getStringProperty("service-discovery.consul.host", "CONSUL_HOST", "consul")),
|
||||
consulPort = Port(props.getIntProperty("service-discovery.consul.port", "CONSUL_PORT", 8500))
|
||||
)
|
||||
|
||||
private fun createSecurityConfig(props: Properties) = SecurityConfig(
|
||||
jwt = SecurityConfig.JwtConfig(
|
||||
secret = props.getStringProperty(
|
||||
secret = JwtSecret(props.getStringProperty(
|
||||
"security.jwt.secret",
|
||||
"JWT_SECRET",
|
||||
"default-secret-please-change-in-production"
|
||||
),
|
||||
issuer = props.getStringProperty("security.jwt.issuer", "JWT_ISSUER", "meldestelle-api"),
|
||||
audience = props.getStringProperty("security.jwt.audience", "JWT_AUDIENCE", "meldestelle-clients"),
|
||||
realm = props.getStringProperty("security.jwt.realm", "JWT_REALM", "meldestelle"),
|
||||
)),
|
||||
issuer = JwtIssuer(props.getStringProperty("security.jwt.issuer", "JWT_ISSUER", "meldestelle-api")),
|
||||
audience = JwtAudience(props.getStringProperty("security.jwt.audience", "JWT_AUDIENCE", "meldestelle-clients")),
|
||||
realm = JwtRealm(props.getStringProperty("security.jwt.realm", "JWT_REALM", "meldestelle")),
|
||||
expirationInMinutes = props.getLongProperty(
|
||||
"security.jwt.expirationInMinutes",
|
||||
"JWT_EXPIRATION_MINUTES",
|
||||
60 * 24
|
||||
)
|
||||
),
|
||||
apiKey = props.getStringProperty("security.apiKey", "API_KEY", "").ifEmpty { null }
|
||||
apiKey = props.getStringProperty("security.apiKey", "API_KEY", "").ifEmpty { null }?.let { ApiKey(it) }
|
||||
)
|
||||
|
||||
private fun createLoggingConfig(props: Properties, env: AppEnvironment) = LoggingConfig(
|
||||
@@ -130,7 +130,7 @@ class ConfigLoader(private val configPath: String = "config") {
|
||||
|
||||
private fun createRateLimitConfig(props: Properties) = RateLimitConfig(
|
||||
enabled = props.getBooleanProperty("ratelimit.enabled", "RATE_LIMIT_ENABLED", true),
|
||||
globalLimit = props.getIntProperty("ratelimit.global.limit", "RATE_LIMIT_GLOBAL_LIMIT", 100),
|
||||
globalPeriodMinutes = props.getIntProperty("ratelimit.global.periodMinutes", "RATE_LIMIT_GLOBAL_PERIOD", 1)
|
||||
globalLimit = RateLimit(props.getIntProperty("ratelimit.global.limit", "RATE_LIMIT_GLOBAL_LIMIT", 100)),
|
||||
globalPeriodMinutes = PeriodMinutes(props.getIntProperty("ratelimit.global.periodMinutes", "RATE_LIMIT_GLOBAL_PERIOD", 1))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
package at.mocode.core.utils.config
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Value classes for strongly typed configuration parameters.
|
||||
* These provide compile-time type safety for configuration values.
|
||||
*/
|
||||
|
||||
// === Network Configuration Value Classes ===
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for port numbers.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class Port(val value: Int) {
|
||||
init {
|
||||
require(value in 1..65535) { "Port must be between 1 and 65535, got: $value" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for host names or IP addresses.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class Host(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Host cannot be blank" }
|
||||
require(value.length <= 253) { "Host name cannot exceed 253 characters" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
// === Database Configuration Value Classes ===
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for database names.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class DatabaseName(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Database name cannot be blank" }
|
||||
require(value.matches(Regex("^[a-zA-Z][a-zA-Z0-9_]*$"))) {
|
||||
"Database name must start with a letter and contain only alphanumeric characters and underscores"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for database usernames.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class DatabaseUsername(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Database username cannot be blank" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for database passwords.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class DatabasePassword(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Database password cannot be blank" }
|
||||
}
|
||||
|
||||
override fun toString(): String = "***" // Never expose the actual password
|
||||
|
||||
fun getValue(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for JDBC URLs.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class JdbcUrl(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "JDBC URL cannot be blank" }
|
||||
require(value.startsWith("jdbc:")) { "JDBC URL must start with 'jdbc:'" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for connection pool sizes.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PoolSize(val value: Int) {
|
||||
init {
|
||||
require(value > 0) { "Pool size must be positive" }
|
||||
require(value <= 1000) { "Pool size cannot exceed 1000" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
// === Security Configuration Value Classes ===
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for API keys.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class ApiKey(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "API key cannot be blank" }
|
||||
require(value.length >= 16) { "API key must be at least 16 characters long" }
|
||||
}
|
||||
|
||||
override fun toString(): String = "***" // Never expose the actual key
|
||||
|
||||
fun getValue(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for JWT secrets.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class JwtSecret(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "JWT secret cannot be blank" }
|
||||
require(value.length >= 32) { "JWT secret must be at least 32 characters long" }
|
||||
}
|
||||
|
||||
override fun toString(): String = "***" // Never expose the actual secret
|
||||
|
||||
fun getValue(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for JWT issuer.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class JwtIssuer(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "JWT issuer cannot be blank" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for JWT audience.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class JwtAudience(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "JWT audience cannot be blank" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for JWT realm.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class JwtRealm(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "JWT realm cannot be blank" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
// === Application Configuration Value Classes ===
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for application names.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class ApplicationName(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Application name cannot be blank" }
|
||||
require(value.matches(Regex("^[A-Za-z][A-Za-z0-9-_]*$"))) {
|
||||
"Application name must start with a letter and contain only letters, numbers, hyphens, and underscores"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for application versions.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class ApplicationVersion(val value: String) {
|
||||
init {
|
||||
require(value.isNotBlank()) { "Application version cannot be blank" }
|
||||
require(value.matches(Regex("^\\d+\\.\\d+\\.\\d+(-[a-zA-Z0-9]+)?$"))) {
|
||||
"Application version must follow semantic versioning (e.g., 1.0.0 or 1.0.0-beta)"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for worker thread counts.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class WorkerCount(val value: Int) {
|
||||
init {
|
||||
require(value > 0) { "Worker count must be positive" }
|
||||
require(value <= Runtime.getRuntime().availableProcessors() * 4) {
|
||||
"Worker count should not exceed 4 times the available processors"
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for rate limits.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class RateLimit(val value: Int) {
|
||||
init {
|
||||
require(value > 0) { "Rate limit must be positive" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* A strongly typed wrapper for time periods in minutes.
|
||||
*/
|
||||
@Serializable
|
||||
@JvmInline
|
||||
value class PeriodMinutes(val value: Int) {
|
||||
init {
|
||||
require(value > 0) { "Period must be positive" }
|
||||
}
|
||||
|
||||
override fun toString(): String = value.toString()
|
||||
}
|
||||
@@ -51,11 +51,11 @@ class DatabaseFactory(private val config: DatabaseConfig) {
|
||||
private fun createHikariConfig(): HikariConfig {
|
||||
return HikariConfig().apply {
|
||||
driverClassName = config.driverClassName
|
||||
jdbcUrl = config.jdbcUrl
|
||||
username = config.username
|
||||
password = config.password
|
||||
maximumPoolSize = config.maxPoolSize
|
||||
minimumIdle = config.minPoolSize
|
||||
jdbcUrl = config.jdbcUrl.value
|
||||
username = config.username.value
|
||||
password = config.password.getValue() // Use getValue() for password to access actual value
|
||||
maximumPoolSize = config.maxPoolSize.value
|
||||
minimumIdle = config.minPoolSize.value
|
||||
isAutoCommit = false
|
||||
transactionIsolation = "TRANSACTION_READ_COMMITTED"
|
||||
validationTimeout = 5000
|
||||
|
||||
@@ -31,8 +31,8 @@ class ConfigLoaderTest {
|
||||
val config = configLoader.load(AppEnvironment.DEVELOPMENT)
|
||||
|
||||
// Assert
|
||||
assertEquals("Meldestelle", config.appInfo.name)
|
||||
assertEquals(8081, config.server.port) // Standard-Port
|
||||
assertEquals("Meldestelle", config.appInfo.name.value)
|
||||
assertEquals(8081, config.server.port.value) // Standard-Port
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -53,8 +53,8 @@ class ConfigLoaderTest {
|
||||
val config = configLoader.load(AppEnvironment.DEVELOPMENT)
|
||||
|
||||
// Assert
|
||||
assertEquals("TestApp", config.appInfo.name)
|
||||
assertEquals(9999, config.server.port)
|
||||
assertEquals("TestApp", config.appInfo.name.value)
|
||||
assertEquals(9999, config.server.port.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -83,8 +83,8 @@ class ConfigLoaderTest {
|
||||
|
||||
// Assert
|
||||
assertEquals(AppEnvironment.TEST, config.environment, "Environment should be TEST")
|
||||
assertEquals("TestEnvApp", config.appInfo.name, "app.name should be overridden")
|
||||
assertEquals(9000, config.server.port, "server.port should be overridden")
|
||||
assertEquals("base-db-host", config.database.host, "database.host should come from the base file")
|
||||
assertEquals("TestEnvApp", config.appInfo.name.value, "app.name should be overridden")
|
||||
assertEquals(9000, config.server.port.value, "server.port should be overridden")
|
||||
assertEquals("base-db-host", config.database.host.value, "database.host should come from the base file")
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -1,6 +1,6 @@
|
||||
package at.mocode.core.utils.database
|
||||
|
||||
import at.mocode.core.utils.config.DatabaseConfig
|
||||
import at.mocode.core.utils.config.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jetbrains.exposed.sql.SchemaUtils
|
||||
import org.jetbrains.exposed.sql.Table
|
||||
@@ -23,7 +23,7 @@ class DatabaseFactoryTest {
|
||||
companion object {
|
||||
@Container
|
||||
val postgresContainer = PostgreSQLContainer<Nothing>("postgres:16-alpine").apply {
|
||||
withDatabaseName("test-db")
|
||||
withDatabaseName("testdb")
|
||||
withUsername("test-user")
|
||||
withPassword("test-password")
|
||||
}
|
||||
@@ -37,15 +37,15 @@ class DatabaseFactoryTest {
|
||||
fun setup() {
|
||||
// Erstelle eine DB-Konfiguration mit den dynamischen Daten des gestarteten Containers
|
||||
dbConfig = DatabaseConfig(
|
||||
host = postgresContainer.host,
|
||||
port = postgresContainer.firstMappedPort,
|
||||
name = postgresContainer.databaseName,
|
||||
jdbcUrl = postgresContainer.jdbcUrl,
|
||||
username = postgresContainer.username,
|
||||
password = postgresContainer.password,
|
||||
host = Host(postgresContainer.host),
|
||||
port = Port(postgresContainer.firstMappedPort),
|
||||
name = DatabaseName(postgresContainer.databaseName),
|
||||
jdbcUrl = JdbcUrl(postgresContainer.jdbcUrl),
|
||||
username = DatabaseUsername(postgresContainer.username),
|
||||
password = DatabasePassword(postgresContainer.password),
|
||||
driverClassName = "org.postgresql.Driver",
|
||||
maxPoolSize = 2,
|
||||
minPoolSize = 1,
|
||||
maxPoolSize = PoolSize(2),
|
||||
minPoolSize = PoolSize(1),
|
||||
autoMigrate = false // Wir steuern Migrationen im Test manuell
|
||||
)
|
||||
// Erstelle eine neue Factory-Instanz und verbinde sie mit der Test-DB
|
||||
|
||||
Reference in New Issue
Block a user