-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
090a12e
commit 40e5b37
Showing
27 changed files
with
629 additions
and
228 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,6 @@ | |
.scala-build | ||
.metals | ||
.vscode | ||
.idea | ||
.idea | ||
examples/1-full-stack-app/repo-data | ||
.scala-builder |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# This project is mostly derived from https://github.com/bishabosha/scala3-full-stack-example/tree/for-api-video | ||
scalaVersion = "3.2.2" | ||
|
||
[modules.model] | ||
platforms = ["jvm", "scala-js"] | ||
|
||
[modules.webpage-dom] | ||
platforms = ["scala-js"] | ||
|
||
[modules.webpage-client] | ||
platforms = ["scala-js"] | ||
dependsOn = ["model"] # should smartly chose the scala-js dep | ||
|
||
[modules.webpage] | ||
platforms = ["scala-js"] | ||
kind = "application" | ||
mainClass = "example.start" | ||
dependsOn = ["webpage-client", "webpage-dom"] # should smartly chose the scala-js dep | ||
|
||
[modules.cask-extensions] | ||
|
||
[modules.webserver] | ||
kind = "application" | ||
mainClass = "example.WebServer" | ||
dependsOn = ["model", "cask-extensions"] # should smartly chose the jvm dep | ||
resourceGenerators = [ | ||
# depending on a js application module should select its linked output | ||
{ module = "webpage", dest = "assets/main.js" } | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
//> using dependency "com.lihaoyi::cask:0.9.1" |
51 changes: 51 additions & 0 deletions
51
examples/1-full-stack-app/cask-extensions/src/main/scala/caskx/restApi/endpoints.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package caskx.restApi | ||
|
||
import cask.* | ||
import cask.endpoints.* | ||
import cask.router.* | ||
import cask.internal.Util | ||
|
||
export cask.getJson | ||
|
||
// Source code copied from upickle, but modified to wrap the JSON body in a field called "jsonBody" | ||
abstract class JsonBody(val path: String, override val subpath: Boolean = false) | ||
extends HttpEndpoint[Response[JsonData], ujson.Value] { | ||
val methods = Seq("post") | ||
type InputParser[T] = JsReader[T] | ||
|
||
def wrapFunction(ctx: Request, delegate: Delegate): Result[Response.Raw] = { | ||
val obj = for | ||
str <- | ||
try | ||
val boas = new java.io.ByteArrayOutputStream() | ||
Util.transferTo(ctx.exchange.getInputStream, boas) | ||
Right(new String(boas.toByteArray)) | ||
catch | ||
case e: Throwable => Left(cask.model.Response( | ||
"Unable to deserialize input JSON text: " + e + "\n" + Util.stackTraceString(e), | ||
statusCode = 400 | ||
)) | ||
json <- | ||
try Right(ujson.read(str)) | ||
catch | ||
case e: Throwable => Left(cask.model.Response( | ||
"Input text is invalid JSON: " + e + "\n" + Util.stackTraceString(e), | ||
statusCode = 400 | ||
)) | ||
yield Map("jsonBody" -> json) | ||
obj match | ||
case Left(r) => Result.Success(r.map(Response.Data.WritableData(_))) | ||
case Right(params) => delegate(params) | ||
} | ||
|
||
def wrapPathSegment(s: String): ujson.Value = ujson.Str(s) | ||
} | ||
|
||
class postJson(path: String, subpath: Boolean = false) extends JsonBody(path, subpath): | ||
override val methods = Seq("post") | ||
|
||
class putJson(path: String, subpath: Boolean = false) extends JsonBody(path, subpath): | ||
override val methods = Seq("put") | ||
|
||
class deleteJson(path: String, subpath: Boolean = false) extends cask.getJson(path, subpath): | ||
override val methods = Seq("delete") |
58 changes: 58 additions & 0 deletions
58
examples/1-full-stack-app/cask-extensions/src/main/scala/caskx/websocketApi/actors.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package caskx.websocketApi | ||
|
||
import castor.* | ||
import upickle.default.* | ||
import cask.Ws | ||
import cask.util.Logger | ||
|
||
|
||
case class Broadcast[T](msg: T) | ||
|
||
class WsSubscriberActor[T](using Context, Writer[T]) | ||
extends castor.SimpleActor[Broadcast[T]] { self => | ||
|
||
private val broadcaster = WsBroadcaster[T]() | ||
|
||
def broadcast(t: T): Unit = self.send(Broadcast(t)) | ||
|
||
def run(msg: Broadcast[T]): Unit = broadcaster.send(msg) | ||
|
||
def subscribe(channel: cask.WsChannelActor): castor.Actor[Ws.Event] = | ||
new Subscriber(channel) { self => | ||
broadcaster.send(SubscribeMessage.AddSubscriber(self)) | ||
|
||
def run(msg: Ws.Event): Unit = msg match | ||
case Ws.Close(_, _) => | ||
broadcaster.send(SubscribeMessage.RemoveSubscriber(self)) | ||
case _ => | ||
// ignore other input messages | ||
|
||
} | ||
} | ||
|
||
private type WsSubscriberMessage[T] = SubscribeMessage | Broadcast[T] | ||
|
||
private trait Subscriber(val channel: cask.WsChannelActor) extends castor.SimpleActor[Ws.Event] | ||
|
||
private enum SubscribeMessage: | ||
case AddSubscriber(subscriber: Subscriber) | ||
case RemoveSubscriber(subscriber: Subscriber) | ||
|
||
private class WsBroadcaster[T](using Context, Writer[T]) | ||
extends SimpleActor[WsSubscriberMessage[T]] { outer => | ||
|
||
private var subscribers = Set.empty[Subscriber] | ||
|
||
def run(msg: WsSubscriberMessage[T]): Unit = msg match | ||
case SubscribeMessage.AddSubscriber(subscriber) => | ||
println(s"Adding subscriber $subscriber") | ||
subscribers += subscriber | ||
case SubscribeMessage.RemoveSubscriber(subscriber) => | ||
println(s"Closing subscriber $subscriber") | ||
subscribers -= subscriber | ||
case Broadcast(msg) => | ||
if subscribers.nonEmpty then | ||
val txt = Ws.Text(write(msg)) | ||
subscribers.foreach(_.channel.send(txt)) | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
//> using platform "jvm", "scala-js" | ||
//> using dependency "com.lihaoyi::upickle::3.1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
//> using platform "jvm", "scala-js" | ||
//> using dependency "org.scalameta::munit::1.0.0-M7" |
5 changes: 5 additions & 0 deletions
5
examples/1-full-stack-app/model/src/main/scala/example/Note.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package example | ||
|
||
import upickle.default.{ReadWriter as Codec, *} | ||
|
||
final case class Note(id: String, title: String, content: String) derives Codec |
8 changes: 8 additions & 0 deletions
8
examples/1-full-stack-app/model/src/main/scala/example/SubscriptionMessage.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package example | ||
|
||
import upickle.default.{ReadWriter as Codec, *} | ||
|
||
enum SubscriptionMessage derives Codec: | ||
case Delete(noteId: String) | ||
case Create(note: Note) | ||
case Update(note: Note) |
10 changes: 10 additions & 0 deletions
10
examples/1-full-stack-app/model/src/test/scala/example/JsonParsingSpec.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
package example | ||
|
||
import upickle.default.* | ||
|
||
class JsonParsingSpec extends munit.FunSuite: | ||
test("parse Note") { | ||
val note = Note("1234", "Hello, world!", "Nice to meet you") | ||
val json = write(note) | ||
assertEquals(read[Note](json), note) | ||
} |
74 changes: 74 additions & 0 deletions
74
examples/1-full-stack-app/webpage-client/scala/example/HttpClient.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package example | ||
|
||
import org.scalajs.dom.* | ||
import scala.scalajs.js | ||
|
||
import java.io.IOException | ||
|
||
import scala.concurrent.Future | ||
import scala.concurrent.ExecutionContext | ||
|
||
import upickle.default.* | ||
|
||
class HttpClient(using ExecutionContext): | ||
|
||
private var _socket: Option[WebSocket] = None | ||
|
||
def subscribe(op: SubscriptionMessage => Unit): Unit = | ||
_socket match | ||
case None => | ||
val socket = new WebSocket(s"ws://${window.location.host}/api/notes/subscribe") | ||
_socket = Some(socket) | ||
socket.onmessage = e => | ||
val msg = read[SubscriptionMessage](e.data.toString) | ||
op(msg) | ||
socket.onclose = e => | ||
_socket = None | ||
|
||
case Some(value) => // already subscribed | ||
|
||
def unsubscribe(): Unit = | ||
_socket match | ||
case Some(socket) => | ||
socket.close() | ||
_socket = None | ||
|
||
case None => // already unsubscribed | ||
|
||
|
||
def getAllNotes(): Future[Seq[Note]] = | ||
for | ||
resp <- Fetch.fetch("./api/notes/all").toFuture | ||
notes <- resp.to[Seq[Note]] | ||
yield notes | ||
|
||
def createNote(title: String, content: String): Future[Note] = | ||
val request = Request( | ||
"./api/notes/create", | ||
new: | ||
method = HttpMethod.POST | ||
headers = js.Dictionary("Content-Type" -> "application/json") | ||
body = write(ujson.Obj("title" -> title, "content" -> content)) | ||
) | ||
for | ||
resp <- Fetch.fetch(request).toFuture | ||
note <- resp.to[Note] | ||
yield note | ||
|
||
def deleteNote(id: String): Future[Boolean] = | ||
val request = Request( | ||
s"./api/notes/delete/$id", | ||
new: | ||
method = HttpMethod.DELETE | ||
) | ||
for | ||
resp <- Fetch.fetch(request).toFuture | ||
res <- resp.to[Boolean] | ||
yield res | ||
|
||
extension (resp: Response) | ||
private def to[T: Reader]: Future[T] = | ||
if resp.ok then | ||
for json <- resp.text().toFuture | ||
yield read[T](json) | ||
else Future.failed(new IOException(resp.statusText)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
//> using platform "scala-js" | ||
//> using dep "org.scala-js::scalajs-dom::2.4.0" |
40 changes: 40 additions & 0 deletions
40
examples/1-full-stack-app/webpage-dom/scala/example/DomHelper.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package example | ||
|
||
import org.scalajs.dom.document | ||
import org.scalajs.dom.html.* | ||
|
||
object DomHelper: | ||
export org.scalajs.dom.html.Element | ||
|
||
def div(children: Element*): Div = | ||
val elem = document.createElement("div") | ||
for child <- children do elem.appendChild(child) | ||
elem.asInstanceOf[Div] | ||
|
||
def h1(textContent: String): Heading = | ||
val elem = document.createElement("h1") | ||
elem.textContent = textContent | ||
elem.asInstanceOf[Heading] | ||
|
||
def h2(textContent: String): Heading = | ||
val elem = document.createElement("h2") | ||
elem.textContent = textContent | ||
elem.asInstanceOf[Heading] | ||
|
||
def p(textContent: String): Paragraph = | ||
val elem = document.createElement("p") | ||
elem.textContent = textContent | ||
elem.asInstanceOf[Paragraph] | ||
|
||
def input(): Input = | ||
val elem = document.createElement("input") | ||
elem.asInstanceOf[Input] | ||
|
||
def textarea(): TextArea = | ||
val elem = document.createElement("textarea") | ||
elem.asInstanceOf[TextArea] | ||
|
||
def button(textContent: String): Button = | ||
val elem = document.createElement("button") | ||
elem.textContent = textContent | ||
elem.asInstanceOf[Button] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
//> using platform "scala-js" | ||
//> using dep "org.scala-js::scalajs-dom::2.4.0" |
Oops, something went wrong.