diff options
author | Jakob Odersky <jakob@odersky.com> | 2018-05-06 13:56:16 -0700 |
---|---|---|
committer | Jakob Odersky <jakob@odersky.com> | 2018-05-08 23:02:39 -0700 |
commit | 8ecae787ff7124b008229d958c579c73649dd9e4 (patch) | |
tree | dad7bea34d9b7ea0f716a783f3b57de491ec959e /common | |
download | scala-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.scala | 3 | ||||
-rw-r--r-- | common/js/src/main/scala/http/XhrBackend.scala | 51 | ||||
-rw-r--r-- | common/js/src/main/scala/http/package.scala | 3 | ||||
-rw-r--r-- | common/native/src/main/scala/http/ArrayUtils.scala | 28 | ||||
-rw-r--r-- | common/native/src/main/scala/http/CurlBackend.scala | 215 | ||||
-rw-r--r-- | common/native/src/main/scala/http/curl.scala | 82 | ||||
-rw-r--r-- | common/native/src/main/scala/http/package.scala | 3 | ||||
-rw-r--r-- | common/shared/src/main/scala/ApiProtocol.scala | 12 | ||||
-rw-r--r-- | common/shared/src/main/scala/Message.scala | 39 | ||||
-rw-r--r-- | common/shared/src/main/scala/Templates.scala | 16 | ||||
-rw-r--r-- | common/shared/src/main/scala/TextTemplates.scala | 46 | ||||
-rw-r--r-- | common/shared/src/main/scala/http/Backend.scala | 8 | ||||
-rw-r--r-- | common/shared/src/main/scala/http/Request.scala | 9 | ||||
-rw-r--r-- | common/shared/src/main/scala/http/Response.scala | 6 |
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]) |