Skip to content

Commit 66de085

Browse files
committed
feat: use json for request/response in webapp
1 parent 5caf8c8 commit 66de085

File tree

7 files changed

+102
-46
lines changed

7 files changed

+102
-46
lines changed

webapp/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies {
2222
implementation("org.http4k:http4k-server-jetty")
2323
implementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
2424
implementation("ch.qos.logback:logback-classic:1.4.14")
25+
implementation("com.ubertob.kondor:kondor-core:2.2.2")
2526

2627
testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
2728
testImplementation("io.strikt:strikt-core:0.34.1")

webapp/http_test.sh

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ curl http://localhost:9000/hello/ping
55
echo ""
66
echo ""
77

8-
echo "Execute mission"
9-
curl http://localhost:9000/rover/FF -X POST
8+
echo "Execute mission: FF"
9+
curl -H "Content-Type: application/json" -X POST -d '{"commands":"FF"}' http://localhost:9000/rover/mission
1010
echo ""
1111
echo ""
1212

13-
echo "Execute mission (hit obstacle)"
14-
curl http://localhost:9000/rover/RFF -X POST
13+
echo "Execute mission (hit obstacle): RFF"
14+
curl -H "Content-Type: application/json" -X POST -d '{"commands":"RFF"}' http://localhost:9000/rover/mission
15+
echo ""
16+
echo ""
17+
18+
echo "Execute mission (invalid command): RXFF"
19+
curl -H "Content-Type: application/json" -X POST -d '{"commands":"RXFF"}' http://localhost:9000/rover/mission
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.doubleloop
2+
3+
import com.ubertob.kondor.json.JAny
4+
import com.ubertob.kondor.json.bool
5+
import com.ubertob.kondor.json.jsonnode.JsonNodeObject
6+
import com.ubertob.kondor.json.num
7+
import com.ubertob.kondor.json.str
8+
9+
data class RunAppHttpRequest(val commands: String)
10+
data class RunAppHttpResponse(
11+
val positionX: Int,
12+
val positionY: Int,
13+
val direction: String,
14+
val hitObstacle: Boolean
15+
)
16+
17+
object JRunAppHttpRequest : JAny<RunAppHttpRequest>() {
18+
val commands by str(RunAppHttpRequest::commands)
19+
20+
override fun JsonNodeObject.deserializeOrThrow(): RunAppHttpRequest? =
21+
RunAppHttpRequest(commands = +commands)
22+
}
23+
24+
object JRunAppHttpResponse : JAny<RunAppHttpResponse>() {
25+
val positionX by num(RunAppHttpResponse::positionX)
26+
val positionY by num(RunAppHttpResponse::positionY)
27+
val direction by str(RunAppHttpResponse::direction)
28+
val hitObstacle by bool(RunAppHttpResponse::hitObstacle)
29+
30+
override fun JsonNodeObject.deserializeOrThrow(): RunAppHttpResponse? =
31+
RunAppHttpResponse(
32+
positionX = +positionX,
33+
positionY = +positionY,
34+
direction = +direction,
35+
hitObstacle = +hitObstacle,
36+
)
37+
}
38+
39+
fun RunAppHttpRequest.toRunAppRequest(): RunAppRequest =
40+
RunAppRequest(this.commands)
41+
42+
fun Rover.renderComplete() =
43+
RunAppHttpResponse(
44+
this.position.x,
45+
this.position.y,
46+
this.orientation.toString(),
47+
false
48+
)
49+
50+
fun ObstacleDetected.renderObstacle() =
51+
RunAppHttpResponse(
52+
this.position.x,
53+
this.position.y,
54+
this.orientation.toString(),
55+
true
56+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package io.doubleloop
2+
3+
import com.ubertob.kondor.json.JConverter
4+
import org.http4k.core.ContentType
5+
import org.http4k.core.Request
6+
import org.http4k.core.Response
7+
8+
private const val contentTypeHeaderName = "Content-Type"
9+
10+
fun <T : Any> Request.parseJsonBody(converter: JConverter<T>): T =
11+
converter.fromJson(bodyString()).orThrow()
12+
13+
fun <T : Any> Response.bodyAsJson(converter: JConverter<T>, value: T) =
14+
body(converter.toJson(value)).header(contentTypeHeaderName, ContentType.APPLICATION_JSON.toHeaderValue())

webapp/src/main/kotlin/io/doubleloop/Parsing.kt

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,6 @@ sealed class ParseError {
1414
data class InvalidCommand(val message: String) : ParseError()
1515
}
1616

17-
fun runMission(
18-
inputPlanet: Pair<String, String>,
19-
inputRover: Pair<String, String>,
20-
inputCommands: String
21-
): Either<ParseError, String> = either {
22-
val planet = parsePlanet(inputPlanet).bind()
23-
val rover = parseRover(inputRover).bind()
24-
val commands = parseCommands(inputCommands).bind()
25-
val result = executeAll(planet, rover, commands)
26-
result.fold(::renderObstacle, ::renderComplete)
27-
}
28-
2917
fun parseCommand(input: Char): Either<ParseError, Command> =
3018
when (input.lowercase()) {
3119
"f" -> MoveForward.right()
@@ -94,18 +82,9 @@ fun parsePair(separator: String, input: String): Either<Throwable, Pair<Int, Int
9482
Pair(first, second)
9583
}
9684

97-
fun renderComplete(rover: Rover): String =
98-
"${rover.position.x}:${rover.position.y}:${rover.orientation}"
99-
100-
fun renderObstacle(rover: ObstacleDetected): String =
101-
"O:${renderComplete(rover)}"
102-
10385
fun renderError(error: ParseError): String =
10486
when (error) {
10587
is InvalidPlanet -> "Planet parsing: ${error.message}"
10688
is InvalidRover -> "Rover parsing: ${error.message}"
10789
is InvalidCommand -> "Command parsing: ${error.message}"
10890
}
109-
110-
fun renderError(error: Throwable): String =
111-
error.message ?: "Unknown error"

webapp/src/main/kotlin/io/doubleloop/WebappRoutes.kt

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ package io.doubleloop
33
import arrow.core.raise.catch
44
import kotlinx.coroutines.runBlocking
55
import org.http4k.core.Method
6-
import org.http4k.core.Request
76
import org.http4k.core.Response
87
import org.http4k.core.Status
98
import org.http4k.routing.bind
10-
import org.http4k.routing.path
119
import org.http4k.routing.routes
1210

1311
object WebappRoutes {
@@ -19,30 +17,24 @@ object WebappRoutes {
1917
)
2018

2119
fun roverRoutes(handler: RunAppHandler) = routes(
22-
"/{commands}" bind Method.POST to {
23-
// // create adapters
24-
// val fileMissionSource = FileMissionSource("planet.txt", "rover.txt")
25-
// // create use case handler
26-
// val handler = RunAppHandler(fileMissionSource)
20+
"/mission" bind Method.POST to {
21+
val body = it.parseJsonBody(JRunAppHttpRequest)
2722

28-
// run use case
2923
runBlocking {
3024
catch({
31-
handler.handle(it.toRunAppRequest()).toResponse()
25+
handler.handle(body.toRunAppRequest()).toResponse()
3226
}) { it.toResponse() }
3327
}
3428
}
3529
)
3630

37-
private fun Request.toRunAppRequest(): RunAppRequest =
38-
RunAppRequest(this.path("commands") ?: "")
39-
4031
private fun RunAppResponse.toResponse(): Response =
4132
this.result.fold(
42-
{ Response(Status.UNPROCESSABLE_ENTITY).body(renderObstacle(it)) },
43-
{ Response(Status.OK).body(renderComplete(it)) }
33+
{ Response(Status.UNPROCESSABLE_ENTITY).bodyAsJson(JRunAppHttpResponse, it.renderObstacle()) },
34+
{ Response(Status.OK).bodyAsJson(JRunAppHttpResponse, it.renderComplete()) }
4435
)
4536

4637
private fun Throwable.toResponse() =
47-
Response(Status.BAD_REQUEST).body(renderError(this))
38+
Response(Status.BAD_REQUEST).body(this.message ?: "Unknown error")
39+
4840
}

webapp/src/test/kotlin/io/doubleloop/AppTests.kt renamed to webapp/src/test/kotlin/io/doubleloop/WebAppTests.kt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test
1212
import strikt.api.expectThat
1313
import strikt.assertions.isEqualTo
1414

15-
class AppTests {
15+
class WebAppTests {
1616

1717
val app = createApp(Dependencies.create(Env()))
1818

@@ -26,25 +26,34 @@ class AppTests {
2626

2727
@Test
2828
fun `post commands`() {
29-
val request = Request(POST, "/rover/FF")
29+
val request = Request(POST, "/rover/mission").body("{ \"commands\": \"FF\" }")
3030
val response = app(request)
3131
expectThat(response).status.isEqualTo(OK)
32-
expectThat(response).bodyString.isEqualTo("0:2:N")
32+
expectThat(response).bodyString.isEqualTo("{\"positionX\": 0, \"positionY\": 2, \"direction\": \"N\", \"hitObstacle\": false}")
3333
}
3434

3535
@Test
3636
fun `post commands (hit obstacle)`() {
37-
val request = Request(POST, "/rover/RFF")
37+
val request = Request(POST, "/rover/mission").body("{ \"commands\": \"RFF\" }")
3838
val response = app(request)
3939
expectThat(response).status.isEqualTo(UNPROCESSABLE_ENTITY)
40-
expectThat(response).bodyString.isEqualTo("O:1:0:E")
40+
expectThat(response).bodyString.isEqualTo("{\"positionX\": 1, \"positionY\": 0, \"direction\": \"E\", \"hitObstacle\": true}")
4141
}
4242

4343
@Test
4444
fun `post invalid commands`() {
45-
val request = Request(POST, "/rover/FFX")
45+
val request = Request(POST, "/rover/mission").body("{ \"commands\": \"FFX\" }")
4646
val response = app(request)
4747
expectThat(response).status.isEqualTo(BAD_REQUEST)
4848
expectThat(response).bodyString.isEqualTo("Command parsing: invalid command: X")
4949
}
50+
51+
@Test
52+
fun `convert request and response from and to json`() {
53+
val jsonRequest = JRunAppHttpRequest.fromJson("{ \"commands\": \"FF\" }").orThrow()
54+
expectThat(jsonRequest).isEqualTo(RunAppHttpRequest("FF"))
55+
56+
val jsonResponse = JRunAppHttpResponse.toJson(RunAppHttpResponse(1, 2, "N", false))
57+
expectThat(jsonResponse).isEqualTo("{\"positionX\": 1, \"positionY\": 2, \"direction\": \"N\", \"hitObstacle\": false}")
58+
}
5059
}

0 commit comments

Comments
 (0)