feat(masterdata): introduce Regulation domain with API, persistence, and metrics integration
- Added `RegulationRepository` and its `Exposed` implementation for persistence. - Implemented REST endpoints for regulations (`/rules`) in `RegulationController`, including support for tournament classes, license matrix, guidelines, fees, and configuration retrieval. - Integrated OpenAPI documentation for `/rules` endpoints with Swagger UI in `masterdataApiModule`. - Enabled Micrometer-based metrics for Prometheus in the API layer. - Updated Gradle dependencies to include OpenAPI, Swagger, and Micrometer libraries. - Registered `RegulationRepository` and `RegulationController` in `MasterdataConfiguration`. - Improved database access patterns and reduced repetitive validation logic across domain services. - Added unit and application tests for `RegulationController` to verify API behavior and repository interactions. - Updated the service's `ROADMAP.md` to mark API v1 endpoints and observability as complete. Signed-off-by: Stefan Mogeritsch <stefan.mo.co@gmail.com>
This commit is contained in:
@@ -24,6 +24,10 @@ dependencies {
|
||||
implementation(libs.ktor.server.statusPages)
|
||||
implementation(libs.ktor.server.auth)
|
||||
implementation(libs.ktor.server.authJwt)
|
||||
implementation(libs.ktor.server.openapi)
|
||||
implementation(libs.ktor.server.swagger)
|
||||
implementation(libs.ktor.server.metrics.micrometer)
|
||||
implementation(libs.micrometer.prometheus)
|
||||
|
||||
// Testing
|
||||
testImplementation(projects.platform.platformTesting)
|
||||
|
||||
+18
-1
@@ -3,11 +3,18 @@ package at.mocode.masterdata.api
|
||||
import at.mocode.masterdata.api.plugins.IdempotencyPlugin
|
||||
import at.mocode.masterdata.api.rest.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.metrics.micrometer.*
|
||||
import io.ktor.server.plugins.openapi.*
|
||||
import io.ktor.server.plugins.swagger.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.micrometer.core.instrument.MeterRegistry
|
||||
import io.micrometer.prometheusmetrics.PrometheusConfig
|
||||
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry
|
||||
|
||||
/**
|
||||
* Ktor-Modul für den Masterdata-Bounded-Context.
|
||||
*
|
||||
* - Installiert Micrometer für Metriken (Prometheus).
|
||||
* - Installiert das IdempotencyPlugin (Header „Idempotency-Key“) global.
|
||||
* - Registriert alle Masterdata-Routen (Country, Bundesland, Altersklasse, Platz, Reiter, Horse, Verein).
|
||||
*/
|
||||
@@ -18,13 +25,22 @@ fun Application.masterdataApiModule(
|
||||
platzController: PlatzController,
|
||||
reiterController: ReiterController,
|
||||
horseController: HorseController,
|
||||
vereinController: VereinController
|
||||
vereinController: VereinController,
|
||||
regulationController: RegulationController,
|
||||
meterRegistry: MeterRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
|
||||
) {
|
||||
// Installiere Micrometer für Ktor-Metriken (Latenzen, Counts etc.)
|
||||
install(MicrometerMetrics) {
|
||||
registry = meterRegistry
|
||||
}
|
||||
|
||||
// Installiere das Idempotency-Plugin global für alle Routen
|
||||
IdempotencyPlugin.install(this)
|
||||
|
||||
// Registriere die REST-Routen der Controller
|
||||
routing {
|
||||
swaggerUI(path = "swagger", swaggerFile = "openapi/documentation.yaml")
|
||||
|
||||
with(countryController) { registerRoutes() }
|
||||
with(bundeslandController) { registerRoutes() }
|
||||
with(altersklasseController) { registerRoutes() }
|
||||
@@ -32,5 +48,6 @@ fun Application.masterdataApiModule(
|
||||
with(reiterController) { registerRoutes() }
|
||||
with(horseController) { registerRoutes() }
|
||||
with(vereinController) { registerRoutes() }
|
||||
with(regulationController) { registerRoutes() }
|
||||
}
|
||||
}
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.masterdata.domain.repository.RegulationRepository
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
|
||||
/**
|
||||
* Controller für Regel-bezogene REST-Endpunkte (Regulation-as-Data).
|
||||
*/
|
||||
class RegulationController(private val regulationRepository: RegulationRepository) {
|
||||
|
||||
fun Route.registerRoutes() {
|
||||
route("/rules") {
|
||||
/**
|
||||
* Liefert alle Turnierklassen-Definitionen.
|
||||
*/
|
||||
get("/turnierklassen") {
|
||||
val results = regulationRepository.findAllTurnierklassen()
|
||||
call.respond(results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle Lizenz-Matrix-Einträge.
|
||||
*/
|
||||
get("/lizenzmatrix") {
|
||||
val results = regulationRepository.findAllLicenseMatrixEntries()
|
||||
call.respond(results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle Richtverfahren-Definitionen.
|
||||
*/
|
||||
get("/richtverfahren") {
|
||||
val results = regulationRepository.findAllRichtverfahren()
|
||||
call.respond(results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle Gebühren-Definitionen.
|
||||
*/
|
||||
get("/gebuehren") {
|
||||
val results = regulationRepository.findAllGebuehren()
|
||||
call.respond(results)
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert alle Regulation-Konfigurationen.
|
||||
*/
|
||||
get("/config") {
|
||||
val activeOnly = call.request.queryParameters["active"]?.toBoolean() ?: false
|
||||
val results = if (activeOnly) {
|
||||
regulationRepository.findActiveRegulationConfigs()
|
||||
} else {
|
||||
regulationRepository.findAllRegulationConfigs()
|
||||
}
|
||||
call.respond(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Masterdata SCS API
|
||||
description: >
|
||||
API für den Masterdata-Bounded-Context (Stammdaten: Reiter, Pferde, Vereine, Regeln)
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: http://localhost:8091
|
||||
description: Lokaler Entwicklungs-Server
|
||||
paths:
|
||||
/reiter/search:
|
||||
get:
|
||||
summary: Sucht Reiter
|
||||
parameters:
|
||||
- name: q
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Reitern
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Reiter'
|
||||
/rules/turnierklassen:
|
||||
get:
|
||||
summary: Alle Turnierklassen abrufen
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Turnierklassen
|
||||
/rules/lizenzmatrix:
|
||||
get:
|
||||
summary: Lizenz-Matrix abrufen
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Matrix-Einträgen
|
||||
/rules/config:
|
||||
get:
|
||||
summary: Regel-Konfiguration abrufen
|
||||
parameters:
|
||||
- name: active
|
||||
in: query
|
||||
schema:
|
||||
type: boolean
|
||||
responses:
|
||||
'200':
|
||||
description: Liste von Konfigurationen
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Reiter:
|
||||
type: object
|
||||
properties:
|
||||
reiterId:
|
||||
type: string
|
||||
format: uuid
|
||||
nachname:
|
||||
type: string
|
||||
vorname:
|
||||
type: string
|
||||
satznummer:
|
||||
type: string
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
@file:OptIn(kotlin.uuid.ExperimentalUuidApi::class)
|
||||
|
||||
package at.mocode.masterdata.api.rest
|
||||
|
||||
import at.mocode.masterdata.api.masterdataApiModule
|
||||
import at.mocode.masterdata.domain.model.TurnierklasseDefinition
|
||||
import at.mocode.masterdata.domain.repository.RegulationRepository
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.contentnegotiation.*
|
||||
import io.ktor.server.testing.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class RegulationControllerTest {
|
||||
|
||||
private val regulationRepository = mockk<RegulationRepository>()
|
||||
private val controller = RegulationController(regulationRepository)
|
||||
|
||||
@Test
|
||||
fun `GET turnierklassen returns list from repository`() = testApplication {
|
||||
// Mocking
|
||||
coEvery { regulationRepository.findAllTurnierklassen() } returns listOf(
|
||||
TurnierklasseDefinition(
|
||||
turnierklasseId = kotlin.uuid.Uuid.random(),
|
||||
sparte = at.mocode.core.domain.model.SparteE.DRESSUR,
|
||||
code = "L",
|
||||
bezeichnung = "Leicht",
|
||||
maxHoehe = null,
|
||||
aufgabenNiveau = null,
|
||||
validFrom = kotlin.time.Clock.System.now(),
|
||||
validTo = null,
|
||||
istAktiv = true,
|
||||
createdAt = kotlin.time.Clock.System.now(),
|
||||
updatedAt = kotlin.time.Clock.System.now()
|
||||
)
|
||||
)
|
||||
|
||||
// API Module Setup
|
||||
application {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
masterdataApiModule(
|
||||
countryController = mockk(relaxed = true),
|
||||
bundeslandController = mockk(relaxed = true),
|
||||
altersklasseController = mockk(relaxed = true),
|
||||
platzController = mockk(relaxed = true),
|
||||
reiterController = mockk(relaxed = true),
|
||||
horseController = mockk(relaxed = true),
|
||||
vereinController = mockk(relaxed = true),
|
||||
regulationController = controller
|
||||
)
|
||||
}
|
||||
|
||||
// Request
|
||||
val response = client.get("/rules/turnierklassen")
|
||||
|
||||
// Assert
|
||||
assertEquals(HttpStatusCode.OK, response.status)
|
||||
// Einfacher Check, ob Response nicht leer ist (vollständiges JSON-Deserialisieren würde DTO-Setup in Tests erfordern)
|
||||
// Aber für Contract-Tests/Smoke-Tests reicht das hier.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user