Switch HTTP server API to RequestContext receivers
This commit is contained in:
parent
ca4a0d4b12
commit
f74ed9afe4
@ -43,23 +43,52 @@ suspend fun bootstrapHttpServer() {
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
server.get("/hello") { ex ->
|
||||
ex.setHeader("Content-Type", "text/plain")
|
||||
ex.respondText(200, "hello")
|
||||
server.get("/hello") {
|
||||
setHeader("Content-Type", "text/plain")
|
||||
respondText(200, "hello")
|
||||
}
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Reusable routers
|
||||
|
||||
`Router` collects the same route kinds as `HttpServer`, but does not listen on sockets by itself.
|
||||
Mount it into `HttpServer` or another `Router`.
|
||||
|
||||
```lyng
|
||||
import lyng.io.http.server
|
||||
|
||||
val api = Router()
|
||||
api.get("/health") {
|
||||
respondText(200, "ok")
|
||||
}
|
||||
|
||||
val users = Router()
|
||||
users.getPath("/users/{id}") {
|
||||
respondJson({ id: routeParams["id"] })
|
||||
}
|
||||
|
||||
api.mount(users)
|
||||
|
||||
val server = HttpServer()
|
||||
server.mount(api)
|
||||
server.listen(8080, "127.0.0.1")
|
||||
```
|
||||
|
||||
Mounted routers reuse the built-in server router. They are configuration-time composition, not an extra per-request Lyng dispatch layer.
|
||||
|
||||
---
|
||||
|
||||
#### Regex route
|
||||
|
||||
Regex routes match the whole request path, not a substring.
|
||||
|
||||
```lyng
|
||||
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) { ex ->
|
||||
val m = ex.routeMatch!!
|
||||
ex.respondText(200, "user=" + m[1] + ", post=" + m[2])
|
||||
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
|
||||
val m = routeMatch!!
|
||||
respondText(200, "user=" + m[1] + ", post=" + m[2])
|
||||
}
|
||||
```
|
||||
|
||||
@ -70,10 +99,10 @@ server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) { ex ->
|
||||
Path templates are sugar on top of regex routes. Template parameters are exposed as decoded `routeParams`.
|
||||
|
||||
```lyng
|
||||
server.getPath("/users/{userId}/posts/{postId}") { ex ->
|
||||
ex.respondText(
|
||||
server.getPath("/users/{userId}/posts/{postId}") {
|
||||
respondText(
|
||||
200,
|
||||
ex.routeParams["userId"] + ":" + ex.routeParams["postId"]
|
||||
routeParams["userId"] + ":" + routeParams["postId"]
|
||||
)
|
||||
}
|
||||
```
|
||||
@ -104,7 +133,7 @@ Template rules:
|
||||
- `headers: HttpHeaders`
|
||||
- `body: Buffer`
|
||||
|
||||
`ServerExchange` exposes routing context and response controls:
|
||||
`RequestContext` exposes routing context and response controls:
|
||||
|
||||
- `request: ServerRequest`
|
||||
- `routeMatch: RegexMatch?`
|
||||
@ -125,7 +154,7 @@ For path-template routes, both `routeMatch` and `routeParams` are set.
|
||||
|
||||
#### JSON request/response helpers
|
||||
|
||||
For ordinary HTTP JSON APIs, `ServerExchange` includes two helpers:
|
||||
For ordinary HTTP JSON APIs, `RequestContext` includes two helpers:
|
||||
|
||||
- `jsonBody<T>()` decodes the request body with typed `Json.decodeAs(...)`
|
||||
- `respondJson(body, status = 200)` sets JSON content type and responds with plain `toJsonString()`
|
||||
@ -140,15 +169,15 @@ closed class CreateUserResponse(id: Int, name: String, age: Int)
|
||||
|
||||
val server = HttpServer()
|
||||
|
||||
server.postPath("/api/users") { ex ->
|
||||
val req = ex.jsonBody<CreateUserRequest>()
|
||||
server.postPath("/api/users") {
|
||||
val req = jsonBody<CreateUserRequest>()
|
||||
|
||||
if (req.name.isBlank()) {
|
||||
ex.respondJson({ error: "name must not be empty" }, 400)
|
||||
respondJson({ error: "name must not be empty" }, 400)
|
||||
return
|
||||
}
|
||||
|
||||
ex.respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
||||
respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
||||
}
|
||||
|
||||
server.listen(8080, "127.0.0.1")
|
||||
@ -177,13 +206,13 @@ This means exact routes stay fast and always win over template or regex routes f
|
||||
You can route websocket upgrades by exact path, regex, or path template:
|
||||
|
||||
```lyng
|
||||
server.ws("/chat") { ws, ex ->
|
||||
server.ws("/chat") { ws ->
|
||||
ws.sendText("hello")
|
||||
ws.close()
|
||||
}
|
||||
|
||||
server.wsPath("/ws/{room}") { ws, ex ->
|
||||
ws.sendText("room=" + ex.routeParams["room"])
|
||||
server.wsPath("/ws/{room}") { ws ->
|
||||
ws.sendText("room=" + routeParams["room"])
|
||||
ws.close()
|
||||
}
|
||||
```
|
||||
@ -192,6 +221,23 @@ server.wsPath("/ws/{room}") { ws, ex ->
|
||||
|
||||
#### API surface
|
||||
|
||||
`Router` route registration methods:
|
||||
|
||||
- `get(path: String|Regex, handler)`
|
||||
- `getPath(pathTemplate: String, handler)`
|
||||
- `post(path: String|Regex, handler)`
|
||||
- `postPath(pathTemplate: String, handler)`
|
||||
- `put(path: String|Regex, handler)`
|
||||
- `putPath(pathTemplate: String, handler)`
|
||||
- `delete(path: String|Regex, handler)`
|
||||
- `deletePath(pathTemplate: String, handler)`
|
||||
- `any(path: String|Regex, handler)`
|
||||
- `anyPath(pathTemplate: String, handler)`
|
||||
- `ws(path: String|Regex, handler)`
|
||||
- `wsPath(pathTemplate: String, handler)`
|
||||
- `fallback(handler)`
|
||||
- `mount(router)`
|
||||
|
||||
`HttpServer` route registration methods:
|
||||
|
||||
- `get(path: String|Regex, handler)`
|
||||
@ -207,4 +253,5 @@ server.wsPath("/ws/{room}") { ws, ex ->
|
||||
- `ws(path: String|Regex, handler)`
|
||||
- `wsPath(pathTemplate: String, handler)`
|
||||
- `fallback(handler)`
|
||||
- `mount(router)`
|
||||
- `listen(port, host = null, backlog = 128)`
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
- **[lyng.io.process](lyng.io.process.md):** External process execution and shell commands. Provides `Process`, `RunningProcess`, and `Platform` information.
|
||||
- **[lyng.io.console](lyng.io.console.md):** Rich console/TTY access. Provides `Console` capability detection, geometry, output, and iterable events.
|
||||
- **[lyng.io.http](lyng.io.http.md):** HTTP/HTTPS client access. Provides `Http`, `HttpRequest`, `HttpResponse`, and `HttpHeaders`.
|
||||
- **[lyng.io.http.server](lyng.io.http.server.md):** Minimal HTTP/1.1 and WebSocket server. Provides `HttpServer`, `ServerRequest`, `ServerExchange`, and `ServerWebSocket`.
|
||||
- **[lyng.io.http.server](lyng.io.http.server.md):** Minimal HTTP/1.1 and WebSocket server. Provides `HttpServer`, `Router`, `ServerRequest`, `RequestContext`, and `ServerWebSocket`.
|
||||
- **[lyng.io.ws](lyng.io.ws.md):** WebSocket client access. Provides `Ws`, `WsSession`, and `WsMessage`.
|
||||
- **[lyng.io.net](lyng.io.net.md):** Transport networking. Provides `Net`, `TcpSocket`, `TcpServer`, `UdpSocket`, and `SocketAddress`.
|
||||
- **Shared networking type packages:** `lyng.io.http.types`, `lyng.io.ws.types`, and `lyng.io.net.types` expose reusable value types such as `HttpHeaders`, `WsMessage`, `IpVersion`, `SocketAddress`, and `Datagram` when host code wants type-only imports without installing the corresponding capability object module.
|
||||
|
||||
@ -6,6 +6,7 @@ import net.sergeych.lyng.Scope
|
||||
import net.sergeych.lyng.ScopeFacade
|
||||
import net.sergeych.lyng.Source
|
||||
import net.sergeych.lyng.Arguments
|
||||
import net.sergeych.lyng.CallSignature
|
||||
import net.sergeych.lyng.TypeDecl
|
||||
import net.sergeych.lyng.asFacade
|
||||
import net.sergeych.lyng.obj.Obj
|
||||
@ -70,14 +71,15 @@ fun createHttpServer(policy: NetAccessPolicy, manager: ImportManager): Boolean =
|
||||
|
||||
private suspend fun buildHttpServerModule(module: ModuleScope, policy: NetAccessPolicy) {
|
||||
module.eval(Source(HTTP_SERVER_MODULE_NAME, http_serverLyng))
|
||||
val serverExchangeClass = ObjServerExchange.type(module.requireClass("ServerExchange"))
|
||||
val requestContextClass = ObjServerExchange.type(module.requireClass("RequestContext"))
|
||||
module.addConst("HttpHeaders", ObjHttpHeaders.type)
|
||||
module.addConst("WsMessage", ObjWsMessage.type)
|
||||
module.addConst("ServerRequest", ObjServerRequest.type)
|
||||
module.addConst("ServerExchange", serverExchangeClass)
|
||||
module.addConst("RequestContext", requestContextClass)
|
||||
module.addConst("ServerWebSocket", ObjServerWebSocket.type)
|
||||
module.addConst("HttpServerHandle", ObjHttpServerHandle.type)
|
||||
module.addConst("HttpServer", ObjLyngHttpServer.type(policy, serverExchangeClass))
|
||||
module.addConst("Router", ObjLyngRouter.type(requestContextClass))
|
||||
module.addConst("HttpServer", ObjLyngHttpServer.type(policy, requestContextClass))
|
||||
}
|
||||
|
||||
private suspend inline fun ScopeFacade.httpServerGuard(crossinline block: suspend () -> Obj): Obj {
|
||||
@ -104,6 +106,9 @@ private fun captureCallable(scope: Scope, rawCallable: Obj): RegisteredCallable
|
||||
private suspend fun RegisteredCallable.call(vararg args: Obj): Obj =
|
||||
scope.asFacade().call(callable, Arguments(args.toList()))
|
||||
|
||||
private suspend fun RegisteredCallable.callWithReceiver(receiver: Obj, vararg args: Obj): Obj =
|
||||
scope.asFacade().call(callable, Arguments(args.toList()), receiver)
|
||||
|
||||
private val stringType = TypeDecl.Simple("String", false)
|
||||
private val nullableStringType = TypeDecl.Simple("String", true)
|
||||
private val boolType = TypeDecl.Simple("Bool", false)
|
||||
@ -115,11 +120,12 @@ private val nullableRegexMatchType = TypeDecl.Simple("RegexMatch", true)
|
||||
private val voidType = TypeDecl.Simple("Void", false)
|
||||
private val httpHeadersType = TypeDecl.Simple("HttpHeaders", false)
|
||||
private val serverRequestType = TypeDecl.Simple("ServerRequest", false)
|
||||
private val serverExchangeType = TypeDecl.Simple("ServerExchange", false)
|
||||
private val requestContextType = TypeDecl.Simple("RequestContext", false)
|
||||
private val serverWebSocketType = TypeDecl.Simple("ServerWebSocket", false)
|
||||
private val nullableServerWsMessageType = TypeDecl.Simple("WsMessage", true)
|
||||
private val httpServerHandleType = TypeDecl.Simple("HttpServerHandle", false)
|
||||
private val httpServerType = TypeDecl.Simple("HttpServer", false)
|
||||
private val routerType = TypeDecl.Simple("Router", false)
|
||||
private val nullableAnyType = TypeDecl.TypeNullableAny
|
||||
|
||||
private fun listType(item: TypeDecl) = TypeDecl.Generic("List", listOf(item), false)
|
||||
@ -129,10 +135,17 @@ private fun unionType(vararg options: TypeDecl) = TypeDecl.Union(options.toList(
|
||||
private fun fnType(returnType: TypeDecl, vararg params: TypeDecl) =
|
||||
TypeDecl.Function(receiver = null, params = params.toList(), returnType = returnType)
|
||||
|
||||
private fun receiverFnType(receiver: TypeDecl, returnType: TypeDecl, vararg params: TypeDecl) =
|
||||
TypeDecl.Function(receiver = receiver, params = params.toList(), returnType = returnType)
|
||||
|
||||
private fun receiverCallSignature(receiverTypeName: String) =
|
||||
CallSignature(tailBlockReceiverType = receiverTypeName)
|
||||
|
||||
private fun bridgeFn(
|
||||
owner: ObjClass,
|
||||
name: String,
|
||||
typeDecl: TypeDecl.Function,
|
||||
callSignature: net.sergeych.lyng.CallSignature? = null,
|
||||
code: suspend ScopeFacade.() -> Obj,
|
||||
) {
|
||||
owner.createField(
|
||||
@ -140,6 +153,7 @@ private fun bridgeFn(
|
||||
initialValue = ObjExternCallable.fromBridge { code() },
|
||||
type = net.sergeych.lyng.obj.ObjRecord.Type.Fun,
|
||||
typeDecl = typeDecl,
|
||||
callSignature = callSignature,
|
||||
)
|
||||
}
|
||||
|
||||
@ -157,87 +171,6 @@ private fun bridgeProperty(
|
||||
)
|
||||
}
|
||||
|
||||
private class ObjLyngHttpServer(
|
||||
private val netPolicy: NetAccessPolicy,
|
||||
private val exchangeClass: ObjClass,
|
||||
) : Obj() {
|
||||
private val methodRoutes = linkedMapOf<String, LinkedHashMap<String, RegisteredCallable>>()
|
||||
private val methodRegexRoutes = linkedMapOf<String, MutableList<RegisteredRegexRoute>>()
|
||||
private val anyRoutes = linkedMapOf<String, RegisteredCallable>()
|
||||
private val anyRegexRoutes = mutableListOf<RegisteredRegexRoute>()
|
||||
private val wsRoutes = linkedMapOf<String, RegisteredCallable>()
|
||||
private val wsRegexRoutes = mutableListOf<RegisteredRegexRoute>()
|
||||
private var fallback: RegisteredCallable? = null
|
||||
private var handle: net.sergeych.lyngio.http.server.HttpServer? = null
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type(netPolicy, exchangeClass)
|
||||
|
||||
companion object {
|
||||
private val types = mutableMapOf<Pair<NetAccessPolicy, ObjClass>, ObjClass>()
|
||||
|
||||
fun type(netPolicy: NetAccessPolicy, exchangeClass: ObjClass): ObjClass =
|
||||
types.getOrPut(netPolicy to exchangeClass) {
|
||||
object : ObjClass("HttpServer") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
if (scope.args.list.isNotEmpty()) scope.raiseError("HttpServer() does not accept arguments")
|
||||
return ObjLyngHttpServer(netPolicy, exchangeClass)
|
||||
}
|
||||
}.apply {
|
||||
val routeArgType = unionType(stringType, regexType)
|
||||
val exchangeHandlerType = fnType(nullableAnyType, serverExchangeType)
|
||||
val webSocketHandlerType = fnType(nullableAnyType, serverWebSocketType, serverExchangeType)
|
||||
|
||||
bridgeFn(this, "get", fnType(httpServerType, routeArgType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("GET", this)
|
||||
}
|
||||
bridgeFn(this, "getPath", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("GET", this)
|
||||
}
|
||||
bridgeFn(this, "post", fnType(httpServerType, routeArgType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("POST", this)
|
||||
}
|
||||
bridgeFn(this, "postPath", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("POST", this)
|
||||
}
|
||||
bridgeFn(this, "put", fnType(httpServerType, routeArgType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("PUT", this)
|
||||
}
|
||||
bridgeFn(this, "putPath", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("PUT", this)
|
||||
}
|
||||
bridgeFn(this, "delete", fnType(httpServerType, routeArgType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("DELETE", this)
|
||||
}
|
||||
bridgeFn(this, "deletePath", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("DELETE", this)
|
||||
}
|
||||
bridgeFn(this, "any", fnType(httpServerType, routeArgType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerAny(this)
|
||||
}
|
||||
bridgeFn(this, "anyPath", fnType(httpServerType, stringType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateAny(this)
|
||||
}
|
||||
bridgeFn(this, "ws", fnType(httpServerType, routeArgType, webSocketHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerWs(this)
|
||||
}
|
||||
bridgeFn(this, "wsPath", fnType(httpServerType, stringType, webSocketHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateWs(this)
|
||||
}
|
||||
bridgeFn(this, "fallback", fnType(httpServerType, exchangeHandlerType)) {
|
||||
thisAs<ObjLyngHttpServer>().registerFallback(this)
|
||||
}
|
||||
bridgeFn(this, "listen", fnType(httpServerHandleType, intType, nullableStringType, intType)) {
|
||||
thisAs<ObjLyngHttpServer>().listen(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureMutable(scope: ScopeFacade) {
|
||||
if (handle != null) scope.raiseIllegalState("HttpServer routes cannot be modified after listen()")
|
||||
}
|
||||
|
||||
private fun requireRoutePattern(scope: ScopeFacade, index: Int): RoutePattern = when (val path = scope.args.list.getOrNull(index)) {
|
||||
is ObjString -> {
|
||||
if (!path.value.startsWith('/')) scope.raiseIllegalArgument("path must start with '/'")
|
||||
@ -247,8 +180,24 @@ private class ObjLyngHttpServer(
|
||||
else -> scope.raiseClassCastError("path must be String or Regex")
|
||||
}
|
||||
|
||||
private suspend fun registerRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
private fun requirePathTemplate(scope: ScopeFacade, index: Int): String {
|
||||
val template = scope.requiredArg<ObjString>(index).value
|
||||
if (!template.startsWith('/')) scope.raiseIllegalArgument("pathTemplate must start with '/'")
|
||||
return template
|
||||
}
|
||||
|
||||
private class RouteRegistry(
|
||||
private val exchangeClass: ObjClass,
|
||||
) {
|
||||
private val methodRoutes = linkedMapOf<String, LinkedHashMap<String, RegisteredCallable>>()
|
||||
private val methodRegexRoutes = linkedMapOf<String, MutableList<RegisteredRegexRoute>>()
|
||||
private val anyRoutes = linkedMapOf<String, RegisteredCallable>()
|
||||
private val anyRegexRoutes = mutableListOf<RegisteredRegexRoute>()
|
||||
private val wsRoutes = linkedMapOf<String, RegisteredCallable>()
|
||||
private val wsRegexRoutes = mutableListOf<RegisteredRegexRoute>()
|
||||
private var fallback: RegisteredCallable? = null
|
||||
|
||||
suspend fun registerRoute(method: String, scope: ScopeFacade) {
|
||||
val path = requireRoutePattern(scope, 0)
|
||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||
when (path) {
|
||||
@ -265,11 +214,9 @@ private class ObjLyngHttpServer(
|
||||
routes += RegisteredRegexRoute(path.regex, handler)
|
||||
}
|
||||
}
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
suspend fun registerTemplateRoute(method: String, scope: ScopeFacade) {
|
||||
val template = requirePathTemplate(scope, 0)
|
||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||
val routes = methodRegexRoutes.getOrPut(method) { mutableListOf() }
|
||||
@ -278,11 +225,9 @@ private class ObjLyngHttpServer(
|
||||
scope.raiseIllegalArgument("duplicate path route for $method $template")
|
||||
}
|
||||
routes += RegisteredRegexRoute(compiled.pattern, handler, compiled.paramNames, compiled.identity)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
suspend fun registerAny(scope: ScopeFacade) {
|
||||
val path = requireRoutePattern(scope, 0)
|
||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||
when (path) {
|
||||
@ -297,11 +242,9 @@ private class ObjLyngHttpServer(
|
||||
anyRegexRoutes += RegisteredRegexRoute(path.regex, handler)
|
||||
}
|
||||
}
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
suspend fun registerTemplateAny(scope: ScopeFacade) {
|
||||
val template = requirePathTemplate(scope, 0)
|
||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||
val compiled = compilePathTemplate(template, scope)
|
||||
@ -309,11 +252,9 @@ private class ObjLyngHttpServer(
|
||||
scope.raiseIllegalArgument("duplicate path route for ANY $template")
|
||||
}
|
||||
anyRegexRoutes += RegisteredRegexRoute(compiled.pattern, handler, compiled.paramNames, compiled.identity)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
suspend fun registerWs(scope: ScopeFacade) {
|
||||
val path = requireRoutePattern(scope, 0)
|
||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||
when (path) {
|
||||
@ -328,11 +269,9 @@ private class ObjLyngHttpServer(
|
||||
wsRegexRoutes += RegisteredRegexRoute(path.regex, handler)
|
||||
}
|
||||
}
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
suspend fun registerTemplateWs(scope: ScopeFacade) {
|
||||
val template = requirePathTemplate(scope, 0)
|
||||
val handler = captureCallable(scope.requireScope(), scope.args.list[1])
|
||||
val compiled = compilePathTemplate(template, scope)
|
||||
@ -340,51 +279,68 @@ private class ObjLyngHttpServer(
|
||||
scope.raiseIllegalArgument("duplicate websocket path route for $template")
|
||||
}
|
||||
wsRegexRoutes += RegisteredRegexRoute(compiled.pattern, handler, compiled.paramNames, compiled.identity)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerFallback(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
suspend fun registerFallback(scope: ScopeFacade) {
|
||||
fallback = captureCallable(scope.requireScope(), scope.args.list[0])
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private fun requirePathTemplate(scope: ScopeFacade, index: Int): String {
|
||||
val template = scope.requiredArg<ObjString>(index).value
|
||||
if (!template.startsWith('/')) scope.raiseIllegalArgument("pathTemplate must start with '/'")
|
||||
return template
|
||||
fun mount(scope: ScopeFacade, other: RouteRegistry) {
|
||||
other.methodRoutes.forEach { (method, routes) ->
|
||||
val target = methodRoutes.getOrPut(method) { linkedMapOf() }
|
||||
routes.forEach { (path, handler) ->
|
||||
if (target.containsKey(path)) scope.raiseIllegalArgument("duplicate route for $method $path")
|
||||
target[path] = handler
|
||||
}
|
||||
}
|
||||
other.methodRegexRoutes.forEach { (method, routes) ->
|
||||
val target = methodRegexRoutes.getOrPut(method) { mutableListOf() }
|
||||
routes.forEach { route ->
|
||||
if (target.any { it.identity == route.identity }) {
|
||||
scope.raiseIllegalArgument("duplicate route for $method ${route.identity.removePrefix("path:")}")
|
||||
}
|
||||
target += route
|
||||
}
|
||||
}
|
||||
other.anyRoutes.forEach { (path, handler) ->
|
||||
if (anyRoutes.containsKey(path)) scope.raiseIllegalArgument("duplicate route for ANY $path")
|
||||
anyRoutes[path] = handler
|
||||
}
|
||||
other.anyRegexRoutes.forEach { route ->
|
||||
if (anyRegexRoutes.any { it.identity == route.identity }) {
|
||||
scope.raiseIllegalArgument("duplicate route for ANY ${route.identity.removePrefix("path:")}")
|
||||
}
|
||||
anyRegexRoutes += route
|
||||
}
|
||||
other.wsRoutes.forEach { (path, handler) ->
|
||||
if (wsRoutes.containsKey(path)) scope.raiseIllegalArgument("duplicate websocket route for $path")
|
||||
wsRoutes[path] = handler
|
||||
}
|
||||
other.wsRegexRoutes.forEach { route ->
|
||||
if (wsRegexRoutes.any { it.identity == route.identity }) {
|
||||
scope.raiseIllegalArgument("duplicate websocket route for ${route.identity.removePrefix("path:")}")
|
||||
}
|
||||
wsRegexRoutes += route
|
||||
}
|
||||
if (other.fallback != null) {
|
||||
if (fallback != null) scope.raiseIllegalArgument("fallback is already defined")
|
||||
fallback = other.fallback
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun listen(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
val port = scope.requiredArg<ObjInt>(0).value.toInt()
|
||||
val host = scope.args.list.getOrNull(1)?.let { objOrNullToString(scope, it, "host") }
|
||||
val backlog = scope.args.list.getOrNull(2)?.let { objToInt(scope, it, "backlog") } ?: 128
|
||||
if (port !in 0..65535) scope.raiseIllegalArgument("port must be in 0..65535")
|
||||
if (backlog <= 0) scope.raiseIllegalArgument("backlog must be positive")
|
||||
netPolicy.require(NetAccessOp.TcpListen(host, port, backlog))
|
||||
val started = startHttpServer(
|
||||
config = HttpServerConfig(host = host ?: "127.0.0.1", port = port, backlog = backlog),
|
||||
) { request ->
|
||||
dispatchRequest(request)
|
||||
}
|
||||
handle = started
|
||||
ObjHttpServerHandle(started)
|
||||
}
|
||||
|
||||
private suspend fun dispatchRequest(request: HttpRequest): HttpHandlerResult {
|
||||
suspend fun dispatchRequest(request: HttpRequest): HttpHandlerResult {
|
||||
val path = request.head.path
|
||||
if (request.head.wantsWebSocketUpgrade) {
|
||||
wsRoutes[path]?.let { route ->
|
||||
return HttpHandlerResult.WebSocket { session ->
|
||||
val exchange = ObjServerExchange(request, null, emptyMap(), exchangeClass)
|
||||
route.call(ObjServerWebSocket(session), exchange)
|
||||
route.callWithReceiver(exchange, ObjServerWebSocket(session))
|
||||
}
|
||||
}
|
||||
matchRegexRoute(wsRegexRoutes, path)?.let { matched ->
|
||||
return HttpHandlerResult.WebSocket { session ->
|
||||
val exchange = ObjServerExchange(request, ObjRegexMatch(matched.match), matched.params, exchangeClass)
|
||||
matched.handler.call(ObjServerWebSocket(session), exchange)
|
||||
matched.handler.callWithReceiver(exchange, ObjServerWebSocket(session))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -393,19 +349,19 @@ private class ObjLyngHttpServer(
|
||||
val exactRoute = methodRoutes[method]?.get(path) ?: anyRoutes[path]
|
||||
if (exactRoute != null) {
|
||||
val exchange = ObjServerExchange(request, null, emptyMap(), exchangeClass)
|
||||
exactRoute.call(exchange)
|
||||
exactRoute.callWithReceiver(exchange)
|
||||
return exchangeResult(exactRoute === fallback, exchange)
|
||||
}
|
||||
|
||||
matchRegexRoute(methodRegexRoutes[method], path)?.let { matched ->
|
||||
val exchange = ObjServerExchange(request, ObjRegexMatch(matched.match), matched.params, exchangeClass)
|
||||
matched.handler.call(exchange)
|
||||
matched.handler.callWithReceiver(exchange)
|
||||
return exchangeResult(false, exchange)
|
||||
}
|
||||
|
||||
matchRegexRoute(anyRegexRoutes, path)?.let { matched ->
|
||||
val exchange = ObjServerExchange(request, ObjRegexMatch(matched.match), matched.params, exchangeClass)
|
||||
matched.handler.call(exchange)
|
||||
matched.handler.callWithReceiver(exchange)
|
||||
return exchangeResult(false, exchange)
|
||||
}
|
||||
|
||||
@ -413,7 +369,7 @@ private class ObjLyngHttpServer(
|
||||
HttpResponse(status = 404, body = "not found".encodeToByteArray())
|
||||
)
|
||||
val exchange = ObjServerExchange(request, null, emptyMap(), exchangeClass)
|
||||
fallbackRoute.call(exchange)
|
||||
fallbackRoute.callWithReceiver(exchange)
|
||||
return exchangeResult(true, exchange)
|
||||
}
|
||||
|
||||
@ -430,6 +386,262 @@ private class ObjLyngHttpServer(
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjLyngHttpServer(
|
||||
private val netPolicy: NetAccessPolicy,
|
||||
private val requestContextClass: ObjClass,
|
||||
) : Obj() {
|
||||
private val routes = RouteRegistry(requestContextClass)
|
||||
private var handle: net.sergeych.lyngio.http.server.HttpServer? = null
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type(netPolicy, requestContextClass)
|
||||
|
||||
companion object {
|
||||
private val types = mutableMapOf<Pair<NetAccessPolicy, ObjClass>, ObjClass>()
|
||||
|
||||
fun type(netPolicy: NetAccessPolicy, requestContextClass: ObjClass): ObjClass =
|
||||
types.getOrPut(netPolicy to requestContextClass) {
|
||||
object : ObjClass("HttpServer") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
if (scope.args.list.isNotEmpty()) scope.raiseError("HttpServer() does not accept arguments")
|
||||
return ObjLyngHttpServer(netPolicy, requestContextClass)
|
||||
}
|
||||
}.apply {
|
||||
val routeArgType = unionType(stringType, regexType)
|
||||
val exchangeHandlerType = receiverFnType(requestContextType, nullableAnyType)
|
||||
val webSocketHandlerType = receiverFnType(requestContextType, nullableAnyType, serverWebSocketType)
|
||||
val exchangeHandlerSignature = receiverCallSignature("RequestContext")
|
||||
|
||||
bridgeFn(this, "get", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("GET", this)
|
||||
}
|
||||
bridgeFn(this, "getPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("GET", this)
|
||||
}
|
||||
bridgeFn(this, "post", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("POST", this)
|
||||
}
|
||||
bridgeFn(this, "postPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("POST", this)
|
||||
}
|
||||
bridgeFn(this, "put", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("PUT", this)
|
||||
}
|
||||
bridgeFn(this, "putPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("PUT", this)
|
||||
}
|
||||
bridgeFn(this, "delete", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerRoute("DELETE", this)
|
||||
}
|
||||
bridgeFn(this, "deletePath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateRoute("DELETE", this)
|
||||
}
|
||||
bridgeFn(this, "any", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerAny(this)
|
||||
}
|
||||
bridgeFn(this, "anyPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateAny(this)
|
||||
}
|
||||
bridgeFn(this, "ws", fnType(httpServerType, routeArgType, webSocketHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerWs(this)
|
||||
}
|
||||
bridgeFn(this, "wsPath", fnType(httpServerType, stringType, webSocketHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerTemplateWs(this)
|
||||
}
|
||||
bridgeFn(this, "fallback", fnType(httpServerType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngHttpServer>().registerFallback(this)
|
||||
}
|
||||
bridgeFn(this, "mount", fnType(httpServerType, routerType)) {
|
||||
thisAs<ObjLyngHttpServer>().mount(this)
|
||||
}
|
||||
bridgeFn(this, "listen", fnType(httpServerHandleType, intType, nullableStringType, intType)) {
|
||||
thisAs<ObjLyngHttpServer>().listen(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureMutable(scope: ScopeFacade) {
|
||||
if (handle != null) scope.raiseIllegalState("HttpServer routes cannot be modified after listen()")
|
||||
}
|
||||
|
||||
private suspend fun registerRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.registerRoute(method, scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.registerTemplateRoute(method, scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.registerAny(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.registerTemplateAny(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.registerWs(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.registerTemplateWs(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerFallback(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.registerFallback(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun mount(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
routes.mount(scope, scope.requiredArg<ObjLyngRouter>(0).routes)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun listen(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
ensureMutable(scope)
|
||||
val port = scope.requiredArg<ObjInt>(0).value.toInt()
|
||||
val host = scope.args.list.getOrNull(1)?.let { objOrNullToString(scope, it, "host") }
|
||||
val backlog = scope.args.list.getOrNull(2)?.let { objToInt(scope, it, "backlog") } ?: 128
|
||||
if (port !in 0..65535) scope.raiseIllegalArgument("port must be in 0..65535")
|
||||
if (backlog <= 0) scope.raiseIllegalArgument("backlog must be positive")
|
||||
netPolicy.require(NetAccessOp.TcpListen(host, port, backlog))
|
||||
val started = startHttpServer(
|
||||
config = HttpServerConfig(host = host ?: "127.0.0.1", port = port, backlog = backlog),
|
||||
) { request ->
|
||||
routes.dispatchRequest(request)
|
||||
}
|
||||
handle = started
|
||||
ObjHttpServerHandle(started)
|
||||
}
|
||||
}
|
||||
|
||||
private class ObjLyngRouter(
|
||||
private val requestContextClass: ObjClass,
|
||||
) : Obj() {
|
||||
val routes = RouteRegistry(requestContextClass)
|
||||
|
||||
override val objClass: ObjClass
|
||||
get() = type(requestContextClass)
|
||||
|
||||
companion object {
|
||||
private val types = mutableMapOf<ObjClass, ObjClass>()
|
||||
|
||||
fun type(requestContextClass: ObjClass): ObjClass =
|
||||
types.getOrPut(requestContextClass) {
|
||||
object : ObjClass("Router") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
if (scope.args.list.isNotEmpty()) scope.raiseError("Router() does not accept arguments")
|
||||
return ObjLyngRouter(requestContextClass)
|
||||
}
|
||||
}.apply {
|
||||
val routeArgType = unionType(stringType, regexType)
|
||||
val exchangeHandlerType = receiverFnType(requestContextType, nullableAnyType)
|
||||
val webSocketHandlerType = receiverFnType(requestContextType, nullableAnyType, serverWebSocketType)
|
||||
val exchangeHandlerSignature = receiverCallSignature("RequestContext")
|
||||
|
||||
bridgeFn(this, "get", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerRoute("GET", this)
|
||||
}
|
||||
bridgeFn(this, "getPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerTemplateRoute("GET", this)
|
||||
}
|
||||
bridgeFn(this, "post", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerRoute("POST", this)
|
||||
}
|
||||
bridgeFn(this, "postPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerTemplateRoute("POST", this)
|
||||
}
|
||||
bridgeFn(this, "put", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerRoute("PUT", this)
|
||||
}
|
||||
bridgeFn(this, "putPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerTemplateRoute("PUT", this)
|
||||
}
|
||||
bridgeFn(this, "delete", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerRoute("DELETE", this)
|
||||
}
|
||||
bridgeFn(this, "deletePath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerTemplateRoute("DELETE", this)
|
||||
}
|
||||
bridgeFn(this, "any", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerAny(this)
|
||||
}
|
||||
bridgeFn(this, "anyPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerTemplateAny(this)
|
||||
}
|
||||
bridgeFn(this, "ws", fnType(routerType, routeArgType, webSocketHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerWs(this)
|
||||
}
|
||||
bridgeFn(this, "wsPath", fnType(routerType, stringType, webSocketHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerTemplateWs(this)
|
||||
}
|
||||
bridgeFn(this, "fallback", fnType(routerType, exchangeHandlerType), exchangeHandlerSignature) {
|
||||
thisAs<ObjLyngRouter>().registerFallback(this)
|
||||
}
|
||||
bridgeFn(this, "mount", fnType(routerType, routerType)) {
|
||||
thisAs<ObjLyngRouter>().mount(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun registerRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.registerRoute(method, scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.registerTemplateRoute(method, scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.registerAny(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateAny(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.registerTemplateAny(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.registerWs(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerTemplateWs(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.registerTemplateWs(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun registerFallback(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.registerFallback(scope)
|
||||
scope.thisObj
|
||||
}
|
||||
|
||||
private suspend fun mount(scope: ScopeFacade): Obj = scope.httpServerGuard {
|
||||
routes.mount(scope, scope.requiredArg<ObjLyngRouter>(0).routes)
|
||||
scope.thisObj
|
||||
}
|
||||
}
|
||||
|
||||
private sealed interface RoutePattern {
|
||||
data class Exact(val path: String) : RoutePattern
|
||||
data class Regex(val regex: ObjRegex) : RoutePattern
|
||||
@ -604,9 +816,9 @@ private class ObjServerExchange(
|
||||
|
||||
fun type(base: ObjClass): ObjClass =
|
||||
types.getOrPut(base) {
|
||||
object : ObjClass("ServerExchange") {
|
||||
object : ObjClass("RequestContext") {
|
||||
override suspend fun callOn(scope: Scope): Obj {
|
||||
scope.raiseError("ServerExchange cannot be created directly")
|
||||
scope.raiseError("RequestContext cannot be created directly")
|
||||
}
|
||||
}.apply {
|
||||
bridgeProperty(this, "request", serverRequestType) {
|
||||
@ -618,8 +830,11 @@ private class ObjServerExchange(
|
||||
bridgeProperty(this, "routeParams", mapType(stringType, stringType)) {
|
||||
thisAs<ObjServerExchange>().routeParams.toObjMap()
|
||||
}
|
||||
addFn(
|
||||
bridgeFn(
|
||||
this,
|
||||
"jsonBody",
|
||||
base.getInstanceMemberOrNull("jsonBody")?.typeDecl as? TypeDecl.Function
|
||||
?: fnType(nullableAnyType),
|
||||
callSignature = base.getInstanceMemberOrNull("jsonBody")?.callSignature
|
||||
) {
|
||||
val self = thisAs<ObjServerExchange>()
|
||||
@ -641,8 +856,11 @@ private class ObjServerExchange(
|
||||
self.setHttpResponse(status, bodyText.encodeToByteArray())
|
||||
ObjVoid
|
||||
}
|
||||
addFn(
|
||||
bridgeFn(
|
||||
this,
|
||||
"respondJson",
|
||||
base.getInstanceMemberOrNull("respondJson")?.typeDecl as? TypeDecl.Function
|
||||
?: fnType(voidType, nullableAnyType, intType),
|
||||
callSignature = base.getInstanceMemberOrNull("respondJson")?.callSignature
|
||||
) {
|
||||
val self = thisAs<ObjServerExchange>()
|
||||
@ -677,14 +895,15 @@ private class ObjServerExchange(
|
||||
bridgeFn(
|
||||
this,
|
||||
"acceptWebSocket",
|
||||
fnType(voidType, fnType(nullableAnyType, serverWebSocketType, serverExchangeType))
|
||||
fnType(voidType, receiverFnType(requestContextType, nullableAnyType, serverWebSocketType)),
|
||||
receiverCallSignature("RequestContext")
|
||||
) {
|
||||
val self = thisAs<ObjServerExchange>()
|
||||
val registered = captureCallable(requireScope(), args.list[0])
|
||||
self.ensureMutable(this)
|
||||
self.result = ExchangeResult.WebSocket(
|
||||
HttpHandlerResult.WebSocket { session ->
|
||||
registered.call(ObjServerWebSocket(session), self)
|
||||
registered.callWithReceiver(self, ObjServerWebSocket(session))
|
||||
}
|
||||
)
|
||||
ObjVoid
|
||||
|
||||
@ -51,12 +51,12 @@ class LyngHttpServerModuleTest {
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
server.get("/hello") { ex ->
|
||||
ex.setHeader("Content-Type", "text/plain")
|
||||
ex.respondText(200, "hello from lyng")
|
||||
server.get("/hello") {
|
||||
setHeader("Content-Type", "text/plain")
|
||||
respondText(200, "hello from lyng")
|
||||
}
|
||||
server.fallback { ex ->
|
||||
ex.respondText(404, "miss:" + ex.request.path)
|
||||
server.fallback {
|
||||
respondText(404, "miss:" + request.path)
|
||||
}
|
||||
server.listen(0, "127.0.0.1")
|
||||
""".trimIndent()
|
||||
@ -101,11 +101,11 @@ class LyngHttpServerModuleTest {
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
server.get("/query") { ex ->
|
||||
val q = ex.request.query
|
||||
ex.respondText(
|
||||
server.get("/query") {
|
||||
val q = request.query
|
||||
respondText(
|
||||
200,
|
||||
(ex.request.queryString ?: "<null>") +
|
||||
(request.queryString ?: "<null>") +
|
||||
"|" + q.size +
|
||||
"|" + (q["a"] ?: "<null>") +
|
||||
"|" + (q["b"] ?: "<null>") +
|
||||
@ -165,20 +165,20 @@ class LyngHttpServerModuleTest {
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) { ex ->
|
||||
val m = ex.routeMatch!!
|
||||
ex.respondText(
|
||||
server.get("^/users/([0-9]+)/posts/([0-9]+)$".re) {
|
||||
val m = routeMatch!!
|
||||
respondText(
|
||||
200,
|
||||
m[1] +
|
||||
"|" + m[2] +
|
||||
"|" + ex.request.pathParts[0] +
|
||||
"," + ex.request.pathParts[1] +
|
||||
"," + ex.request.pathParts[2] +
|
||||
"," + ex.request.pathParts[3]
|
||||
"|" + request.pathParts[0] +
|
||||
"," + request.pathParts[1] +
|
||||
"," + request.pathParts[2] +
|
||||
"," + request.pathParts[3]
|
||||
)
|
||||
}
|
||||
server.get("/users/fixed/posts/9") { ex ->
|
||||
ex.respondText(200, "fixed|" + (ex.routeMatch == null))
|
||||
server.get("/users/fixed/posts/9") {
|
||||
respondText(200, "fixed|" + (routeMatch == null))
|
||||
}
|
||||
server.listen(0, "127.0.0.1")
|
||||
""".trimIndent()
|
||||
@ -223,17 +223,17 @@ class LyngHttpServerModuleTest {
|
||||
import lyng.io.http.server
|
||||
|
||||
val server = HttpServer()
|
||||
server.get("/users/fixed/posts/9") { ex ->
|
||||
ex.respondText(200, "fixed|" + ex.routeParams.size)
|
||||
server.get("/users/fixed/posts/9") {
|
||||
respondText(200, "fixed|" + routeParams.size)
|
||||
}
|
||||
server.getPath("/users/{userId}/posts/{postId}") { ex ->
|
||||
ex.respondText(
|
||||
server.getPath("/users/{userId}/posts/{postId}") {
|
||||
respondText(
|
||||
200,
|
||||
ex.routeParams["userId"] + "|" +
|
||||
ex.routeParams["postId"] + "|" +
|
||||
ex.request.pathParts[1] + "|" +
|
||||
ex.request.pathParts[3] + "|" +
|
||||
(ex.routeMatch != null)
|
||||
routeParams["userId"] + "|" +
|
||||
routeParams["postId"] + "|" +
|
||||
request.pathParts[1] + "|" +
|
||||
request.pathParts[3] + "|" +
|
||||
(routeMatch != null)
|
||||
)
|
||||
}
|
||||
server.listen(0, "127.0.0.1")
|
||||
@ -282,9 +282,9 @@ class LyngHttpServerModuleTest {
|
||||
closed class CreateUserResponse(id: Int, name: String, age: Int)
|
||||
|
||||
val server = HttpServer()
|
||||
server.postPath("/api/users") { ex ->
|
||||
val req = ex.jsonBody<CreateUserRequest>()
|
||||
ex.respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
||||
server.postPath("/api/users") {
|
||||
val req = jsonBody<CreateUserRequest>()
|
||||
respondJson(CreateUserResponse(101, req.name, req.age), 201)
|
||||
}
|
||||
server.listen(0, "127.0.0.1")
|
||||
""".trimIndent()
|
||||
@ -315,6 +315,76 @@ class LyngHttpServerModuleTest {
|
||||
handle.invokeInstanceMethod(scope, "close")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun routerMountPreservesBuiltInRoutingSemantics() = runBlocking {
|
||||
val engine = getSystemNetEngine()
|
||||
if (!engine.isSupported || !engine.isTcpAvailable) return@runBlocking
|
||||
|
||||
val scope = Script.newScope()
|
||||
createHttpServerModule(PermitAllNetAccessPolicy, scope)
|
||||
|
||||
val code = """
|
||||
import lyng.io.http.server
|
||||
|
||||
val api = Router()
|
||||
api.get("/health") {
|
||||
respondText(200, "ok")
|
||||
}
|
||||
|
||||
val users = Router()
|
||||
users.getPath("/users/{id}") {
|
||||
respondText(200, "user:" + routeParams["id"])
|
||||
}
|
||||
users.fallback {
|
||||
respondText(404, "router-miss:" + request.path)
|
||||
}
|
||||
|
||||
api.mount(users)
|
||||
|
||||
val server = HttpServer()
|
||||
server.mount(api)
|
||||
server.listen(0, "127.0.0.1")
|
||||
""".trimIndent()
|
||||
|
||||
val handle = Compiler.compile(code).execute(scope)
|
||||
val port = waitForPort(handle, scope)
|
||||
|
||||
val client = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
||||
try {
|
||||
client.writeUtf8("GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||
client.flush()
|
||||
val response = readHttpResponse(client)
|
||||
assertTrue(response.contains("200 OK"), response)
|
||||
assertTrue(response.endsWith("ok"), response)
|
||||
} finally {
|
||||
client.close()
|
||||
}
|
||||
|
||||
val client2 = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
||||
try {
|
||||
client2.writeUtf8("GET /users/alice HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||
client2.flush()
|
||||
val response = readHttpResponse(client2)
|
||||
assertTrue(response.contains("200 OK"), response)
|
||||
assertTrue(response.endsWith("user:alice"), response)
|
||||
} finally {
|
||||
client2.close()
|
||||
}
|
||||
|
||||
val client3 = engine.tcpConnect("127.0.0.1", port, 2_000, true)
|
||||
try {
|
||||
client3.writeUtf8("GET /missing HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
|
||||
client3.flush()
|
||||
val response = readHttpResponse(client3)
|
||||
assertTrue(response.contains("404"), response)
|
||||
assertTrue(response.endsWith("router-miss:/missing"), response)
|
||||
} finally {
|
||||
client3.close()
|
||||
}
|
||||
|
||||
handle.invokeInstanceMethod(scope, "close")
|
||||
}
|
||||
|
||||
private suspend fun waitForPort(handle: Obj, scope: net.sergeych.lyng.Scope): Int {
|
||||
repeat(100) {
|
||||
val port = runCatching {
|
||||
|
||||
@ -28,7 +28,7 @@ extern class ServerWebSocket {
|
||||
}
|
||||
|
||||
/* Mutable exchange object for one incoming request. */
|
||||
extern class ServerExchange {
|
||||
extern class RequestContext {
|
||||
val request: ServerRequest
|
||||
val routeMatch: RegexMatch?
|
||||
val routeParams: Map<String, String>
|
||||
@ -38,7 +38,7 @@ extern class ServerExchange {
|
||||
fun respondJson(body: Object?, status: Int = 200): void
|
||||
fun setHeader(name: String, value: String): void
|
||||
fun addHeader(name: String, value: String): void
|
||||
fun acceptWebSocket(handler: (ServerWebSocket, ServerExchange) -> Object?): void
|
||||
fun acceptWebSocket(handler: RequestContext.(ServerWebSocket) -> Object?): void
|
||||
fun isHandled(): Bool
|
||||
}
|
||||
|
||||
@ -48,20 +48,39 @@ extern class HttpServerHandle {
|
||||
fun close(): void
|
||||
}
|
||||
|
||||
/* Exact-path HTTP/WebSocket server with built-in router. */
|
||||
/* Reusable route collection mounted into HttpServer or other Router. */
|
||||
extern class Router {
|
||||
fun get(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
||||
fun getPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
||||
fun post(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
||||
fun postPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
||||
fun put(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
||||
fun putPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
||||
fun delete(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
||||
fun deletePath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
||||
fun any(path: String|Regex, handler: RequestContext.() -> Object?): Router
|
||||
fun anyPath(pathTemplate: String, handler: RequestContext.() -> Object?): Router
|
||||
fun ws(path: String|Regex, handler: RequestContext.(ServerWebSocket) -> Object?): Router
|
||||
fun wsPath(pathTemplate: String, handler: RequestContext.(ServerWebSocket) -> Object?): Router
|
||||
fun fallback(handler: RequestContext.() -> Object?): Router
|
||||
fun mount(router: Router): Router
|
||||
}
|
||||
|
||||
/* HTTP/WebSocket server with built-in router. */
|
||||
extern class HttpServer {
|
||||
fun get(path: String|Regex, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun getPath(pathTemplate: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun post(path: String|Regex, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun postPath(pathTemplate: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun put(path: String|Regex, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun putPath(pathTemplate: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun delete(path: String|Regex, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun deletePath(pathTemplate: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun any(path: String|Regex, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun anyPath(pathTemplate: String, handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun ws(path: String|Regex, handler: (ServerWebSocket, ServerExchange) -> Object?): HttpServer
|
||||
fun wsPath(pathTemplate: String, handler: (ServerWebSocket, ServerExchange) -> Object?): HttpServer
|
||||
fun fallback(handler: (ServerExchange) -> Object?): HttpServer
|
||||
fun get(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun getPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun post(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun postPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun put(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun putPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun delete(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun deletePath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun any(path: String|Regex, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun anyPath(pathTemplate: String, handler: RequestContext.() -> Object?): HttpServer
|
||||
fun ws(path: String|Regex, handler: RequestContext.(ServerWebSocket) -> Object?): HttpServer
|
||||
fun wsPath(pathTemplate: String, handler: RequestContext.(ServerWebSocket) -> Object?): HttpServer
|
||||
fun fallback(handler: RequestContext.() -> Object?): HttpServer
|
||||
fun mount(router: Router): HttpServer
|
||||
fun listen(port: Int, host: String? = null, backlog: Int = 128): HttpServerHandle
|
||||
}
|
||||
|
||||
@ -3463,12 +3463,13 @@ class Compiler(
|
||||
is FastLocalVarRef -> ref.name
|
||||
is LocalSlotRef -> ref.name
|
||||
is FieldRef -> ref.name
|
||||
is ImplicitThisMemberRef -> ref.name
|
||||
else -> null
|
||||
}
|
||||
if (name != null) {
|
||||
if (lookupGenericFunctionDecl(name) != null) return true
|
||||
if (name.firstOrNull()?.isUpperCase() == true) return true
|
||||
return ref is FieldRef
|
||||
return ref is FieldRef || ref is ImplicitThisMemberRef
|
||||
}
|
||||
return ref is ConstRef && ref.constValue is ObjClass
|
||||
}
|
||||
@ -3482,6 +3483,13 @@ class Compiler(
|
||||
implicitItType: TypeDecl? = null,
|
||||
expectedCallableType: TypeDecl.Function? = null
|
||||
): ObjRef {
|
||||
fun receiverTypeName(typeDecl: TypeDecl?): String? = when (typeDecl) {
|
||||
is TypeDecl.Simple -> typeDecl.name
|
||||
is TypeDecl.Generic -> typeDecl.name
|
||||
else -> null
|
||||
}
|
||||
|
||||
val effectiveExpectedReceiverType = expectedReceiverType ?: receiverTypeName(expectedCallableType?.receiver)
|
||||
// lambda args are different:
|
||||
val startPos = cc.currentPos()
|
||||
val label = lastLabel
|
||||
@ -3545,7 +3553,13 @@ class Compiler(
|
||||
val capturePlan = CapturePlan(paramSlotPlan, isFunction = true, propagateToParentFunction = lambdaDepth > 0)
|
||||
capturePlanStack.add(capturePlan)
|
||||
val parsedBody = try {
|
||||
inCodeContext(CodeContext.Function("<lambda>", implicitThisMembers = true, implicitThisTypeName = expectedReceiverType)) {
|
||||
inCodeContext(
|
||||
CodeContext.Function(
|
||||
"<lambda>",
|
||||
implicitThisMembers = true,
|
||||
implicitThisTypeName = effectiveExpectedReceiverType
|
||||
)
|
||||
) {
|
||||
val returnLabels = label?.let { setOf(it) } ?: emptySet()
|
||||
returnLabelStack.addLast(returnLabels)
|
||||
try {
|
||||
@ -3798,13 +3812,13 @@ class Compiler(
|
||||
inferredReturnClass = returnClass,
|
||||
inlineBodyRef = inlineBodyRef,
|
||||
supportsDirectInvokeFastPath = supportsDirectInvokeFastPath,
|
||||
preferredThisType = expectedReceiverType,
|
||||
preferredThisType = effectiveExpectedReceiverType,
|
||||
wrapAsExtensionCallable = wrapAsExtensionCallable,
|
||||
returnLabels = returnLabels,
|
||||
pos = startPos
|
||||
)
|
||||
val lambdaTypeDecl = TypeDecl.Function(
|
||||
receiver = null,
|
||||
receiver = effectiveExpectedReceiverType?.let { TypeDecl.Simple(it, false) },
|
||||
params = lambdaParamTypeDecls.toList(),
|
||||
returnType = inferredReturnDecl ?: returnClass?.let { TypeDecl.Simple(it.className, false) } ?: TypeDecl.TypeAny,
|
||||
nullable = false
|
||||
@ -7121,7 +7135,19 @@ class Compiler(
|
||||
}
|
||||
val result = when (left) {
|
||||
is ImplicitThisMemberRef ->
|
||||
if (left.methodId == null && left.fieldId != null) {
|
||||
if (!explicitTypeArgs.isNullOrEmpty()) {
|
||||
val explicitReceiver: ObjRef = (left.preferredThisTypeName() ?: implicitThisTypeName)
|
||||
?.let { QualifiedThisRef(it, left.atPos) }
|
||||
?: LocalVarRef("this", left.atPos)
|
||||
MethodCallRef(
|
||||
explicitReceiver,
|
||||
left.name,
|
||||
args,
|
||||
detectedBlockArgument,
|
||||
isOptional,
|
||||
explicitTypeArgs
|
||||
)
|
||||
} else if (left.methodId == null && left.fieldId != null) {
|
||||
CallRef(left, args, detectedBlockArgument, isOptional, explicitTypeArgs).also { callRef ->
|
||||
applyExplicitCallTypeArgs(callRef, explicitTypeArgs)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user