From 1be8fe291d232540af88d00d8036717044ebaf2c Mon Sep 17 00:00:00 2001 From: David Kleiven Date: Sun, 24 Sep 2023 12:21:53 +0200 Subject: [PATCH] Add end-point for extracting buses from a network --- pom.xml | 27 +++++++++++ src/main/ApiDataModels.kt | 25 ++++++---- src/main/ApiUtil.kt | 11 +++++ src/main/App.kt | 17 ------- src/main/Application.kt | 52 ++++++++++++++++++++ src/main/resources/application.conf | 9 ++++ src/test/ApiDataModelsTest.kt | 7 ++- src/test/ApplicationTest.kt | 75 +++++++++++++++++++++++++++++ 8 files changed, 192 insertions(+), 31 deletions(-) create mode 100644 src/main/ApiUtil.kt delete mode 100644 src/main/App.kt create mode 100644 src/main/Application.kt create mode 100644 src/main/resources/application.conf create mode 100644 src/test/ApplicationTest.kt diff --git a/pom.xml b/pom.xml index e8b26a1..63f66de 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,21 @@ ktor-server-netty-jvm ${ktor.version} + + io.ktor + ktor-serialization-kotlinx-json-jvm + ${ktor.version} + + + io.ktor + ktor-server-test-host-jvm + ${ktor.version} + + + io.ktor + ktor-server-content-negotiation-jvm + ${ktor.version} + org.jetbrains.kotlin kotlin-test-junit5 @@ -56,6 +71,18 @@ ${kotlin.version} true + + + kotlinx-serialization + + + + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} + + org.apache.maven.plugins diff --git a/src/main/ApiDataModels.kt b/src/main/ApiDataModels.kt index 1470f7f..5632179 100644 --- a/src/main/ApiDataModels.kt +++ b/src/main/ApiDataModels.kt @@ -1,11 +1,13 @@ package com.github.statnett.loadflowservice import com.powsybl.iidm.network.Network +import kotlinx.serialization.Serializable /** * Class for holding properties from the PowsbyBl bus class that are * returned via the Rest API */ +@Serializable data class BusProperties( val id: String, val voltage: Double, @@ -14,13 +16,16 @@ data class BusProperties( val reactivePower: Double, ) -fun busPropertiesFromNetwork(network: Network) = - network.getBusView().getBusStream().map { - BusProperties( - id = it.getId(), - voltage = it.getV(), - angle = it.getAngle(), - activePower = it.getP(), - reactivePower = it.getQ(), - ) - } +fun busPropertiesFromNetwork(network: Network): List { + return network.busView.buses + .map { bus -> + BusProperties( + id = bus.id, + voltage = bus.v, + angle = bus.angle, + activePower = bus.p, + reactivePower = bus.q, + ) + } + .toList() +} diff --git a/src/main/ApiUtil.kt b/src/main/ApiUtil.kt new file mode 100644 index 0000000..7c94950 --- /dev/null +++ b/src/main/ApiUtil.kt @@ -0,0 +1,11 @@ +package com.github.statnett.loadflowservice + +import java.io.ByteArrayInputStream + +fun busesFromRequest( + type: String, + body: ByteArray, +): List { + val network = networkFromStream(type, ByteArrayInputStream(body)) + return busPropertiesFromNetwork(network) +} diff --git a/src/main/App.kt b/src/main/App.kt deleted file mode 100644 index 594a883..0000000 --- a/src/main/App.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.statnett.loadflowservice - -import io.ktor.server.application.* -import io.ktor.server.engine.* -import io.ktor.server.netty.Netty -import io.ktor.server.response.* -import io.ktor.server.routing.* - -fun main() { - embeddedServer(Netty, port = 8080) { - routing { - get("/") { - call.respondText("Hello, world!") - } - } - }.start(wait = true) -} diff --git a/src/main/Application.kt b/src/main/Application.kt new file mode 100644 index 0000000..4e1b5de --- /dev/null +++ b/src/main/Application.kt @@ -0,0 +1,52 @@ +package com.github.statnett.loadflowservice + +import io.ktor.http.HttpStatusCode +import io.ktor.http.content.PartData +import io.ktor.http.content.forEachPart +import io.ktor.http.content.streamProvider +import io.ktor.serialization.kotlinx.json.* +import io.ktor.server.application.* +import io.ktor.server.plugins.contentnegotiation.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* + +fun main(args: Array): Unit = io.ktor.server.netty.EngineMain.main(args) + +fun Application.module() { + install(ContentNegotiation) { + json() + } + + routing { + get("/") { + call.respondText("Hello, world!") + } + + post("/get-buses") { + var fileName = "" + var fileBytes = byteArrayOf() + + val multiPartData = call.receiveMultipart() + + multiPartData.forEachPart { part -> + when (part) { + is PartData.FileItem -> { + fileName = part.originalFileName as String + fileBytes = part.streamProvider().readBytes() + } + + else -> {} + } + part.dispose() + } + + if (fileName == "") { + call.response.status(HttpStatusCode.UnprocessableEntity) + } else { + val busProps = busesFromRequest(fileName, fileBytes) + call.respond(busProps) + } + } + } +} diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf new file mode 100644 index 0000000..5a6bfd2 --- /dev/null +++ b/src/main/resources/application.conf @@ -0,0 +1,9 @@ +ktor { + deployment { + port = 8080 + port = ${?PORT} + } + application { + modules = [ com.github.statnett.loadflowservice.ApplicationKt.module ] + } +} \ No newline at end of file diff --git a/src/test/ApiDataModelsTest.kt b/src/test/ApiDataModelsTest.kt index a0797e9..b9fa7ca 100644 --- a/src/test/ApiDataModelsTest.kt +++ b/src/test/ApiDataModelsTest.kt @@ -1,14 +1,13 @@ +import com.github.statnett.loadflowservice.busPropertiesFromNetwork +import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory import kotlin.test.Test import kotlin.test.assertEquals -import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory -import com.github.statnett.loadflowservice.busPropertiesFromNetwork class ApiDataModelTest { - @Test fun `Should be 14 buses in test network`() { val network = IeeeCdfNetworkFactory.create14() val buses = busPropertiesFromNetwork(network) assertEquals(buses.count(), 14) } -} \ No newline at end of file +} diff --git a/src/test/ApplicationTest.kt b/src/test/ApplicationTest.kt new file mode 100644 index 0000000..7e1ce40 --- /dev/null +++ b/src/test/ApplicationTest.kt @@ -0,0 +1,75 @@ +import com.powsybl.ieeecdf.converter.IeeeCdfNetworkFactory +import io.ktor.client.request.* +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitFormWithBinaryData +import io.ktor.client.statement.* +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpStatusCode +import io.ktor.server.testing.testApplication +import java.io.File +import java.nio.file.Paths +import java.util.Properties +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ApplicationTest { + @Test + fun testRoot() = + testApplication { + val response = client.get("/") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("Hello, world!", response.bodyAsText()) + } + + @Test + fun `test get buses returns 422 on missing file content`() = + testApplication { + val response = + client.submitFormWithBinaryData( + url = "/get-buses", + formData = + formData { + append("network", "not file content") + }, + ) + assertEquals(HttpStatusCode.UnprocessableEntity, response.status) + } + + @Test + fun `test receive 14 buses for 14 bus network`() = + testApplication { + // Initialize temporary file + val file = File.createTempFile("network", ".xiidm") + file.deleteOnExit() + + IeeeCdfNetworkFactory.create14().write("XIIDM", Properties(), Paths.get(file.path)) + + val response = + client.submitFormWithBinaryData( + url = "/get-buses", + formData = + formData { + append( + "network", + file.readBytes(), + Headers.build { + append(HttpHeaders.ContentDisposition, "filename=${file.name}") + }, + ) + }, + ) + assertEquals(HttpStatusCode.OK, response.status) + assertEquals(response.headers["Content-Type"], "application/json; charset=UTF-8") + val body: String = response.bodyAsText() + + // Roughly validate contant + assertTrue(body.startsWith("[{")) + assertTrue(body.endsWith("}]")) + + val busString = "{\"id\":\"VL1_0\",\"voltage\":143.1,\"angle\":0.0,\"activePower\":0.0,\"reactivePower\":0.0}" + assertContains(body, busString) + } +}