From f74ed9afe4427effb2d42da6d2e5bf00c914868e Mon Sep 17 00:00:00 2001 From: sergeych Date: Sun, 26 Apr 2026 15:57:57 +0300 Subject: [PATCH] Switch HTTP server API to RequestContext receivers --- docs/lyng.io.http.server.md | 83 ++- docs/lyngio.md | 2 +- .../io/http/server/LyngHttpServerModule.kt | 499 +++++++++++++----- .../http/server/LyngHttpServerModuleTest.kt | 130 +++-- lyngio/stdlib/lyng/io/http_server.lyng | 51 +- .../kotlin/net/sergeych/lyng/Compiler.kt | 36 +- 6 files changed, 591 insertions(+), 210 deletions(-) diff --git a/docs/lyng.io.http.server.md b/docs/lyng.io.http.server.md index f49ef2a..e1f8a60 100644 --- a/docs/lyng.io.http.server.md +++ b/docs/lyng.io.http.server.md @@ -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()` 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() +server.postPath("/api/users") { + val req = jsonBody() 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)` diff --git a/docs/lyngio.md b/docs/lyngio.md index da96883..70f6084 100644 --- a/docs/lyngio.md +++ b/docs/lyngio.md @@ -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. diff --git a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModule.kt b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModule.kt index 751280a..3660f15 100644 --- a/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModule.kt +++ b/lyngio/src/commonMain/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModule.kt @@ -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,10 +171,24 @@ private fun bridgeProperty( ) } -private class ObjLyngHttpServer( - private val netPolicy: NetAccessPolicy, +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 '/'") + RoutePattern.Exact(path.value) + } + is ObjRegex -> RoutePattern.Regex(path) + else -> scope.raiseClassCastError("path must be String or Regex") +} + +private fun requirePathTemplate(scope: ScopeFacade, index: Int): String { + val template = scope.requiredArg(index).value + if (!template.startsWith('/')) scope.raiseIllegalArgument("pathTemplate must start with '/'") + return template +} + +private class RouteRegistry( private val exchangeClass: ObjClass, -) : Obj() { +) { private val methodRoutes = linkedMapOf>() private val methodRegexRoutes = linkedMapOf>() private val anyRoutes = linkedMapOf() @@ -168,87 +196,8 @@ private class ObjLyngHttpServer( private val wsRoutes = linkedMapOf() private val wsRegexRoutes = mutableListOf() 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, 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().registerRoute("GET", this) - } - bridgeFn(this, "getPath", fnType(httpServerType, stringType, exchangeHandlerType)) { - thisAs().registerTemplateRoute("GET", this) - } - bridgeFn(this, "post", fnType(httpServerType, routeArgType, exchangeHandlerType)) { - thisAs().registerRoute("POST", this) - } - bridgeFn(this, "postPath", fnType(httpServerType, stringType, exchangeHandlerType)) { - thisAs().registerTemplateRoute("POST", this) - } - bridgeFn(this, "put", fnType(httpServerType, routeArgType, exchangeHandlerType)) { - thisAs().registerRoute("PUT", this) - } - bridgeFn(this, "putPath", fnType(httpServerType, stringType, exchangeHandlerType)) { - thisAs().registerTemplateRoute("PUT", this) - } - bridgeFn(this, "delete", fnType(httpServerType, routeArgType, exchangeHandlerType)) { - thisAs().registerRoute("DELETE", this) - } - bridgeFn(this, "deletePath", fnType(httpServerType, stringType, exchangeHandlerType)) { - thisAs().registerTemplateRoute("DELETE", this) - } - bridgeFn(this, "any", fnType(httpServerType, routeArgType, exchangeHandlerType)) { - thisAs().registerAny(this) - } - bridgeFn(this, "anyPath", fnType(httpServerType, stringType, exchangeHandlerType)) { - thisAs().registerTemplateAny(this) - } - bridgeFn(this, "ws", fnType(httpServerType, routeArgType, webSocketHandlerType)) { - thisAs().registerWs(this) - } - bridgeFn(this, "wsPath", fnType(httpServerType, stringType, webSocketHandlerType)) { - thisAs().registerTemplateWs(this) - } - bridgeFn(this, "fallback", fnType(httpServerType, exchangeHandlerType)) { - thisAs().registerFallback(this) - } - bridgeFn(this, "listen", fnType(httpServerHandleType, intType, nullableStringType, intType)) { - thisAs().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 '/'") - RoutePattern.Exact(path.value) - } - is ObjRegex -> RoutePattern.Regex(path) - else -> scope.raiseClassCastError("path must be String or Regex") - } - - private suspend fun registerRoute(method: String, scope: ScopeFacade): Obj = scope.httpServerGuard { - ensureMutable(scope) + 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(index).value - if (!template.startsWith('/')) scope.raiseIllegalArgument("pathTemplate must start with '/'") - return template - } - - private suspend fun listen(scope: ScopeFacade): Obj = scope.httpServerGuard { - ensureMutable(scope) - val port = scope.requiredArg(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) + 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 } - 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 -> + 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, 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().registerRoute("GET", this) + } + bridgeFn(this, "getPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("GET", this) + } + bridgeFn(this, "post", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerRoute("POST", this) + } + bridgeFn(this, "postPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("POST", this) + } + bridgeFn(this, "put", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerRoute("PUT", this) + } + bridgeFn(this, "putPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("PUT", this) + } + bridgeFn(this, "delete", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerRoute("DELETE", this) + } + bridgeFn(this, "deletePath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("DELETE", this) + } + bridgeFn(this, "any", fnType(httpServerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerAny(this) + } + bridgeFn(this, "anyPath", fnType(httpServerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateAny(this) + } + bridgeFn(this, "ws", fnType(httpServerType, routeArgType, webSocketHandlerType), exchangeHandlerSignature) { + thisAs().registerWs(this) + } + bridgeFn(this, "wsPath", fnType(httpServerType, stringType, webSocketHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateWs(this) + } + bridgeFn(this, "fallback", fnType(httpServerType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerFallback(this) + } + bridgeFn(this, "mount", fnType(httpServerType, routerType)) { + thisAs().mount(this) + } + bridgeFn(this, "listen", fnType(httpServerHandleType, intType, nullableStringType, intType)) { + thisAs().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(0).routes) + scope.thisObj + } + + private suspend fun listen(scope: ScopeFacade): Obj = scope.httpServerGuard { + ensureMutable(scope) + val port = scope.requiredArg(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() + + 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().registerRoute("GET", this) + } + bridgeFn(this, "getPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("GET", this) + } + bridgeFn(this, "post", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerRoute("POST", this) + } + bridgeFn(this, "postPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("POST", this) + } + bridgeFn(this, "put", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerRoute("PUT", this) + } + bridgeFn(this, "putPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("PUT", this) + } + bridgeFn(this, "delete", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerRoute("DELETE", this) + } + bridgeFn(this, "deletePath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateRoute("DELETE", this) + } + bridgeFn(this, "any", fnType(routerType, routeArgType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerAny(this) + } + bridgeFn(this, "anyPath", fnType(routerType, stringType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateAny(this) + } + bridgeFn(this, "ws", fnType(routerType, routeArgType, webSocketHandlerType), exchangeHandlerSignature) { + thisAs().registerWs(this) + } + bridgeFn(this, "wsPath", fnType(routerType, stringType, webSocketHandlerType), exchangeHandlerSignature) { + thisAs().registerTemplateWs(this) + } + bridgeFn(this, "fallback", fnType(routerType, exchangeHandlerType), exchangeHandlerSignature) { + thisAs().registerFallback(this) + } + bridgeFn(this, "mount", fnType(routerType, routerType)) { + thisAs().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(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().routeParams.toObjMap() } - addFn( + bridgeFn( + this, "jsonBody", + base.getInstanceMemberOrNull("jsonBody")?.typeDecl as? TypeDecl.Function + ?: fnType(nullableAnyType), callSignature = base.getInstanceMemberOrNull("jsonBody")?.callSignature ) { val self = thisAs() @@ -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() @@ -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() 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 diff --git a/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModuleTest.kt b/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModuleTest.kt index 123a136..292c511 100644 --- a/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModuleTest.kt +++ b/lyngio/src/jvmTest/kotlin/net/sergeych/lyng/io/http/server/LyngHttpServerModuleTest.kt @@ -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 ?: "") + + (request.queryString ?: "") + "|" + q.size + "|" + (q["a"] ?: "") + "|" + (q["b"] ?: "") + @@ -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() - ex.respondJson(CreateUserResponse(101, req.name, req.age), 201) + server.postPath("/api/users") { + val req = jsonBody() + 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 { diff --git a/lyngio/stdlib/lyng/io/http_server.lyng b/lyngio/stdlib/lyng/io/http_server.lyng index 39c2d0c..a8d6b67 100644 --- a/lyngio/stdlib/lyng/io/http_server.lyng +++ b/lyngio/stdlib/lyng/io/http_server.lyng @@ -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 @@ -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 } diff --git a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt index f1cefe8..4831bb5 100644 --- a/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt +++ b/lynglib/src/commonMain/kotlin/net/sergeych/lyng/Compiler.kt @@ -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("", implicitThisMembers = true, implicitThisTypeName = expectedReceiverType)) { + inCodeContext( + CodeContext.Function( + "", + 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) }