From 4e853a9d5b9563dbe1909757bf4be4d8e7d2b36a Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Sat, 14 Sep 2019 16:45:39 +0800 Subject: . --- cask/src/cask/endpoints/FormEndpoint.scala | 7 ++-- cask/src/cask/endpoints/JsonEndpoint.scala | 55 ++++++++++++++++++++----- cask/src/cask/endpoints/StaticEndpoints.scala | 8 ++-- cask/src/cask/endpoints/WebEndpoints.scala | 14 ++++--- cask/src/cask/endpoints/WebSocketEndpoint.scala | 17 ++++---- cask/src/cask/internal/Util.scala | 7 +++- cask/src/cask/main/Decorators.scala | 28 ++++++++----- cask/src/cask/main/Main.scala | 34 ++++++++------- cask/src/cask/model/Response.scala | 40 +++++++++++------- cask/src/cask/package.scala | 3 +- cask/test/src/test/cask/FailureTests.scala | 2 +- 11 files changed, 144 insertions(+), 71 deletions(-) (limited to 'cask') 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)) } } -- cgit v1.2.3