Skip to content

Commit b3bfed8

Browse files
committed
Update routing to Ktor style
1 parent e2f484f commit b3bfed8

12 files changed

Lines changed: 376 additions & 75 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#Mon Sep 02 03:41:15 CEST 2024
22
distributionBase=GRADLE_USER_HOME
33
distributionPath=wrapper/dists
4-
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
4+
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip
55
zipStoreBase=GRADLE_USER_HOME
66
zipStorePath=wrapper/dists

src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ package net.ccbluex.netty.http
2121

2222
import io.netty.handler.codec.http.*
2323
import net.ccbluex.netty.http.HttpServer.Companion.logger
24+
import net.ccbluex.netty.http.application.ApplicationCall
25+
import net.ccbluex.netty.http.application.ResponseException
2426
import net.ccbluex.netty.http.model.RequestContext
2527
import net.ccbluex.netty.http.util.httpBadRequest
2628
import net.ccbluex.netty.http.util.httpInternalServerError
2729
import net.ccbluex.netty.http.util.httpNotFound
28-
import net.ccbluex.netty.http.model.RequestObject
2930
import net.ccbluex.netty.http.util.httpNoContent
3031

3132
/**
@@ -54,18 +55,23 @@ internal suspend fun HttpServer.processRequestContext(context: RequestContext) =
5455
return@runCatching httpNotFound(context.path, "Route not found")
5556

5657
logger.debug("Found destination {}", node)
57-
val requestObject = RequestObject(
58+
val call = ApplicationCall(
5859
uri = context.uri,
5960
path = context.path,
6061
remainingPath = remaining,
6162
method = method,
6263
body = content.toString(Charsets.UTF_8),
63-
params = params,
64-
queryParams = context.params,
64+
parameters = params,
65+
queryParameters = context.params,
6566
headers = context.headers
6667
)
6768

68-
return@runCatching node.handle(requestObject)
69+
return@runCatching node.handle(call)
70+
}.recoverCatching {
71+
if (it is ResponseException) {
72+
return@recoverCatching it.response
73+
}
74+
throw it
6975
}.getOrElse {
7076
logger.error("Error while processing request object: $context", it)
7177
httpInternalServerError(it.message ?: "Unknown error")
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package net.ccbluex.netty.http.application
2+
3+
import com.google.gson.Gson
4+
import com.google.gson.JsonElement
5+
import io.netty.handler.codec.http.FullHttpResponse
6+
import io.netty.handler.codec.http.HttpHeaders
7+
import io.netty.handler.codec.http.HttpMethod
8+
import io.netty.handler.codec.http.HttpResponseStatus
9+
import net.ccbluex.netty.http.util.DEFAULT_GSON
10+
import net.ccbluex.netty.http.util.httpBadRequest
11+
import net.ccbluex.netty.http.util.httpFile
12+
import net.ccbluex.netty.http.util.httpFileStream
13+
import net.ccbluex.netty.http.util.httpForbidden
14+
import net.ccbluex.netty.http.util.httpInternalServerError
15+
import net.ccbluex.netty.http.util.httpNoContent
16+
import net.ccbluex.netty.http.util.httpNotFound
17+
import net.ccbluex.netty.http.util.httpResponse
18+
import net.ccbluex.netty.http.util.httpUnauthorized
19+
import java.io.File
20+
import java.io.InputStream
21+
22+
open class ApplicationCall(
23+
val uri: String,
24+
val path: String,
25+
val remainingPath: String,
26+
val method: HttpMethod,
27+
val body: String,
28+
val parameters: Map<String, String>,
29+
val queryParameters: Map<String, String>,
30+
val headers: HttpHeaders
31+
) {
32+
33+
private var response: FullHttpResponse? = null
34+
35+
inline fun <reified T> receive(gson: Gson = DEFAULT_GSON): T = gson.fromJson(body, T::class.java)
36+
37+
fun respond(response: FullHttpResponse) {
38+
this.response = response
39+
}
40+
41+
@JvmOverloads
42+
fun respond(status: HttpResponseStatus, body: JsonElement, gson: Gson = DEFAULT_GSON) {
43+
respond(httpResponse(status, body, gson))
44+
}
45+
46+
@JvmOverloads
47+
fun respond(status: HttpResponseStatus, body: Any, gson: Gson = DEFAULT_GSON) {
48+
respond(httpResponse(status, body, gson))
49+
}
50+
51+
@JvmOverloads
52+
fun respond(body: JsonElement, gson: Gson = DEFAULT_GSON) {
53+
respond(HttpResponseStatus.OK, body, gson)
54+
}
55+
56+
@JvmOverloads
57+
fun respond(body: Any, gson: Gson = DEFAULT_GSON) {
58+
respond(HttpResponseStatus.OK, body, gson)
59+
}
60+
61+
fun respondNoContent() {
62+
respond(httpNoContent())
63+
}
64+
65+
fun respondFile(file: File) {
66+
respond(httpFile(file))
67+
}
68+
69+
@JvmOverloads
70+
fun respondFileStream(
71+
stream: InputStream,
72+
contentType: String? = null,
73+
contentLength: Int = 256,
74+
) {
75+
respond(httpFileStream(stream, contentType, contentLength))
76+
}
77+
78+
fun takeResponse(): FullHttpResponse {
79+
return response ?: throw IllegalStateException("Route handler completed without responding for $method $path")
80+
}
81+
82+
fun badRequest(reason: String): Nothing = abort(httpBadRequest(reason))
83+
84+
fun forbidden(reason: String): Nothing = abort(httpForbidden(reason))
85+
86+
fun unauthorized(reason: String): Nothing = abort(httpUnauthorized(reason))
87+
88+
fun notFound(path: String, reason: String): Nothing = abort(httpNotFound(path, reason))
89+
90+
fun internalServerError(reason: String): Nothing = abort(httpInternalServerError(reason))
91+
92+
private fun abort(response: FullHttpResponse): Nothing = throw ResponseException(response)
93+
}
94+
95+
class ResponseException(val response: FullHttpResponse) : RuntimeException(null, null, false, false)

src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -21,43 +21,50 @@ package net.ccbluex.netty.http.model
2121

2222
import io.netty.handler.codec.http.HttpHeaders
2323
import io.netty.handler.codec.http.HttpMethod
24+
import net.ccbluex.netty.http.application.ApplicationCall
2425
import net.ccbluex.netty.http.util.DEFAULT_GSON
2526

2627
/**
27-
* Represents an HTTP request object.
28+
* Compatibility wrapper for the pre-2.6 request API.
2829
*
29-
* @property uri The full URI of the request.
30-
* @property path The path of the request.
31-
* @property remainingPath The ending part of the path which was not matched by the route.
32-
* @property method The HTTP method of the request.
33-
* @property body The body of the request.
34-
* @property params The inline URI parameters of the request.
35-
* @property queryParams The query parameters of the request.
36-
* @property headers The headers of the request.
30+
* New code should use [ApplicationCall] inside a routing context instead.
3731
*/
38-
data class RequestObject(
39-
val uri: String,
40-
val path: String,
41-
val remainingPath: String,
42-
val method: HttpMethod,
43-
val body: String,
44-
val params: Map<String, String>,
45-
val queryParams: Map<String, String>,
46-
val headers: HttpHeaders
32+
@Deprecated("Use ApplicationCall within RoutingContext handlers")
33+
class RequestObject(
34+
uri: String,
35+
path: String,
36+
remainingPath: String,
37+
method: HttpMethod,
38+
body: String,
39+
params: Map<String, String>,
40+
queryParams: Map<String, String>,
41+
headers: HttpHeaders
42+
) : ApplicationCall(
43+
uri = uri,
44+
path = path,
45+
remainingPath = remainingPath,
46+
method = method,
47+
body = body,
48+
parameters = params,
49+
queryParameters = queryParams,
50+
headers = headers
4751
) {
4852

53+
val params: Map<String, String> get() = parameters
54+
val queryParams: Map<String, String> get() = queryParameters
55+
4956
/**
5057
* Converts the body of the request to a JSON object of the specified type.
5158
*
5259
* @return The JSON object of the specified type.
5360
*/
5461
inline fun <reified T> asJson(): T {
55-
return GSON_INSTANCE.fromJson(body, T::class.java)
62+
return receive(GSON_INSTANCE)
5663
}
5764

5865
companion object {
5966
@JvmField
6067
val GSON_INSTANCE = DEFAULT_GSON
6168
}
6269

63-
}
70+
}

