diff --git a/README.md b/README.md index 19bbf5b..426cce7 100644 --- a/README.md +++ b/README.md @@ -41,77 +41,41 @@ The SDK offers APIs to abstract the Datastar protocol while allowing you to adap The following shows a simple implementation base of the Java `HttpServer`. ```kotlin - -val server = HttpServer.create( - InetSocketAddress(8080), // Port used - 0, // Backlog, 0 means default - "/", // Path - { exchange -> // Exchange handler - - // The `readSignals` method extracts the signals from the request. - // If you use a web framework, you likely don't need this since the framework probably already handles this in its own way. - // However, this method in the SDK allows you to provide your own unmarshalling strategy so you can adapt it to your preferred technology! - val request: Request = adaptRequest(exchange) - val signals = readSignals(request, jsonUnmarshaller) - - // Connect a Datastar SSE generator to the response. - val response: Response = adaptResponse(exchange) - val generator = ServerSentEventGenerator(response) - - - // Below are some simple examples of how to use the generator. - generator.patchElements( - elements = "
Merge
", - ) - - generator.patchSignals( - signals = - """ - { - "one":1, - "two":2 - } - """.trimIndent(), - ) - - generator.executeScript( - script = "alert('Hello World!')", - ) - - exchange.close() - } +// Depending on your context, you'll need to adapt the `Request` and `Response` interfaces, as well as implementation of the `JsonUnmarshaller` type. +val jsonUnmarshaller: JsonUnmarshaller = ... +val request: Request = ... +val response: Response = ... + +// The `readSignals` method extracts the signals from the request. +// If you use a web framework, you likely don't need this since the framework probably already handles this in its own way. +// However, this method in the SDK allows you to provide your own unmarshalling strategy so you can adapt it to your preferred technology! +val request: Request = adaptRequest(exchange) +val signals = readSignals(request, jsonUnmarshaller) + +// Connect a Datastar SSE generator to the response. +val response: Response = adaptResponse(exchange) +val generator = ServerSentEventGenerator(response) + +// Below are some simple examples of how to use the generator. +generator.patchElements( + elements = "
Merge
", ) -fun adaptRequest(exchange: HttpExchange): Request = object : Request { - override fun bodyString() = exchange.requestBody.use { it.readAllBytes().decodeToString() } - - override fun isGet() = exchange.requestMethod == "GET" - - override fun readParam(string: String) = - exchange.requestURI.query - ?.let { URLDecoder.decode(it, Charsets.UTF_8) } - ?.split("&") - ?.find { it.startsWith("$string=") } - ?.substringAfter("=")!! -} - -fun adaptResponse(exchange: HttpExchange): Response = object : Response { - - override fun sendConnectionHeaders( - status: Int, - headers: Map>, - ) { - exchange.responseHeaders.putAll(headers) - exchange.sendResponseHeaders(status, 0) - } +generator.patchSignals( + signals = + """ + { + "one":1, + "two":2 + } + """, +) - override fun write(text: String) { - exchange.responseBody.write(text.toByteArray()) - } +generator.executeScript( + script = "alert('Hello World!')", +) +``` - override fun flush() { - exchange.responseBody.flush() - } +### Examples -} -``` \ No newline at end of file +You can find runnable examples of how to use the SDK in multiple concrete web application frameworks and contexts in the [examples](examples/README.md) folder. \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..85ccb0f --- /dev/null +++ b/examples/README.md @@ -0,0 +1,31 @@ +# Datastar Kotlin Examples + +* [About the examples](#about-the-examples) +* [Running the examples](#running-the-examples) + * [Prerequisites](#prerequisites) + * [Java HTTP Server](#java-http-server) + +## About the examples + +This directory contains examples of using Datastar in Kotlin. +All are a simple counter implemented using Datastar server-sent events. + +- the front end consists in a single HTML page located in the `front` directory +- each back implementation is in a separate directory, consisting in a single Kotlin file + +## Running the examples + +### Prerequisites + +To make the examples work as simply as possible, each back implementation is a JBang script. + +JBang is a tool that allows to run Kotlin scripts taking care of all the dependencies without the need to use more heavy weight tools like Maven or Gradle. +You can find the installation instructions on the [official documentation](https://www.jbang.dev/documentation/jbang/latest/installation.html). + +### Java HTTP Server + +This example uses the plain Java `HttpServer` to serve the front end. ([code](java-httpserver/server.kt)) + +```shell +cd ./java-httpserver ; jbang server.kt ; cd .. +``` diff --git a/examples/front/counter.html b/examples/front/counter.html new file mode 100644 index 0000000..b8a6a79 --- /dev/null +++ b/examples/front/counter.html @@ -0,0 +1,185 @@ + + + + + + Counting Stars + + + + + +
+

