aboutsummaryrefslogtreecommitdiff
path: root/common
diff options
context:
space:
mode:
authorJakob Odersky <jakob@odersky.com>2018-05-06 13:56:16 -0700
committerJakob Odersky <jakob@odersky.com>2018-05-08 23:02:39 -0700
commit8ecae787ff7124b008229d958c579c73649dd9e4 (patch)
treedad7bea34d9b7ea0f716a783f3b57de491ec959e /common
downloadscala-triad-8ecae787ff7124b008229d958c579c73649dd9e4.tar.gz
scala-triad-8ecae787ff7124b008229d958c579c73649dd9e4.tar.bz2
scala-triad-8ecae787ff7124b008229d958c579c73649dd9e4.zip
Initial commit
Diffstat (limited to 'common')
-rw-r--r--common/js/src/main/scala/JsTemplates.scala3
-rw-r--r--common/js/src/main/scala/http/XhrBackend.scala51
-rw-r--r--common/js/src/main/scala/http/package.scala3
-rw-r--r--common/native/src/main/scala/http/ArrayUtils.scala28
-rw-r--r--common/native/src/main/scala/http/CurlBackend.scala215
-rw-r--r--common/native/src/main/scala/http/curl.scala82
-rw-r--r--common/native/src/main/scala/http/package.scala3
-rw-r--r--common/shared/src/main/scala/ApiProtocol.scala12
-rw-r--r--common/shared/src/main/scala/Message.scala39
-rw-r--r--common/shared/src/main/scala/Templates.scala16
-rw-r--r--common/shared/src/main/scala/TextTemplates.scala46
-rw-r--r--common/shared/src/main/scala/http/Backend.scala8
-rw-r--r--common/shared/src/main/scala/http/Request.scala9
-rw-r--r--common/shared/src/main/scala/http/Response.scala6
14 files changed, 521 insertions, 0 deletions
diff --git a/common/js/src/main/scala/JsTemplates.scala b/common/js/src/main/scala/JsTemplates.scala
new file mode 100644
index 0000000..bbb29c5
--- /dev/null
+++ b/common/js/src/main/scala/JsTemplates.scala
@@ -0,0 +1,3 @@
+package triad
+
+object JsTemplates extends Templates(scalatags.JsDom)
diff --git a/common/js/src/main/scala/http/XhrBackend.scala b/common/js/src/main/scala/http/XhrBackend.scala
new file mode 100644
index 0000000..3a791c1
--- /dev/null
+++ b/common/js/src/main/scala/http/XhrBackend.scala
@@ -0,0 +1,51 @@
+package triad
+package http
+
+import org.scalajs.dom.{ErrorEvent, Event, XMLHttpRequest}
+
+import scala.concurrent.{Future, Promise, TimeoutException}
+import scala.scalajs.js
+import scala.scalajs.js.typedarray.{ArrayBuffer, Int8Array}
+
+trait XhrBackend extends Backend {
+
+ def send(request: Request): Future[Response] = {
+ val promise = Promise[Response]
+ val xhr = new XMLHttpRequest()
+
+ xhr.open(request.method, request.url)
+ xhr.responseType = "arraybuffer"
+ for ((name, value) <- request.headers) {
+ xhr.setRequestHeader(name, value)
+ }
+
+ xhr.send(js.Array(request.body: _*))
+
+ xhr.onload = (e: Event) => {
+ val body: Array[Byte] = if (!js.isUndefined(xhr.response)) {
+ val buffer = new Int8Array(xhr.response.asInstanceOf[ArrayBuffer])
+ buffer.toArray
+ } else {
+ Array.empty[Byte]
+ }
+
+ val response = Response(
+ xhr.status,
+ Map.empty,
+ body
+ )
+ promise.success(response)
+ }
+
+ xhr.onerror = (e: ErrorEvent) => {
+ promise.failure(new RuntimeException(s"XHR error: ${e.message}"))
+ }
+ xhr.ontimeout = (e: Event) => {
+ promise.failure(
+ new TimeoutException(s"Request timed out: ${xhr.statusText}"))
+ }
+
+ promise.future
+ }
+
+}
diff --git a/common/js/src/main/scala/http/package.scala b/common/js/src/main/scala/http/package.scala
new file mode 100644
index 0000000..7b680eb
--- /dev/null
+++ b/common/js/src/main/scala/http/package.scala
@@ -0,0 +1,3 @@
+package triad
+
+package object http extends XhrBackend
diff --git a/common/native/src/main/scala/http/ArrayUtils.scala b/common/native/src/main/scala/http/ArrayUtils.scala
new file mode 100644
index 0000000..00c1067
--- /dev/null
+++ b/common/native/src/main/scala/http/ArrayUtils.scala
@@ -0,0 +1,28 @@
+package triad
+package http
+
+import scala.scalanative.native._
+
+object ArrayUtils {
+
+ def toBuffer(array: Array[Byte])(implicit z: Zone): Ptr[Byte] = {
+ val buffer = z.alloc(array.size)
+ var i = 0
+ while (i < array.size) {
+ buffer(i) = array(i)
+ i += 1
+ }
+ buffer
+ }
+
+ def toArray(buffer: Ptr[Byte], size: CSize): Array[Byte] = {
+ val array = new Array[Byte](size.toInt)
+ var i = 0
+ while (i < array.size) {
+ array(i) = buffer(i)
+ i += 1
+ }
+ array
+ }
+
+}
diff --git a/common/native/src/main/scala/http/CurlBackend.scala b/common/native/src/main/scala/http/CurlBackend.scala
new file mode 100644
index 0000000..4dc8577
--- /dev/null
+++ b/common/native/src/main/scala/http/CurlBackend.scala
@@ -0,0 +1,215 @@
+package triad
+package http
+
+import curl._
+import curlh._
+
+import scala.collection.{Map, mutable}
+import scala.collection.mutable.ArrayBuffer
+import scala.concurrent.Future
+import scala.scalanative.native._
+import scala.util.{Failure, Success, Try}
+
+object CurlBackend {
+
+ type Chunk = CStruct4[Ptr[CStruct0], Ptr[CStruct0], CSize, Ptr[Byte]]
+ implicit class ChunksOps(val self: Ptr[Chunk]) extends AnyVal {
+ @inline def prev: Ptr[Chunk] = (!self._1).cast[Ptr[Chunk]]
+ @inline def prev_=(other: Ptr[Chunk]): Unit =
+ !(self._1) = other.cast[Ptr[CStruct0]]
+ @inline def next: Ptr[Chunk] = (!self._2).cast[Ptr[Chunk]]
+ @inline def next_=(other: Ptr[Chunk]): Unit =
+ !(self._2) = other.cast[Ptr[CStruct0]]
+ @inline def size: CSize = !(self._3)
+ @inline def size_=(value: CSize): Unit = !(self._3) = value
+ @inline def buffer: Ptr[Byte] = !(self._4)
+ @inline def buffer_=(value: Ptr[Byte]): Unit = !(self._4) = value
+ }
+
+ object Chunk {
+
+ @inline def NullPtr[T] = 0.cast[Ptr[T]]
+
+ def allocHead() = allocAppend(0, NullPtr[Chunk])
+
+ def allocAppend(size: CSize,
+ head: Ptr[Chunk] = NullPtr[Chunk]): Ptr[Chunk] = {
+ val chunk: Ptr[Chunk] = stdlib.malloc(sizeof[Chunk]).cast[Ptr[Chunk]]
+ if (chunk == NullPtr[Chunk]) return NullPtr[Chunk]
+
+ chunk.buffer = stdlib.malloc(size)
+
+ if (chunk.buffer == NullPtr[Chunk] && size != 0) {
+ stdlib.free(chunk.cast[Ptr[Byte]])
+ return NullPtr[Chunk]
+ }
+ chunk.size = size
+
+ if (head == NullPtr[Chunk]) { // this will be the head
+ chunk.next = chunk
+ chunk.prev = chunk
+ } else {
+ val last = head.prev
+ last.next = chunk
+ chunk.prev = last
+ head.prev = chunk
+ chunk.next = head
+ }
+ chunk
+ }
+
+ def freeAll(head: Ptr[Chunk]): Unit = {
+ var chunk: Ptr[Chunk] = head
+ do {
+ val next = chunk.next
+ stdlib.free(chunk.buffer)
+ stdlib.free(chunk.cast[Ptr[Byte]])
+ chunk = next
+ } while (chunk != head)
+ }
+
+ def toArray(head: Ptr[Chunk]): Array[Byte] = {
+ val buffer = new ArrayBuffer[Byte]()
+ var chunk = head
+ do {
+ val next = chunk.next
+ var i = 0l
+ while (i < next.size) {
+ buffer += next.buffer(i)
+ i += 1
+ }
+ chunk = next
+ } while (chunk != head)
+ buffer.toArray
+ }
+
+ def traverse(head: Ptr[Chunk])(fct: Array[Byte] => Unit) = {
+ var chunk = head
+ do {
+ val next = chunk.next
+ val buffer = new ArrayBuffer[Byte]()
+ var i = 0l
+ while (i < next.size) {
+ buffer += next.buffer(i)
+ i += 1
+ }
+ chunk = next
+ fct(buffer.toArray)
+ } while (chunk != head)
+ }
+
+ }
+
+ private def receive(data: Ptr[Byte],
+ size: CSize,
+ nmemb: CSize,
+ userdata: Ptr[Byte]): CSize = {
+ val head = userdata.cast[Ptr[Chunk]]
+ val length = size * nmemb
+ val chunk = Chunk.allocAppend(length, head)
+ string.memcpy(chunk.buffer, data, chunk.size)
+ chunk.size
+ }
+ private val receivePtr: WriteFunction = CFunctionPtr.fromFunction4(receive)
+
+ private def chain[A](success: A)(calls: (() => A)*) = {
+ var result: A = success
+ for (c <- calls if result == success) {
+ result = c()
+ }
+ result
+ }
+
+ private def request(request: Request)(implicit z: Zone): Try[Response] = {
+ val curl: CURL = curl_easy_init()
+ if (curl != null) {
+ val errorBuffer = stackalloc[Byte](CURL_ERROR_SIZE)
+ !errorBuffer = 0
+ val requestHeaders = stackalloc[curl_slist](1)
+ !requestHeaders = 0.cast[curl_slist]
+
+ val responseChunks = Chunk.allocHead()
+ val responseHeaderChunks = Chunk.allocHead()
+
+ val curlResult = chain(CURLcode.CURL_OK)(
+ () =>
+ curl_easy_setopt(curl, CURLoption.CURLOPT_ERRORBUFFER, errorBuffer),
+ () =>
+ curl_easy_setopt(curl,
+ CURLoption.CURLOPT_CUSTOMREQUEST,
+ toCString(request.method)),
+ () =>
+ curl_easy_setopt(curl,
+ CURLoption.CURLOPT_URL,
+ toCString(request.url)),
+ () => {
+ val buffer = ArrayUtils.toBuffer(request.body)
+ curl_easy_setopt(curl, CURLoption.CURLOPT_POSTFIELDS, buffer)
+ curl_easy_setopt(curl,
+ CURLoption.CURLOPT_POSTFIELDSIZE,
+ request.body.size)
+ },
+ () => {
+ for ((k, v) <- request.headers) {
+ !requestHeaders =
+ curl_slist_append(!requestHeaders, toCString(s"$k:$v"))
+ }
+ curl_easy_setopt(curl, CURLoption.CURLOPT_HTTPHEADER, !requestHeaders)
+ },
+ () =>
+ curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEFUNCTION, receivePtr),
+ () =>
+ curl_easy_setopt(curl, CURLoption.CURLOPT_WRITEDATA, responseChunks),
+ () =>
+ curl_easy_setopt(curl,
+ CURLoption.CURLOPT_HEADERDATA,
+ responseHeaderChunks),
+ () => curl_easy_perform(curl)
+ )
+
+ val result = curlResult match {
+ case CURLcode.CURL_OK =>
+ val responseCode: Ptr[Long] = stackalloc[Long](1)
+ curl_easy_getinfo(curl, CURLINFO.CURLINFO_RESPONSE_CODE, responseCode)
+
+ val responseHeaders = mutable.HashMap.empty[String, String]
+ Chunk.traverse(responseHeaderChunks) { headerChunk =>
+ val line = new String(headerChunk, "utf-8").trim
+ if (line.contains(":")) {
+ val parts = line.split(":", 2)
+ responseHeaders += parts(0) -> parts(1)
+ }
+ }
+
+ Success(
+ Response(
+ statusCode = (!responseCode).toInt,
+ headers = responseHeaders.toMap,
+ body = Chunk.toArray(responseChunks)
+ ))
+
+ case code =>
+ val message = curl_easy_strerror(curl, code)
+ Failure(
+ new RuntimeException(
+ s"${fromCString(errorBuffer)} (curl exit status $code)"))
+ }
+ Chunk.freeAll(responseChunks)
+ Chunk.freeAll(responseHeaderChunks)
+ curl_slist_free_all(!requestHeaders)
+ curl_easy_cleanup(curl)
+ result
+ } else {
+ Failure(new RuntimeException(s"curl failed to initialize"))
+ }
+ }
+
+ def curlVersion = fromCString(curl_version())
+
+}
+
+trait CurlBackend extends Backend {
+ def send(req: Request): Future[Response] = Zone { implicit z =>
+ Future.fromTry(CurlBackend.request(req))
+ }
+}
diff --git a/common/native/src/main/scala/http/curl.scala b/common/native/src/main/scala/http/curl.scala
new file mode 100644
index 0000000..1eda584
--- /dev/null
+++ b/common/native/src/main/scala/http/curl.scala
@@ -0,0 +1,82 @@
+package triad
+package http
+
+import scala.scalanative.native._
+
+object curlh {
+ final val CURL_ERROR_SIZE = 256
+
+ type CURL = Ptr[CStruct0]
+
+ /*
+ * #define CURLOPTTYPE_LONG 0
+ * #define CURLOPTTYPE_OBJECTPOINT 10000
+ * #define CURLOPTTYPE_STRINGPOINT 10000
+ * #define CURLOPTTYPE_FUNCTIONPOINT 20000
+ * #define CURLOPTTYPE_OFF_T 30000
+ */
+
+ type CURLcode = CInt
+ object CURLcode {
+ final val CURL_OK: CInt = 0
+ }
+
+ type CURLoption = CInt
+ object CURLoption {
+ final val CURLOPT_VERBOSE: CInt = 41
+ final val CURLOPT_POSTFIELDSIZE: CInt = 60
+ final val CURLOPT_WRITEDATA: CInt = 10001
+ final val CURLOPT_URL: CInt = 10002
+ final val CURLOPT_ERRORBUFFER: CInt = 10010
+ final val CURLOPT_POSTFIELDS: CInt = 10015
+ final val CURLOPT_HTTPHEADER: CInt = 10023
+ final val CURLOPT_HEADERDATA: CInt = 10029
+ final val CURLOPT_CUSTOMREQUEST: CInt = 10036
+ final val CURLOPT_WRITEFUNCTION: CInt = 20011
+ }
+
+ type CURLINFO = CInt
+ object CURLINFO {
+ final val CURLINFO_RESPONSE_CODE = 0x200002
+ }
+
+ type WriteFunction = CFunctionPtr4[
+ Ptr[Byte], // data
+ CSize, // size
+ CSize, // nmemb
+ Ptr[Byte], // userdata
+ CSize // return
+ ]
+
+ type curl_slist = Ptr[CStruct0]
+
+}
+
+@link("curl")
+@extern
+object curl {
+ import curlh._
+
+ def curl_easy_init(): CURL = extern
+
+ def curl_easy_setopt(curl: CURL,
+ option: CURLoption,
+ parameter: CVararg*): CURLcode = extern
+
+ def curl_easy_perform(curl: CURL): CURLcode = extern
+
+ def curl_easy_getinfo(curl: CURL,
+ option: CURLINFO,
+ parameter: CVararg*): CURLcode = extern
+
+ def curl_easy_cleanup(curl: CURL): Unit = extern
+
+ def curl_easy_strerror(curl: CURL, code: CURLcode): CString = extern
+
+ def curl_slist_append(head: curl_slist, string: CString): curl_slist = extern
+
+ def curl_slist_free_all(head: curl_slist): Unit = extern
+
+ def curl_version(): CString = extern
+
+}
diff --git a/common/native/src/main/scala/http/package.scala b/common/native/src/main/scala/http/package.scala
new file mode 100644
index 0000000..63b5405
--- /dev/null
+++ b/common/native/src/main/scala/http/package.scala
@@ -0,0 +1,3 @@
+package triad
+
+package object http extends CurlBackend
diff --git a/common/shared/src/main/scala/ApiProtocol.scala b/common/shared/src/main/scala/ApiProtocol.scala
new file mode 100644
index 0000000..5d0e00a
--- /dev/null
+++ b/common/shared/src/main/scala/ApiProtocol.scala
@@ -0,0 +1,12 @@
+package triad
+
+import java.time.Instant
+import spray.json.{DerivedJsonProtocol, JsNumber, JsValue, JsonFormat}
+
+object ApiProtocol extends DerivedJsonProtocol {
+ implicit val timestampFormat: JsonFormat[Instant] = new JsonFormat[Instant] {
+ def read(js: JsValue) = Instant.ofEpochMilli(js.convertTo[Long])
+ def write(i: Instant) = JsNumber(i.toEpochMilli)
+ }
+ implicit val messageFormat = jsonFormat[Message]
+}
diff --git a/common/shared/src/main/scala/Message.scala b/common/shared/src/main/scala/Message.scala
new file mode 100644
index 0000000..7ee36cb
--- /dev/null
+++ b/common/shared/src/main/scala/Message.scala
@@ -0,0 +1,39 @@
+package triad
+
+import java.security.MessageDigest
+import java.time.Instant
+
+case class Message(content: String,
+ author: String,
+ timestamp: Instant = Instant.now()) {
+
+ lazy val id: String = {
+ val digest = MessageDigest.getInstance("SHA-256")
+ digest.update(content.getBytes)
+ digest.update(author.getBytes)
+ digest.update((timestamp.getEpochSecond & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 8) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 16) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 24) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 32) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 40) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 48) & 0xff).toByte)
+ digest.update(((timestamp.getEpochSecond >> 56) & 0xff).toByte)
+ Message.bytesToHex(digest.digest())
+ }
+
+}
+
+object Message {
+ private def bytesToHex(hash: Array[Byte]): String = {
+ val hexString = new StringBuffer(hash.length * 2)
+ var i = 0
+ while (i < hash.length) {
+ val hex = Integer.toHexString(0xff & hash(i))
+ if (hex.length == 1) hexString.append('0')
+ hexString.append(hex)
+ i += 1
+ }
+ hexString.toString
+ }
+}
diff --git a/common/shared/src/main/scala/Templates.scala b/common/shared/src/main/scala/Templates.scala
new file mode 100644
index 0000000..82e1dda
--- /dev/null
+++ b/common/shared/src/main/scala/Templates.scala
@@ -0,0 +1,16 @@
+package triad
+
+class Templates[Builder, Output <: FragT, FragT](
+ val bundle: scalatags.generic.Bundle[Builder, Output, FragT]) {
+ import bundle.all._
+
+ def message(msg: Message) = li(
+ div(`class` := "from")(msg.author),
+ div(`class` := "content")(msg.content)
+ )
+
+ def conversation(messages: Seq[Message]): Tag = ul(id := "conversation")(
+ for (msg <- messages.sortBy(_.timestamp)) yield message(msg)
+ )
+
+}
diff --git a/common/shared/src/main/scala/TextTemplates.scala b/common/shared/src/main/scala/TextTemplates.scala
new file mode 100644
index 0000000..24a3b0e
--- /dev/null
+++ b/common/shared/src/main/scala/TextTemplates.scala
@@ -0,0 +1,46 @@
+package triad
+
+object TextTemplates extends Templates(scalatags.Text) {
+ import bundle.all._
+
+ def scripts(js: Boolean = true) =
+ if (js)
+ Seq(
+ script(`type` := "text/javascript",
+ src := "/assets/ui/js/ui-fastopt.js"),
+ script(`type` := "text/javascript")(
+ raw(
+ """|document.addEventListener("DOMContentLoaded", function(event) {
+ | try {
+ | // root element that will contain the ScalaJS application
+ | var root = document.getElementById("conversation");
+ |
+ | // clear any existing content
+ | while (root.firstChild) {
+ | root.removeChild(root.firstChild);
+ | }
+ |
+ | // run ScalaJS application
+ | console.info("Starting ScalaJS application...")
+ | triad.Main().main(root)
+ | } catch(ex) {
+ | // display warning message in case of exception
+ | //document.getElementById("scalajs-error").style.display = "block";
+ | throw ex;
+ | }
+ |});
+ |""".stripMargin
+ )
+ )
+ )
+ else Seq.empty
+
+ def page(messages: Seq[Message], js: Boolean = true) = html(
+ head(),
+ body(
+ conversation(messages),
+ scripts(js)
+ )
+ )
+
+}
diff --git a/common/shared/src/main/scala/http/Backend.scala b/common/shared/src/main/scala/http/Backend.scala
new file mode 100644
index 0000000..f3ce5f8
--- /dev/null
+++ b/common/shared/src/main/scala/http/Backend.scala
@@ -0,0 +1,8 @@
+package triad
+package http
+
+import scala.concurrent.Future
+
+trait Backend {
+ def send(request: Request): Future[Response]
+}
diff --git a/common/shared/src/main/scala/http/Request.scala b/common/shared/src/main/scala/http/Request.scala
new file mode 100644
index 0000000..ec7d28d
--- /dev/null
+++ b/common/shared/src/main/scala/http/Request.scala
@@ -0,0 +1,9 @@
+package triad
+package http
+
+case class Request(
+ method: String,
+ url: String,
+ headers: Map[String, String] = Map.empty,
+ body: Array[Byte] = Array.empty
+)
diff --git a/common/shared/src/main/scala/http/Response.scala b/common/shared/src/main/scala/http/Response.scala
new file mode 100644
index 0000000..4ba2342
--- /dev/null
+++ b/common/shared/src/main/scala/http/Response.scala
@@ -0,0 +1,6 @@
+package triad
+package http
+
+case class Response(statusCode: Int,
+ headers: Map[String, String],
+ body: Array[Byte])