Skip to content

Commit

Permalink
KRZ-180 Async with ID
Browse files Browse the repository at this point in the history
  • Loading branch information
nob13 committed Apr 18, 2024
1 parent 663a269 commit 9a7b21a
Show file tree
Hide file tree
Showing 21 changed files with 290 additions and 36 deletions.
4 changes: 3 additions & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ It is built upon:
- [ScalaTags](https://github.com/com-lihaoyi/scalatags) (optional)
- [Circe](https://circe.github.io/circe/) (optional)
- [ZIO](https://zio.dev/) (optional, example server)
- [Tapir](https://github.com/softwaremill/tapir) (optional, example server)

Please note, this is beta software and some parts may have been hastily written.

Expand Down Expand Up @@ -51,7 +52,8 @@ For simplification, there is a `SimpleComponentBase`, which makes it easier to i
- `extras`: Contains various components, including a simple router.
- `rpc`: An experimental RPC library for making calls between JavaScript and JVM. Needs `@experimental`-Annotation
- `examples`: Sample applications.
- `miniserver`: A simple ZIO-ZHTTP-based server for starting the example application.
- `miniserver-ziohttp`: A simple ZIO-ZHTTP-based server for starting the example application.
- `miniserver-loom`: A Simple Tapir/Virtual threads based server for starting the example application (Needs JVM >=21)
- `engine-common`: Contains common engine code.
- `engine-naive`: Contains the naive rendering engine.
- `runner`: Wraps the naive engine with examples.
Expand Down
61 changes: 50 additions & 11 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ val scalaJsJavaTimeVersion = "2.5.0"
val scalaXmlVersion = "2.1.0"
val scalaTagsVersion = "0.12.0"
val circeVersion = "0.14.6"
val tapirVersion = "1.9.11"
val questVersion = "0.2.0"

val isIntelliJ = {
val isIdea = sys.props.get("idea.managed").contains("true")
Expand Down Expand Up @@ -74,12 +76,9 @@ val logsettings = libraryDependencies ++= Seq(
/** Defines a component. */
lazy val lib = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file("lib"))
.settings(
name := "kreuzberg",
name := "kreuzberg",
testSettings,
publishSettings,
libraryDependencies += (
"dev.zio" %%% "zio" % zioVersion % (if (isIntelliJ) Compile else Provided)
)
publishSettings
)
.jsSettings(
libraryDependencies ++= Seq(
Expand Down Expand Up @@ -152,17 +151,43 @@ lazy val extras = (crossProject(JSPlatform, JVMPlatform, NativePlatform) in file
)
.dependsOn(lib % "compile->compile;test->test", scalatags)

lazy val miniserver = (project in file("miniserver"))
// Common Code for Server Side
lazy val miniserverCommon = (project in file("miniserver-common"))
.settings(
name := "kreuzberg-miniserver",
name := "kreuzberg-miniserver-common",
testSettings,
publishSettings
)
.dependsOn(lib.jvm, scalatags.jvm, rpc.jvm)

// ZIO Based Mini Server
lazy val miniserverZioHttp = (project in file("miniserver-ziohttp"))
.settings(
name := "kreuzberg-miniserver-ziohttp",
libraryDependencies ++= Seq(
"dev.zio" %% "zio-http" % zioHttpVersion,
"dev.zio" %% "zio-logging-slf4j2" % zioLoggingVersion
),
testSettings,
publishSettings
)
.dependsOn(lib.jvm, scalatags.jvm, rpc.jvm)
.dependsOn(miniserverCommon)

// Tapir/Loom based Mini Server
lazy val miniserverLoom = (project in file("miniserver-loom"))
.settings(
name := "kreuzberg-miniserver-loom",
libraryDependencies ++= Seq(
"org.slf4j" % "slf4j-api" % "2.0.12",
"com.softwaremill.sttp.tapir" %% "tapir-netty-server-loom" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % tapirVersion,
"com.softwaremill.sttp.tapir" %% "tapir-json-circe" % tapirVersion,
"net.reactivecore" %% "quest" % questVersion
),
testSettings,
publishSettings
)
.dependsOn(miniserverCommon)

lazy val examples = (crossProject(JSPlatform, JVMPlatform) in file("examples"))
.settings(
Expand All @@ -179,14 +204,26 @@ lazy val examples = (crossProject(JSPlatform, JVMPlatform) in file("examples"))
scalaJSUseMainModuleInitializer := true
)
.jvmSettings(logsettings)
.jvmConfigure(_.dependsOn(miniserver))
.jvmConfigure(_.dependsOn(miniserverZioHttp, miniserverLoom))
.jsConfigure(_.dependsOn(engineNaive))
.dependsOn(lib, xml, scalatags, extras, rpc)

lazy val runner = (project in file("runner"))
.settings(
Compile / compile := (Compile / compile).dependsOn(examples.js / Compile / fastOptJS).value,
Compile / run / mainClass := (examples.jvm / Compile / run / mainClass).value,
Compile / run / mainClass := Some("kreuzberg.examples.showcase.ServerMain"),
reStartArgs := Seq("serve"),
publishArtifact := false,
publish / skip := true,
publishLocal := {},
testSettings
)
.dependsOn(examples.jvm)

lazy val runnerLoom = (project in file("runner-loom"))
.settings(
Compile / compile := (Compile / compile).dependsOn(examples.js / Compile / fastOptJS).value,
Compile / run / mainClass := Some("kreuzberg.examples.showcase.ServerMainLoom"),
reStartArgs := Seq("serve"),
publishArtifact := false,
publish / skip := true,
Expand Down Expand Up @@ -218,7 +255,9 @@ lazy val root = (project in file("."))
extras.js,
extras.jvm,
extras.native,
miniserver,
miniserverZioHttp,
miniserverLoom,
miniserverCommon,
examples.js,
examples.jvm,
rpc.js,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package kreuzberg.examples.showcase

import kreuzberg.examples.showcase.todo.TodoApi
import kreuzberg.miniserver.*
import kreuzberg.miniserver.ziohttp.Bootstrapper
import kreuzberg.rpc.{Dispatcher, Failure, SecurityError}
import zio.ZIO

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package kreuzberg.examples.showcase

import kreuzberg.examples.showcase.todo.{ListItemResponse, TodoApi}
import kreuzberg.miniserver.loom.MiniServer
import kreuzberg.miniserver.{AssetCandidatePath, AssetPaths, MiniServerConfig}
import kreuzberg.rpc.{Dispatcher, Id, SecurityError}

import scala.annotation.experimental

class TodoServiceLoom extends TodoApi[Id] {
private var items: Vector[String] = Vector.empty

private object lock

override def listItems(): ListItemResponse = {
lock.synchronized {
ListItemResponse(
items,
statusCode = 200
)
}
}

override def addItem(item: String): Unit = {
lock.synchronized {
items = items :+ item
}
}
}

@experimental
object ServerMainLoom extends App {
val todoDispatcher: Dispatcher[Id] =
Dispatcher.makeIdDispatcher[TodoApi[Id]](new TodoServiceLoom: TodoApi[Id]).preRequestFlatMap { request =>
// Demonstrating adding a pre filter
// Note: Headers are lower cased
val id = request.headers.collectFirst {
case (key, value) if key == "x-client-id" => value
}
id match {
case None => throw new SecurityError("Missing client id")
case _ => request
}
}

val config = MiniServerConfig[Id](
AssetPaths(
Seq(
AssetCandidatePath("examples/js/target/client_bundle/client/fast"),
AssetCandidatePath("../examples/js/target/client_bundle/client/fast"),
AssetCandidatePath("../../examples/js/target/client_bundle/client/fast")
)
),
api = Some(todoDispatcher)
)

val miniServer = MiniServer(config)
miniServer.run()
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package kreuzberg.miniserver
import kreuzberg.scalatags.*
import scalatags.Text.all.*
import scalatags.Text.tags2.noscript
import kreuzberg.scalatags._

case class Index(config: MiniServerConfig) {
/** Index page for MiniServer. */
case class Index(config: MiniServerConfig[_]) {
def index = html(
head(
script(src := config.hashedUrl("main.js")),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package kreuzberg.miniserver
import kreuzberg.*
import zio.http.{Http, HttpApp, Request}
import zio.{Task, ZIO}
import kreuzberg.rpc.Dispatcher

case class MiniServerConfig(
/**
* Configuration for MiniServer.
* @tparam F
* Effect Type, can be Id.
*/
case class MiniServerConfig[F[_]](
assetPaths: AssetPaths,
extraJs: Seq[String] = Nil,
extraCss: Seq[String] = Nil,
Expand All @@ -14,9 +18,8 @@ case class MiniServerConfig(
".*\\.js\\.map",
".*\\.css\\.map"
),
api: Option[Task[ZioDispatcher]] = None,
noScriptText: Option[String] = None, // if not given, use default.
extraApp: Option[Task[HttpApp[Any, Throwable]]] = None
api: Option[F[Dispatcher[F]]] = None,
noScriptText: Option[String] = None // if not given, use default.
) {
def hashedUrl(name: String): String = assetPaths.hashedUrl(name, deploymentType)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package kreuzberg.miniserver.loom

import io.circe.Json
import kreuzberg.rpc.{Dispatcher, Id, UnknownServiceError}
import sttp.model.StatusCode
import sttp.tapir.*
import sttp.tapir.json.circe.*

import scala.util.control.NonFatal
import quest._

case class ApiHandler(dispatcher: Dispatcher[Id]) {

val apiEndpoint
: PublicEndpoint[(List[String], Json, List[sttp.model.Header]), (Json, StatusCode), (Json, StatusCode), Any] = {
endpoint.post
.in("api" / paths)
.in(jsonBody[Json])
.in(headers)
.out(jsonBody[Json])
.out(statusCode)
.errorOut(jsonBody[Json])
.errorOut(statusCode)
}

val handler = apiEndpoint.serverLogic[Id] { case (paths, json, headers) =>
quest[Either[(Json, StatusCode), (Json, StatusCode)]] {
val (serviceName, callName) = paths match {
case List(s, c) => (s, c)
case _ => bail(Left(Json.obj("msg" -> Json.fromString("Invalid path")) -> StatusCode.BadRequest))
}

if (!dispatcher.handles(serviceName)) {
bail(Left(UnknownServiceError(serviceName).encodeToJson -> StatusCode.NotFound))
}

val request = kreuzberg.rpc.Request(json, headers.map { h => h.name -> h.value })
val response =
try {
dispatcher.call(serviceName, callName, request)
} catch {
case NonFatal(e) =>
val decoded = kreuzberg.rpc.Failure.fromThrowable(e)
bail(Left(decoded.encodeToJson -> StatusCode.InternalServerError))
}
Right(response.json -> StatusCode(response.statusCode))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package kreuzberg.miniserver.loom

import kreuzberg.miniserver.{DeploymentType, Index, MiniServerConfig}
import org.slf4j.LoggerFactory
import sttp.model.StatusCode
import sttp.tapir.*
import sttp.tapir.server.ServerEndpoint
import sttp.tapir.server.netty.loom.{Id, NettyIdServer}
import sttp.tapir.swagger.bundle.SwaggerInterpreter

import java.nio.ByteBuffer
import scala.util.Using

class MiniServer(config: MiniServerConfig[Id]) {
val logger = LoggerFactory.getLogger(getClass)

def run(): Unit = {

val endpoints = List(
assetHandler,
indexHandler
) ++ apiEndpointHandler.toList ++ List(
otherIndexHandler
)

val docEndpoints: List[ServerEndpoint[Any, Id]] = SwaggerInterpreter()
.fromServerEndpoints[Id](endpoints, "MiniServer", "0.1")

val server = NettyIdServer()
.port(config.port)
.addEndpoints(endpoints)
.addEndpoints(if (config.deploymentType.contains(DeploymentType.Debug)) docEndpoints else Nil)
.start()
logger.info(s"Listening on port ${config.port}")
}

private val indexHtml: String = Index(config).index.toString

val assetEndpoint: PublicEndpoint[List[String], StatusCode, ByteBuffer, Any] = {
endpoint.get
.in("assets" / paths)
.errorOut(statusCode)
.out(byteBufferBody)
}

private val assetHandler = assetEndpoint.serverLogic[Id] { paths =>
val fullName = paths.mkString("/")
config.locateAsset(fullName) match {
case None => Left(StatusCode.NotFound)
case Some(value) =>
Using.resource(value.load()) { data =>
val bytes = data.readAllBytes()
Right(ByteBuffer.wrap(bytes))
}
}
}

val indexEndpoint: PublicEndpoint[Unit, Unit, String, Any] = {
endpoint.get
.in("")
.out(htmlBodyUtf8)
}

private val indexHandler = indexEndpoint.serverLogicSuccess[Id] { _ =>
indexHtml
}

val otherIndexEndpoint: PublicEndpoint[List[String], Unit, String, Any] = {
endpoint.get
.in(paths)
.out(htmlBodyUtf8)
}

private val otherIndexHandler = otherIndexEndpoint.serverLogicSuccess[Id] { _ =>
indexHtml
}

val apiEndpointHandler = config.api.map(ApiHandler(_).handler)
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package kreuzberg.miniserver
package kreuzberg.miniserver.ziohttp

import kreuzberg.rpc.Dispatcher
import zio.{Cause, IO, Task, UIO, ZIO}
import kreuzberg.rpc.Failure
import kreuzberg.rpc.{CodecError, Dispatcher, Failure, ServiceExecutionError}
import zio.http.*
import kreuzberg.rpc.CodecError
import kreuzberg.rpc.ServiceExecutionError
import zio.{Cause, IO, Task, UIO, ZIO}

import scala.util.control.NonFatal

Expand Down

0 comments on commit 9a7b21a

Please sign in to comment.