Counting Stars

+
+ Loading... +
+
+ + +
+
+ + + \ No newline at end of file diff --git a/examples/java-httpserver/server.kt b/examples/java-httpserver/server.kt new file mode 100755 index 0000000..34b3574 --- /dev/null +++ b/examples/java-httpserver/server.kt @@ -0,0 +1,100 @@ +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer +import dev.datastar.kotlin.sdk.Response +import dev.datastar.kotlin.sdk.ServerSentEventGenerator +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.runBlocking +import java.io.File +import java.net.InetSocketAddress +import java.util.concurrent.Executors + +///usr/bin/env jbang "$0" "$@" ; exit $? +//JAVA 21 +//KOTLIN 2.2.0 +//DEPS dev.cloudgt.datastar:kotlin-sdk:0.1.0 +//DEPS org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2 + + +fun main(): Unit = server().run { + start() + println("Let's go counting star... http://localhost:${address.port}") +} + +fun server( + counter: MutableStateFlow = MutableStateFlow(0), +): HttpServer = HttpServer.create( + InetSocketAddress(8080), + 0 +).apply { + + executor = Executors.newVirtualThreadPerTaskExecutor() + + val counterPage = File( + "../front/counter.html" + ).readBytes() + + createContext("/") { exchange -> + exchange.responseHeaders.add("Content-Type", "text/html") + exchange.sendResponseHeaders(200, counterPage.size.toLong()) + exchange.responseBody.use { os -> + os.write(counterPage) + } + exchange.close() + } + + sseContext(path = "/counter") { + runBlocking { + counter.collect { event -> + this@sseContext.patchElements( + """${event}""" + ) + } + } + } + + createContext("/increment") { exchange -> + counter.value++ + exchange.sendResponseHeaders(204, -1) + exchange.close() + } + + createContext("/decrement") { exchange -> + counter.value-- + exchange.sendResponseHeaders(204, -1) + exchange.close() + } + +} + +private fun HttpServer.sseContext( + path: String, + sender: ServerSentEventGenerator.() -> Unit +) { + createContext(path) { exchange -> + try { + val generator = ServerSentEventGenerator(adaptResponse(exchange)) + sender(generator) + } finally { + exchange.close() + } + } +} + +private fun adaptResponse(exchange: HttpExchange): Response = + object : Response { + override fun sendConnectionHeaders( + status: Int, + headers: Map>, + ) { + exchange.responseHeaders.putAll(headers) + exchange.sendResponseHeaders(status, 0) + } + + override fun write(text: String) { + exchange.responseBody.write(text.toByteArray()) + } + + override fun flush() { + exchange.responseBody.flush() + } + } \ No newline at end of file diff --git a/sdk/build.gradle.kts b/sdk/build.gradle.kts index 17662b0..ae92a8d 100644 --- a/sdk/build.gradle.kts +++ b/sdk/build.gradle.kts @@ -97,7 +97,7 @@ publishing { licenses { license { name = "MIT" - url = "https://mit-license.org/" + url = "https://github.com/starfederation/datastar/blob/develop/LICENSE.md" } } developers {