Add HTTP respondHtml sugar

This commit is contained in:
sergeych committed 2026-04-29 22:28:37 +03:00
1 parent c8e03d69ad
commit 739fdfc94b
5 files changed
+138 -9

No files matched your search

@@ -2,6 +2,7 @@ package net.sergeych.lyng.io.http.server
import kotlinx.serialization.json.Json
import net.sergeych.lyng.ModuleScope
import net.sergeych.lyng.Pos
import net.sergeych.lyng.Scope
import net.sergeych.lyng.ScopeFacade
import net.sergeych.lyng.Source
@@ -28,6 +29,7 @@ import net.sergeych.lyng.obj.requiredArg
import net.sergeych.lyng.obj.thisAs
import net.sergeych.lyng.io.http.ObjHttpHeaders
import net.sergeych.lyng.io.http.createHttpTypesModule
import net.sergeych.lyng.io.html.createHtmlModule
import net.sergeych.lyng.io.ws.ObjWsMessage
import net.sergeych.lyng.io.ws.createWsTypesModule
import net.sergeych.lyng.serialization.ObjJsonClass
@@ -60,6 +62,7 @@ fun createHttpServer(policy: NetAccessPolicy, scope: Scope): Boolean = createHtt
fun createHttpServerModule(policy: NetAccessPolicy, manager: ImportManager): Boolean {
createHttpTypesModule(manager)
createWsTypesModule(manager)
createHtmlModule(manager)
if (manager.packageNames.contains(HTTP_SERVER_MODULE_NAME)) return false
manager.addPackage(HTTP_SERVER_MODULE_NAME) { module ->
buildHttpServerModule(module, policy)
@@ -119,6 +122,7 @@ private val regexType = TypeDecl.Simple("Regex", false)
private val nullableRegexMatchType = TypeDecl.Simple("RegexMatch", true)
private val voidType = TypeDecl.Simple("Void", false)
private val httpHeadersType = TypeDecl.Simple("HttpHeaders", false)
private val htmlTagType = TypeDecl.Simple("HtmlTag", false)
private val serverRequestType = TypeDecl.Simple("ServerRequest", false)
private val requestContextType = TypeDecl.Simple("RequestContext", false)
private val serverWebSocketType = TypeDecl.Simple("ServerWebSocket", false)
@@ -876,6 +880,33 @@ private class ObjServerExchange(
self.setHttpResponse(status, bodyText.encodeToByteArray())
ObjVoid
}
bridgeFn(
this,
"respondHtml",
base.getInstanceMemberOrNull("respondHtml")?.typeDecl as? TypeDecl.Function
?: fnType(voidType, intType, receiverFnType(htmlTagType, voidType)),
callSignature = receiverCallSignature("HtmlTag")
) {
val self = thisAs<ObjServerExchange>()
val first = args.list.getOrNull(0)
val second = args.list.getOrNull(1)
val status = args.named["code"]?.let { objToInt(this, it, "code") } ?: when {
first is ObjInt && second != null -> first.value.toInt()
second is ObjInt -> second.value.toInt()
else -> 200
}
val builder = when {
first is ObjInt -> second
else -> first
} ?: raiseIllegalArgument("respondHtml requires a builder")
val htmlModule = requireScope().importManager.createModuleScope(Pos.builtIn, "lyng.io.html")
val htmlFn = htmlModule.get("html")?.value ?: raiseIllegalState("lyng.io.html.html is not available")
val bodyText = (call(htmlFn, Arguments(listOf(builder))) as ObjString).value
self.ensureMutable(this)
self.responseHeaders["Content-Type"] = mutableListOf("text/html; charset=utf-8")
self.setHttpResponse(status, bodyText.encodeToByteArray())
ObjVoid
}
bridgeFn(this, "setHeader", fnType(voidType, stringType, stringType)) {
val self = thisAs<ObjServerExchange>()
val name = requiredArg<ObjString>(0).value
@@ -315,6 +315,53 @@ class LyngHttpServerModuleTest {
handle.invokeInstanceMethod(scope, "close")
}
@Test
fun respondHtmlRendersHtmlDslAndSetsContentType() = 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
import lyng.io.html
val server = HttpServer()
server.getPath("/html/{name}") {
respondHtml(code: 202) {
head { title { +"Greeting" } }
body {
h3 { +("Hello, " + routeParams["name"]) }
}
}
}
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 /html/alice%26bob HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
client.flush()
val response = readHttpResponse(client)
assertTrue(response.contains("202"), response)
assertTrue(response.contains("Content-Type: text/html; charset=utf-8"), response)
assertTrue(
response.endsWith(
"<!doctype html><html><head><title>Greeting</title></head><body><h3>Hello, alice&amp;bob</h3></body></html>"
),
response
)
} finally {
client.close()
}
handle.invokeInstanceMethod(scope, "close")
}
@Test
fun routerMountPreservesBuiltInRoutingSemantics() = runBlocking {
val engine = getSystemNetEngine()