src/main/kotlin/net/ccbluex/netty/http/rest/FileServant.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ package net.ccbluex.netty.http.rest
2121

2222
import io.netty.handler.codec.http.FullHttpResponse
2323
import io.netty.handler.codec.http.HttpMethod
24+
import net.ccbluex.netty.http.application.ApplicationCall
2425
import net.ccbluex.netty.http.util.httpFile
2526
import net.ccbluex.netty.http.util.httpForbidden
2627
import net.ccbluex.netty.http.util.httpNotFound
27-
import net.ccbluex.netty.http.model.RequestObject
2828
import java.io.File
2929

3030
/**
@@ -37,8 +37,8 @@ class FileServant(part: String, private val baseFolder: File) : Node(part) {
3737

3838
override val isExecutable = true
3939

40-
override suspend fun handle(request: RequestObject): FullHttpResponse {
41-
val path = request.remainingPath
40+
override suspend fun handle(call: ApplicationCall): FullHttpResponse {
41+
val path = call.remainingPath
4242
val sanitizedPath = path.replace("..", "")
4343
val file = baseFolder.resolve(sanitizedPath)
4444

@@ -59,7 +59,6 @@ class FileServant(part: String, private val baseFolder: File) : Node(part) {
5959

6060
override fun matches(index: Int, part: String) = super.matches(index, part) || index == 0 && isRoot
6161

62-
override fun matchesMethod(method: HttpMethod) =
63-
method == HttpMethod.GET && super.matchesMethod(method)
62+
override fun matchesMethod(method: HttpMethod) = method == HttpMethod.GET
6463

65-
}
64+
}

0 commit comments

Comments
 (0)