summaryrefslogtreecommitdiff
path: root/cask
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.li@databricks.com>2019-09-14 16:45:39 +0800
committerLi Haoyi <haoyi.li@databricks.com>2019-09-14 17:40:26 +0800
commit4e853a9d5b9563dbe1909757bf4be4d8e7d2b36a (patch)
tree684863947658d22f91804090c81a68d012b82b6c /cask
parent90b6806e5fb91b207f9d8e2da2a58c25928badea (diff)
downloadcask-4e853a9d5b9563dbe1909757bf4be4d8e7d2b36a.tar.gz
cask-4e853a9d5b9563dbe1909757bf4be4d8e7d2b36a.tar.bz2
cask-4e853a9d5b9563dbe1909757bf4be4d8e7d2b36a.zip
.
Diffstat (limited to 'cask')
-rw-r--r--cask/src/cask/endpoints/FormEndpoint.scala7
-rw-r--r--cask/src/cask/endpoints/JsonEndpoint.scala55
-rw-r--r--cask/src/cask/endpoints/StaticEndpoints.scala8
-rw-r--r--cask/src/cask/endpoints/WebEndpoints.scala14
-rw-r--r--cask/src/cask/endpoints/WebSocketEndpoint.scala17
-rw-r--r--cask/src/cask/internal/Util.scala7
-rw-r--r--cask/src/cask/main/Decorators.scala28
-rw-r--r--cask/src/cask/main/Main.scala34
-rw-r--r--cask/src/cask/model/Response.scala40
-rw-r--r--cask/src/cask/package.scala3
-rw-r--r--cask/test/src/test/cask/FailureTests.scala2
11 files changed, 144 insertions, 71 deletions
diff --git a/cask/src/cask/endpoints/FormEndpoint.scala b/cask/src/cask/endpoints/FormEndpoint.scala
index 471c5e5..436bed4 100644
--- a/cask/src/cask/endpoints/FormEndpoint.scala
+++ b/cask/src/cask/endpoints/FormEndpoint.scala
@@ -44,13 +44,13 @@ object FormReader{
}
}
class postForm(val path: String, override val subpath: Boolean = false) extends Endpoint {
- type Output = Response
+ type InnerReturned = Response.Raw
val methods = Seq("post")
type Input = Seq[FormEntry]
type InputParser[T] = FormReader[T]
def wrapFunction(ctx: Request,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
+ delegate: Delegate): Router.Result[Response.Raw] = {
try {
val formData = FormParserFactory.builder().build().createParser(ctx.exchange).parseBlocking()
delegate(
@@ -62,7 +62,8 @@ class postForm(val path: String, override val subpath: Boolean = false) extends
)
} catch{case e: Exception =>
Router.Result.Success(cask.model.Response(
- "Unable to parse form data: " + e + "\n" + Util.stackTraceString(e)
+ "Unable to parse form data: " + e + "\n" + Util.stackTraceString(e),
+ statusCode = 400
))
}
}
diff --git a/cask/src/cask/endpoints/JsonEndpoint.scala b/cask/src/cask/endpoints/JsonEndpoint.scala
index e0d1257..edf0c46 100644
--- a/cask/src/cask/endpoints/JsonEndpoint.scala
+++ b/cask/src/cask/endpoints/JsonEndpoint.scala
@@ -1,11 +1,11 @@
package cask.endpoints
-import java.io.ByteArrayOutputStream
+import java.io.{ByteArrayOutputStream, InputStream, OutputStream, OutputStreamWriter}
import cask.internal.{Router, Util}
import cask.main.Endpoint
import cask.model.{Request, Response}
-
+import collection.JavaConverters._
sealed trait JsReader[T] extends Router.ArgReader[ujson.Value, T, cask.model.Request]
object JsReader{
@@ -26,13 +26,25 @@ object JsReader{
}
}
}
+trait JsonData extends Response.Data
+object JsonData{
+ implicit class JsonDataImpl[T: upickle.default.Writer](t: T) extends JsonData{
+ def write(out: OutputStream) = {
+ val writer = new OutputStreamWriter(out)
+ implicitly[upickle.default.Writer[T]].write(new ujson.BaseRenderer(writer), t)
+ writer.flush()
+ }
+ }
+}
+
class postJson(val path: String, override val subpath: Boolean = false) extends Endpoint{
- type Output = Response
+ type InnerReturned = Response[JsonData]
val methods = Seq("post")
- type Input = ujson.Js.Value
+ type Input = ujson.Value
type InputParser[T] = JsReader[T]
+ override type OuterReturned = Router.Result[Response.Raw]
def wrapFunction(ctx: Request,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
+ delegate: Delegate): Router.Result[Response.Raw] = {
val obj = for{
str <-
try {
@@ -41,21 +53,42 @@ class postJson(val path: String, override val subpath: Boolean = false) extends
Right(new String(boas.toByteArray))
}
catch{case e: Throwable => Left(cask.model.Response(
- "Unable to deserialize input JSON text: " + e + "\n" + Util.stackTraceString(e)
+ "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)
+ "Input text is invalid JSON: " + e + "\n" + Util.stackTraceString(e),
+ statusCode = 400
))}
obj <-
try Right(json.obj)
- catch {case e: Throwable => Left(cask.model.Response("Input JSON must be a dictionary"))}
+ catch {case e: Throwable => Left(cask.model.Response(
+ "Input JSON must be a dictionary",
+ statusCode = 400
+ ))}
} yield obj.toMap
obj match{
- case Left(r) => Router.Result.Success(r)
- case Right(params) => delegate(params)
+ case Left(r) => Router.Result.Success(r.map(Response.Data.StringData))
+ case Right(params) => delegate(params).map(_.data)
}
}
- def wrapPathSegment(s: String): Input = ujson.Js.Str(s)
+ def wrapPathSegment(s: String): Input = ujson.Str(s)
}
+
+class getJson(val path: String, override val subpath: Boolean = false) extends Endpoint{
+ type InnerReturned = Response[JsonData]
+ val methods = Seq("get")
+ type Input = Seq[String]
+ type InputParser[T] = QueryParamReader[T]
+ override type OuterReturned = Router.Result[Response.Raw]
+ def wrapFunction(ctx: Request,
+ delegate: Delegate): Router.Result[Response.Raw] = {
+
+ val res = delegate(WebEndpoint.buildMapFromQueryParams(ctx))
+
+ res.map(_.data)
+ }
+ def wrapPathSegment(s: String) = Seq(s)
+} \ No newline at end of file
diff --git a/cask/src/cask/endpoints/StaticEndpoints.scala b/cask/src/cask/endpoints/StaticEndpoints.scala
index fd194ca..401f845 100644
--- a/cask/src/cask/endpoints/StaticEndpoints.scala
+++ b/cask/src/cask/endpoints/StaticEndpoints.scala
@@ -4,12 +4,12 @@ import cask.main.Endpoint
import cask.model.Request
class staticFiles(val path: String) extends Endpoint{
- type Output = String
+ type InnerReturned = String
val methods = Seq("get")
type Input = Seq[String]
type InputParser[T] = QueryParamReader[T]
override def subpath = true
- def wrapFunction(ctx: Request, delegate: Delegate): Returned = {
+ def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned = {
delegate(Map()).map(t =>
cask.model.StaticFile(
(cask.internal.Util.splitPath(t) ++ ctx.remainingPathSegments)
@@ -23,12 +23,12 @@ class staticFiles(val path: String) extends Endpoint{
}
class staticResources(val path: String, resourceRoot: ClassLoader = getClass.getClassLoader) extends Endpoint{
- type Output = String
+ type InnerReturned = String
val methods = Seq("get")
type Input = Seq[String]
type InputParser[T] = QueryParamReader[T]
override def subpath = true
- def wrapFunction(ctx: Request, delegate: Delegate): Returned = {
+ def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned = {
delegate(Map()).map(t =>
cask.model.StaticResource(
(cask.internal.Util.splitPath(t) ++ ctx.remainingPathSegments)
diff --git a/cask/src/cask/endpoints/WebEndpoints.scala b/cask/src/cask/endpoints/WebEndpoints.scala
index ab3b480..7cac4f5 100644
--- a/cask/src/cask/endpoints/WebEndpoints.scala
+++ b/cask/src/cask/endpoints/WebEndpoints.scala
@@ -8,12 +8,17 @@ import collection.JavaConverters._
trait WebEndpoint extends Endpoint{
- type Output = Response
+ type InnerReturned = Response.Raw
type Input = Seq[String]
type InputParser[T] = QueryParamReader[T]
def wrapFunction(ctx: Request,
- delegate: Map[String, Input] => Router.Result[Output]): Router.Result[Response] = {
-
+ delegate: Delegate): Router.Result[Response.Raw] = {
+ delegate(WebEndpoint.buildMapFromQueryParams(ctx))
+ }
+ def wrapPathSegment(s: String) = Seq(s)
+}
+object WebEndpoint{
+ def buildMapFromQueryParams(ctx: Request) = {
val b = Map.newBuilder[String, Seq[String]]
val queryParams = ctx.exchange.getQueryParameters
for(k <- queryParams.keySet().iterator().asScala){
@@ -22,9 +27,8 @@ trait WebEndpoint extends Endpoint{
deque.toArray(arr)
b += (k -> (arr: Seq[String]))
}
- delegate(b.result())
+ b.result()
}
- def wrapPathSegment(s: String) = Seq(s)
}
class get(val path: String, override val subpath: Boolean = false) extends WebEndpoint{
val methods = Seq("get")
diff --git a/cask/src/cask/endpoints/WebSocketEndpoint.scala b/cask/src/cask/endpoints/WebSocketEndpoint.scala
index f747341..5f35832 100644
--- a/cask/src/cask/endpoints/WebSocketEndpoint.scala
+++ b/cask/src/cask/endpoints/WebSocketEndpoint.scala
@@ -3,22 +3,25 @@ package cask.endpoints
import cask.internal.Router
import cask.model.Request
import io.undertow.websockets.WebSocketConnectionCallback
-
+import collection.JavaConverters._
sealed trait WebsocketResult
object WebsocketResult{
- implicit class Response(val value: cask.model.Response) extends WebsocketResult
+ implicit class Response[T](value0: cask.model.Response[T])
+ (implicit f: T => cask.model.Response.Data) extends WebsocketResult{
+ def value = value0.map(f)
+ }
implicit class Listener(val value: WebSocketConnectionCallback) extends WebsocketResult
}
class websocket(val path: String, override val subpath: Boolean = false) extends cask.main.BaseEndpoint{
- type Output = WebsocketResult
+ type InnerReturned = WebsocketResult
val methods = Seq("websocket")
type Input = Seq[String]
type InputParser[T] = QueryParamReader[T]
- type Returned = Router.Result[WebsocketResult]
- def wrapFunction(ctx: Request, delegate: Delegate): Returned = delegate(Map())
+ type OuterReturned = Router.Result[WebsocketResult]
+ def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned = {
+ delegate(WebEndpoint.buildMapFromQueryParams(ctx))
+ }
def wrapPathSegment(s: String): Input = Seq(s)
-
-
}
diff --git a/cask/src/cask/internal/Util.scala b/cask/src/cask/internal/Util.scala
index 3f7ab61..87e2a15 100644
--- a/cask/src/cask/internal/Util.scala
+++ b/cask/src/cask/internal/Util.scala
@@ -7,9 +7,14 @@ import scala.collection.mutable
import java.io.OutputStream
import scala.annotation.switch
+import scala.concurrent.{ExecutionContext, Future, Promise}
object Util {
-
+ def firstFutureOf[T](futures: Seq[Future[T]])(implicit ec: ExecutionContext) = {
+ val p = Promise[T]
+ futures.foreach(_.foreach(p.trySuccess))
+ p.future
+ }
/**
* Convert a string to a C&P-able literal. Basically
* copied verbatim from the uPickle source code.
diff --git a/cask/src/cask/main/Decorators.scala b/cask/src/cask/main/Decorators.scala
index 7aa361f..d2fc0c7 100644
--- a/cask/src/cask/main/Decorators.scala
+++ b/cask/src/cask/main/Decorators.scala
@@ -5,12 +5,17 @@ import cask.internal.Router.ArgReader
import cask.model.{Request, Response}
+/**
+ * Annotates a Cask endpoint that returns a HTTP [[Response]]; similar to a
+ * [[Decorator]] but with additional metadata and capabilities.
+ */
trait Endpoint extends BaseEndpoint {
- type Returned = Router.Result[Response]
+ type OuterReturned = Router.Result[Response.Raw]
}
+
/**
- * Used to annotate a single Cask endpoint function; similar to a [[Decorator]]
- * but with additional metadata and capabilities.
+ * An [[Endpoint]] that may return something else than a HTTP response, e.g.
+ * a websocket endpoint which may instead return a websocket event handler
*/
trait BaseEndpoint extends BaseDecorator{
/**
@@ -31,7 +36,7 @@ trait BaseEndpoint extends BaseDecorator{
*/
def subpath: Boolean = false
- def convertToResultType(t: Output): Output = t
+ def convertToResultType(t: InnerReturned): InnerReturned = t
/**
* [[Endpoint]]s are unique among decorators in that they alone can bind
@@ -44,13 +49,16 @@ trait BaseEndpoint extends BaseDecorator{
}
+/**
+ * A [[Decorator]] that may deal with values other than HTTP [[Response]]s
+ */
trait BaseDecorator{
type Input
type InputParser[T] <: ArgReader[Input, T, Request]
- type Output
- type Delegate = Map[String, Input] => Router.Result[Output]
- type Returned <: Router.Result[Any]
- def wrapFunction(ctx: Request, delegate: Delegate): Returned
+ type InnerReturned
+ type Delegate = Map[String, Input] => Router.Result[InnerReturned]
+ type OuterReturned <: Router.Result[Any]
+ def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned
def getParamParser[T](implicit p: InputParser[T]) = p
}
@@ -66,9 +74,9 @@ trait BaseDecorator{
* lists (if any).
*/
trait Decorator extends BaseDecorator{
- type Returned = Router.Result[Response]
+ type OuterReturned = Router.Result[Response.Raw]
type Input = Any
- type Output = Response
+ type InnerReturned = Response.Raw
type InputParser[T] = NoOpParser[Input, T]
}
diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala
index 88d7a61..7128006 100644
--- a/cask/src/cask/main/Main.scala
+++ b/cask/src/cask/main/Main.scala
@@ -48,7 +48,7 @@ abstract class BaseMain{
)
}.toMap
- def writeResponse(exchange: HttpServerExchange, response: Response) = {
+ def writeResponse(exchange: HttpServerExchange, response: Response.Raw) = {
response.headers.foreach{case (k, v) =>
exchange.getResponseHeaders.put(new HttpString(k), v)
}
@@ -58,7 +58,7 @@ abstract class BaseMain{
response.data.write(exchange.getOutputStream)
}
- def handleNotFound(): Response = {
+ def handleNotFound(): Response.Raw = {
Response(
s"Error 404: ${Status.codesToStatus(404).reason}",
statusCode = 404
@@ -68,19 +68,20 @@ abstract class BaseMain{
def defaultHandler = new BlockingHandler(
new HttpHandler() {
- def handleRequest(exchange: HttpServerExchange): Unit = {
+ def handleRequest(exchange: HttpServerExchange): Unit = try {
+// println("Handling Request: " + exchange.getRequestPath)
val (effectiveMethod, runner) = if (exchange.getRequestHeaders.getFirst("Upgrade") == "websocket") {
"websocket" -> ((r: Any) =>
r.asInstanceOf[WebsocketResult] match{
case l: WebsocketResult.Listener =>
io.undertow.Handlers.websocket(l.value).handleRequest(exchange)
- case r: WebsocketResult.Response =>
+ case r: WebsocketResult.Response[_] =>
writeResponseHandler(r).handleRequest(exchange)
}
- )
+ )
} else (
exchange.getRequestMethod.toString.toLowerCase(),
- (r: Any) => writeResponse(exchange, r.asInstanceOf[Response])
+ (r: Any) => writeResponse(exchange, r.asInstanceOf[Response.Raw])
)
routeTries(effectiveMethod).lookup(Util.splitPath(exchange.getRequestPath).toList, Map()) match {
@@ -93,7 +94,7 @@ abstract class BaseMain{
case head :: rest =>
head.wrapFunction(
ctx,
- args => rec(rest, args :: bindings).asInstanceOf[Router.Result[head.Output]]
+ args => rec(rest, args :: bindings).asInstanceOf[Router.Result[head.InnerReturned]]
)
case Nil =>
@@ -116,16 +117,21 @@ abstract class BaseMain{
rec((metadata.decorators ++ routes.decorators ++ mainDecorators).toList, Nil)match{
case Router.Result.Success(res) => runner(res)
case e: Router.Result.Error =>
- writeResponse(exchange, handleEndpointError(exchange, routes, metadata, e))
+ writeResponse(
+ exchange,
+ handleEndpointError(exchange, routes, metadata, e).map(Response.Data.StringData)
+ )
None
}
}
-
+// println("Completed Request: " + exchange.getRequestPath)
+ }catch{case e: Throwable =>
+ e.printStackTrace()
}
}
)
- def writeResponseHandler(r: WebsocketResult.Response) = new BlockingHandler(
+ def writeResponseHandler(r: WebsocketResult.Response[_]) = new BlockingHandler(
new HttpHandler {
def handleRequest(exchange: HttpServerExchange): Unit = {
writeResponse(exchange, r.value)
@@ -142,15 +148,15 @@ abstract class BaseMain{
case _: Router.Result.Error.InvalidArguments => 400
case _: Router.Result.Error.MismatchedArguments => 400
}
- Response(
+ val str =
if (!debugMode) s"Error $statusCode: ${Status.codesToStatus(statusCode).reason}"
else ErrorMsgs.formatInvokeError(
routes,
metadata.entryPoint.asInstanceOf[EntryPoint[cask.main.Routes, _]],
e
- ),
- statusCode = statusCode
- )
+ )
+ println(str)
+ Response(str, statusCode = statusCode)
}
diff --git a/cask/src/cask/model/Response.scala b/cask/src/cask/model/Response.scala
index 59b44c9..5b51689 100644
--- a/cask/src/cask/model/Response.scala
+++ b/cask/src/cask/model/Response.scala
@@ -4,6 +4,7 @@ import java.io.{InputStream, OutputStream, OutputStreamWriter}
import cask.internal.Util
+
/**
* The basic response returned by a HTTP endpoint.
*
@@ -11,38 +12,49 @@ import cask.internal.Util
* bytes, uPickle JSON-convertable types or arbitrary input streams. You can
* also construct your own implementations of `Response.Data`.
*/
-case class Response(
- data: Response.Data,
+case class Response[T](
+ data: T,
statusCode: Int,
headers: Seq[(String, String)],
cookies: Seq[Cookie]
-)
+){
+ def map[V](f: T => V) = new Response(f(data), statusCode, headers, cookies)
+}
object Response{
- def apply(data: Data,
- statusCode: Int = 200,
- headers: Seq[(String, String)] = Nil,
- cookies: Seq[Cookie] = Nil) = new Response(data, statusCode, headers, cookies)
+ type Raw = Response[Data]
+ def apply[T](data: T,
+ statusCode: Int = 200,
+ headers: Seq[(String, String)] = Nil,
+ cookies: Seq[Cookie] = Nil) = new Response(data, statusCode, headers, cookies)
- implicit def dataResponse[T](t: T)(implicit c: T => Data) = Response(t)
+ implicit def dataResponse[T, V](t: T)(implicit c: T => V): Response[V] = {
+ Response[V](t)
+ }
+ implicit def dataResponse2[T, V](t: Response[T])(implicit c: T => V): Response[V] = {
+ t.map(c)
+ }
trait Data{
def write(out: OutputStream): Unit
}
object Data{
+ implicit class UnitData(s: Unit) extends Data{
+ def write(out: OutputStream) = ()
+ }
implicit class StringData(s: String) extends Data{
def write(out: OutputStream) = out.write(s.getBytes)
}
+ implicit class NumericData[T: Numeric](s: T) extends Data{
+ def write(out: OutputStream) = out.write(s.toString.getBytes)
+ }
+ implicit class BooleanData(s: Boolean) extends Data{
+ def write(out: OutputStream) = out.write(s.toString.getBytes)
+ }
implicit class BytesData(b: Array[Byte]) extends Data{
def write(out: OutputStream) = out.write(b)
}
implicit class StreamData(b: InputStream) extends Data{
def write(out: OutputStream) = Util.transferTo(b, out)
}
- implicit def JsonResponse[T: upickle.default.Writer](t: T) = new Data{
- def write(out: OutputStream) = implicitly[upickle.default.Writer[T]].write(
- new ujson.BaseRenderer(new OutputStreamWriter(out)),
- t
- )
- }
}
}
object Redirect{
diff --git a/cask/src/cask/package.scala b/cask/src/cask/package.scala
index 405bab7..51aaaf6 100644
--- a/cask/src/cask/package.scala
+++ b/cask/src/cask/package.scala
@@ -1,6 +1,6 @@
package object cask {
// model
- type Response = model.Response
+ type Response[T] = model.Response[T]
val Response = model.Response
val Abort = model.Abort
val Redirect = model.Redirect
@@ -29,6 +29,7 @@ package object cask {
type staticFiles = endpoints.staticFiles
type staticResources = endpoints.staticResources
type postJson = endpoints.postJson
+ type getJson = endpoints.getJson
type postForm = endpoints.postForm
// main
diff --git a/cask/test/src/test/cask/FailureTests.scala b/cask/test/src/test/cask/FailureTests.scala
index d24d52c..ac0f3d8 100644
--- a/cask/test/src/test/cask/FailureTests.scala
+++ b/cask/test/src/test/cask/FailureTests.scala
@@ -5,7 +5,7 @@ import utest._
object FailureTests extends TestSuite {
class myDecorator extends cask.Decorator {
- def wrapFunction(ctx: Request, delegate: Delegate): Returned = {
+ def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned = {
delegate(Map("extra" -> 31337))
}
}