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)
+ }
+}