Skip to content

Commit

Permalink
Update documentation, add exception handling, improve tests
Browse files Browse the repository at this point in the history
  • Loading branch information
davidkleiven committed Sep 29, 2023
1 parent 548f644 commit 379c3e7
Show file tree
Hide file tree
Showing 5 changed files with 224 additions and 21 deletions.
10 changes: 10 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@
<artifactId>ktor-server-swagger-jvm</artifactId>
<version>${ktor.version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-cors-jvm</artifactId>
<version>${ktor.version}</version>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-status-pages-jvm</artifactId>
<version>${ktor.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit5</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion src/main/ApiUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ enum class DiagramType { Generic, Substation, VoltageLevel }
// Case insensitive enum value matching
fun getDiagramType(value: String): DiagramType {
return try {
DiagramType.entries.first { item -> item.toString().lowercase() == value.lowercase() }
DiagramType.entries.first { item -> item.toString().lowercase() == value.replace("-", "").lowercase() }
} catch (e: NoSuchElementException) {
DiagramType.Generic
}
Expand Down
26 changes: 17 additions & 9 deletions src/main/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ 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.plugins.cors.routing.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.plugins.swagger.*
import io.ktor.server.request.*
import io.ktor.server.response.*
Expand All @@ -17,6 +19,17 @@ fun Application.module() {
json()
}

install(CORS) {
anyHost()
allowHeader(HttpHeaders.ContentType)
}

install(StatusPages) {
exception<Throwable> { call, cause ->
call.respondText("500: $cause", status = HttpStatusCode.InternalServerError)
}
}

routing {
get("/") {
call.respondText("Hello, world!")
Expand Down Expand Up @@ -47,7 +60,7 @@ fun Application.module() {
}
}

post("/voltage-levels") {
post("/voltage-level-names") {
val files = multiPartDataHandler(call.receiveMultipart())
if (files.isEmpty()) {
call.response.status(HttpStatusCode.UnprocessableEntity)
Expand Down Expand Up @@ -77,14 +90,9 @@ fun Application.module() {
if (files.isEmpty()) {
call.response.status(HttpStatusCode.UnprocessableEntity)
} else {
try {
val network = networkFromFileContent(files[0])
val diagram = singleLineDiagram(diagramType, name, network)
call.respondText(diagram, ContentType.Image.SVG, HttpStatusCode.OK)
} catch (e: PowsyblException) {
call.respondText(e.toString(), ContentType.Text.Plain, HttpStatusCode.BadRequest)
}

val network = networkFromFileContent(files[0])
val diagram = singleLineDiagram(diagramType, name, network)
call.respondText(diagram, ContentType.Image.SVG, HttpStatusCode.OK)
}
}

Expand Down
149 changes: 140 additions & 9 deletions src/main/resources/openapi/documentation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,78 @@ info:
version: "1.0.0"
servers:
- url: "http://0.0.0.0:8080"
- url: "http://devbox:8080"
paths:
/get-buses:
/buses:
post:
operationId: getBuses
operationId: buses
description: "Return all buses in the network"
requestBody:
description: "Form with network sent as file"
$ref: "#/components/networkFile"
responses:
200:
description: "List with all buses in the network"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Bus"
422:
$ref: "#/components/missingNetworkFileResponse"
500:
$ref: "#/components/internalError"

/default-load-parameters:
get:
description: "Get default parameters for the load flow calculation"
responses:
200:
description: "Default load parameters as JSON"
content:
application/json:
type: object
/substation-names:
post:
description: "List all substation names in the network. These names can for instance be used in the diagram methods"
requestBody:
$ref: "#/components/networkFile"
responses:
200:
description: "List with all substation names"
content:
application/json:
type: array
items:
type: string
422:
$ref: "#/components/missingNetworkFileResponse"
500:
$ref: "#/components/internalError"

/voltage-levels:
post:
description: "List all voltage levels in the network. These names can for instance be used in the diagram methods"
requestBody:
$ref: "#/components/networkFile"
responses:
200:
description: "List with all voltage levels"
content:
application/json:
type: array
items:
type: string
422:
$ref: "#/components/missingNetworkFileResponse"
500:
$ref: "#/components/internalError"

/run-load-flow:
post:
description: "Run load flow calculation"
requestBody:
description: "Form with network data sent as file"
required: true
content:
multipart/form-data:
Expand All @@ -21,15 +86,52 @@ paths:
network:
type: string
format: binary
load-parameters:
type: string
required:
- network

responses:
"200":
description: "List with all buses in the network"
200:
description: "Result of load flow calculation"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Bus"
type: object
422:
$ref: "#/components/missingNetworkFileResponse"
500:
$ref: "#/components/internalError"

/diagram:
post:
description: "Create a network area diagram"
requestBody:
$ref: "#/components/networkFile"
responses:
200:
$ref: "#/components/svgDiagram"
422:
$ref: "#/components/missingNetworkFileResponse"
500:
$ref: "#/components/internalError"

/diagram/{type}/{name}:
post:
description: "Create a diagram of a named group of components"
requestBody:
$ref: "#/components/networkFile"
responses:
200:
$ref: "#components/svgDiagram"
400:
description: "Unknown type or name"
content:
text/plain:
type: string
422:
$ref: "#/components/missingNetworkFileResponse"
500:
$ref: "#/components/internalError"


components:
Expand All @@ -47,3 +149,32 @@ components:
type: double
reactivePower:
type: double
networkFile:
description: "Form with network data sent as file"
required: true
content:
multipart/form-data:
schema:
type: object
properties:
network:
type: string
format: binary
missingNetworkFileResponse:
description: "No network file provided"
content:
text/plain:
type: string

internalError:
description: "An internal error occurred"
content:
text/plain:
type: string

svgDiagram:
description: "Generated svg figure"
content:
image/svg+xml:
type: string

58 changes: 56 additions & 2 deletions src/test/AppTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.testing.*
import org.junit.jupiter.api.DynamicTest
import org.junit.jupiter.api.TestFactory
import java.io.File
import java.nio.file.Paths
import java.util.*
Expand Down Expand Up @@ -38,6 +40,44 @@ class ApplicationTest {
assertEquals(HttpStatusCode.UnprocessableEntity, response.status)
}

@TestFactory
fun `test missing network in form`() = listOf(
"/buses",
"/run-load-flow",
"/substation-names",
"/voltage-level-names",
"/diagram",
"/diagram/substation/S1",
"/diagram/voltage-level/VL1"
).map { url ->
DynamicTest.dynamicTest("422 when no network is passed to $url") {
testApplication {
val response = client.submitFormWithBinaryData(url = url, formData = listOf())
assertEquals(HttpStatusCode.UnprocessableEntity, response.status)
}
}
}

@TestFactory
fun `test internal server error when file parsing fails`() = listOf(
"/buses",
"/run-load-flow",
"/substation-names",
"/voltage-level-names",
"/diagram",
"/diagram/substation/S1",
"/diagram/voltage-level/VL1"
).map { url ->
DynamicTest.dynamicTest("500 when file content can not be parsed $url") {
testApplication {
val response = client.submitFormWithBinaryData(url = url, formData = formDataWithEmptyNetwork())
assertEquals(HttpStatusCode.InternalServerError, response.status)
val body = response.bodyAsText()
assertTrue(body.contains("PowsyblException"))
}
}
}

@Test
fun `test receive 14 buses for 14 bus network`() =
testApplication {
Expand Down Expand Up @@ -115,7 +155,9 @@ class ApplicationTest {
url = "/diagram/substation/non-existent-station",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
assertEquals(response.status, HttpStatusCode.BadRequest)
assertEquals(HttpStatusCode.InternalServerError, response.status)
val body = response.bodyAsText()
assertTrue(body.contains("Substation 'non-existent-station' not found"))
}

@Test
Expand Down Expand Up @@ -146,7 +188,7 @@ class ApplicationTest {
fun `test 2 voltage levels extracted`() =
testApplication {
val response = client.submitFormWithBinaryData(
url = "/voltage-levels",
url = "/voltage-level-names",
formData = formDataFromFile(ieeeCdfNetwork14File())
)
assertEquals(response.status, HttpStatusCode.OK)
Expand Down Expand Up @@ -180,6 +222,18 @@ fun formDataFromFile(file: File): List<PartData> {
}
}

fun formDataWithEmptyNetwork(): List<PartData> {
return formData {
append(
"network",
byteArrayOf(),
Headers.build {
append(HttpHeaders.ContentDisposition, "filename=emptyFile.xiidm")
}
)
}
}

fun ieeeCdfNetwork14File(): File {
// Initialize temporary file
val file = File.createTempFile("network", ".xiidm")
Expand Down

0 comments on commit 379c3e7

Please sign in to comment.