From 583893aff7e39e085dbf5bec27c9b0b24e5e8d2e Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 16 Sep 2019 08:58:31 +0800 Subject: Break up `Router.scala` into a `router/` folder with multiple files --- cask/src/cask/decorators/compress.scala | 1 - cask/src/cask/endpoints/FormEndpoint.scala | 11 +- cask/src/cask/endpoints/JsonEndpoint.scala | 17 +- cask/src/cask/endpoints/ParamReader.scala | 4 +- cask/src/cask/endpoints/StaticEndpoints.scala | 2 +- cask/src/cask/endpoints/WebEndpoints.scala | 8 +- cask/src/cask/endpoints/WebSocketEndpoint.scala | 6 +- cask/src/cask/internal/Router.scala | 351 ---------------------- cask/src/cask/main/Decorators.scala | 136 --------- cask/src/cask/main/ErrorMsgs.scala | 21 +- cask/src/cask/main/Main.scala | 24 +- cask/src/cask/main/Routes.scala | 4 +- cask/src/cask/main/RoutesEndpointMetadata.scala | 68 ----- cask/src/cask/package.scala | 4 +- cask/src/cask/router/Decorators.scala | 136 +++++++++ cask/src/cask/router/EntryPoint.scala | 51 ++++ cask/src/cask/router/Macros.scala | 178 +++++++++++ cask/src/cask/router/Misc.scala | 23 ++ cask/src/cask/router/Result.scala | 66 ++++ cask/src/cask/router/RoutesEndpointMetadata.scala | 68 +++++ cask/src/cask/router/Runtime.scala | 39 +++ 21 files changed, 614 insertions(+), 604 deletions(-) delete mode 100644 cask/src/cask/internal/Router.scala delete mode 100644 cask/src/cask/main/Decorators.scala delete mode 100644 cask/src/cask/main/RoutesEndpointMetadata.scala create mode 100644 cask/src/cask/router/Decorators.scala create mode 100644 cask/src/cask/router/EntryPoint.scala create mode 100644 cask/src/cask/router/Macros.scala create mode 100644 cask/src/cask/router/Misc.scala create mode 100644 cask/src/cask/router/Result.scala create mode 100644 cask/src/cask/router/RoutesEndpointMetadata.scala create mode 100644 cask/src/cask/router/Runtime.scala diff --git a/cask/src/cask/decorators/compress.scala b/cask/src/cask/decorators/compress.scala index 7a7eefb..0ffab28 100644 --- a/cask/src/cask/decorators/compress.scala +++ b/cask/src/cask/decorators/compress.scala @@ -2,7 +2,6 @@ package cask.decorators import java.io.{ByteArrayOutputStream, OutputStream} import java.util.zip.{DeflaterOutputStream, GZIPOutputStream} -import cask.internal.Router import cask.model.{Request, Response} import collection.JavaConverters._ diff --git a/cask/src/cask/endpoints/FormEndpoint.scala b/cask/src/cask/endpoints/FormEndpoint.scala index 6f65786..84ba618 100644 --- a/cask/src/cask/endpoints/FormEndpoint.scala +++ b/cask/src/cask/endpoints/FormEndpoint.scala @@ -1,13 +1,14 @@ package cask.endpoints -import cask.internal.{Router, Util} -import cask.main.HttpEndpoint +import cask.internal.Util +import cask.router.HttpEndpoint import cask.model._ +import cask.router.{ArgReader, Result} import io.undertow.server.handlers.form.FormParserFactory import collection.JavaConverters._ -sealed trait FormReader[T] extends Router.ArgReader[Seq[FormEntry], T, Request] +sealed trait FormReader[T] extends ArgReader[Seq[FormEntry], T, Request] object FormReader{ implicit def paramFormReader[T: QueryParamReader] = new FormReader[T]{ def arity = implicitly[QueryParamReader[T]].arity @@ -49,7 +50,7 @@ class postForm(val path: String, override val subpath: Boolean = false) val methods = Seq("post") type InputParser[T] = FormReader[T] def wrapFunction(ctx: Request, - delegate: Delegate): Router.Result[Response.Raw] = { + delegate: Delegate): Result[Response.Raw] = { try { val formData = FormParserFactory.builder().build().createParser(ctx.exchange).parseBlocking() delegate( @@ -60,7 +61,7 @@ class postForm(val path: String, override val subpath: Boolean = false) .toMap ) } catch{case e: Exception => - Router.Result.Success(cask.model.Response( + Result.Success(cask.model.Response( "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 6d3db82..842eae6 100644 --- a/cask/src/cask/endpoints/JsonEndpoint.scala +++ b/cask/src/cask/endpoints/JsonEndpoint.scala @@ -2,14 +2,15 @@ package cask.endpoints import java.io.{ByteArrayOutputStream, InputStream, OutputStream, OutputStreamWriter} -import cask.internal.{Router, Util} -import cask.main.HttpEndpoint +import cask.internal.Util +import cask.router.HttpEndpoint import cask.model.Response.DataCompanion import cask.model.{Request, Response} +import cask.router.{ArgReader, Result} import collection.JavaConverters._ -sealed trait JsReader[T] extends Router.ArgReader[ujson.Value, T, cask.model.Request] +sealed trait JsReader[T] extends ArgReader[ujson.Value, T, cask.model.Request] object JsReader{ implicit def defaultJsReader[T: upickle.default.Reader] = new JsReader[T]{ def arity = 1 @@ -43,9 +44,9 @@ class postJson(val path: String, override val subpath: Boolean = false) extends HttpEndpoint[Response[JsonData], ujson.Value]{ val methods = Seq("post") type InputParser[T] = JsReader[T] - override type OuterReturned = Router.Result[Response.Raw] + override type OuterReturned = Result[Response.Raw] def wrapFunction(ctx: Request, - delegate: Delegate): Router.Result[Response.Raw] = { + delegate: Delegate): Result[Response.Raw] = { val obj = for{ str <- try { @@ -71,7 +72,7 @@ class postJson(val path: String, override val subpath: Boolean = false) ))} } yield obj.toMap obj match{ - case Left(r) => Router.Result.Success(r.map(Response.Data.StringData)) + case Left(r) => Result.Success(r.map(Response.Data.StringData)) case Right(params) => delegate(params) } } @@ -82,8 +83,8 @@ class getJson(val path: String, override val subpath: Boolean = false) extends HttpEndpoint[Response[JsonData], Seq[String]]{ val methods = Seq("get") type InputParser[T] = QueryParamReader[T] - override type OuterReturned = Router.Result[Response.Raw] - def wrapFunction(ctx: Request, delegate: Delegate): Router.Result[Response.Raw] = { + override type OuterReturned = Result[Response.Raw] + def wrapFunction(ctx: Request, delegate: Delegate): Result[Response.Raw] = { delegate(WebEndpoint.buildMapFromQueryParams(ctx)) } diff --git a/cask/src/cask/endpoints/ParamReader.scala b/cask/src/cask/endpoints/ParamReader.scala index e43f482..4ac34f0 100644 --- a/cask/src/cask/endpoints/ParamReader.scala +++ b/cask/src/cask/endpoints/ParamReader.scala @@ -1,11 +1,11 @@ package cask.endpoints -import cask.internal.Router +import cask.router.ArgReader import cask.model.{Cookie, Request} import io.undertow.server.HttpServerExchange import io.undertow.server.handlers.form.{FormData, FormParserFactory} -abstract class ParamReader[T] extends Router.ArgReader[Unit, T, cask.model.Request]{ +abstract class ParamReader[T] extends ArgReader[Unit, T, cask.model.Request]{ def arity: Int def read(ctx: cask.model.Request, label: String, v: Unit): T } diff --git a/cask/src/cask/endpoints/StaticEndpoints.scala b/cask/src/cask/endpoints/StaticEndpoints.scala index 0abfcf5..1e11055 100644 --- a/cask/src/cask/endpoints/StaticEndpoints.scala +++ b/cask/src/cask/endpoints/StaticEndpoints.scala @@ -1,6 +1,6 @@ package cask.endpoints -import cask.main.HttpEndpoint +import cask.router.HttpEndpoint import cask.model.Request class staticFiles(val path: String) extends HttpEndpoint[String, Seq[String]]{ diff --git a/cask/src/cask/endpoints/WebEndpoints.scala b/cask/src/cask/endpoints/WebEndpoints.scala index 8bb3bae..89ca421 100644 --- a/cask/src/cask/endpoints/WebEndpoints.scala +++ b/cask/src/cask/endpoints/WebEndpoints.scala @@ -1,8 +1,8 @@ package cask.endpoints -import cask.internal.Router -import cask.main.HttpEndpoint +import cask.router.HttpEndpoint import cask.model.{Request, Response} +import cask.router.{ArgReader, Result} import collection.JavaConverters._ @@ -10,7 +10,7 @@ import collection.JavaConverters._ trait WebEndpoint extends HttpEndpoint[Response.Raw, Seq[String]]{ type InputParser[T] = QueryParamReader[T] def wrapFunction(ctx: Request, - delegate: Delegate): Router.Result[Response.Raw] = { + delegate: Delegate): Result[Response.Raw] = { delegate(WebEndpoint.buildMapFromQueryParams(ctx)) } def wrapPathSegment(s: String) = Seq(s) @@ -40,7 +40,7 @@ class put(val path: String, override val subpath: Boolean = false) extends WebEn class route(val path: String, val methods: Seq[String], override val subpath: Boolean = false) extends WebEndpoint abstract class QueryParamReader[T] - extends Router.ArgReader[Seq[String], T, cask.model.Request]{ + extends ArgReader[Seq[String], T, cask.model.Request]{ def arity: Int def read(ctx: cask.model.Request, label: String, v: Seq[String]): T } diff --git a/cask/src/cask/endpoints/WebSocketEndpoint.scala b/cask/src/cask/endpoints/WebSocketEndpoint.scala index fae7fde..6ca5def 100644 --- a/cask/src/cask/endpoints/WebSocketEndpoint.scala +++ b/cask/src/cask/endpoints/WebSocketEndpoint.scala @@ -2,8 +2,8 @@ package cask.endpoints import java.nio.ByteBuffer -import cask.internal.Router import cask.model.Request +import cask.router.Result import cask.util.Logger import io.undertow.websockets.WebSocketConnectionCallback import io.undertow.websockets.core.{AbstractReceiveListener, BufferedBinaryMessage, BufferedTextMessage, CloseMessage, WebSocketChannel, WebSockets} @@ -21,10 +21,10 @@ object WebsocketResult{ } class websocket(val path: String, override val subpath: Boolean = false) - extends cask.main.Endpoint[WebsocketResult, Seq[String]]{ + extends cask.router.Endpoint[WebsocketResult, Seq[String]]{ val methods = Seq("websocket") type InputParser[T] = QueryParamReader[T] - type OuterReturned = Router.Result[WebsocketResult] + type OuterReturned = Result[WebsocketResult] def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned = { delegate(WebEndpoint.buildMapFromQueryParams(ctx)) } diff --git a/cask/src/cask/internal/Router.scala b/cask/src/cask/internal/Router.scala deleted file mode 100644 index 4b11811..0000000 --- a/cask/src/cask/internal/Router.scala +++ /dev/null @@ -1,351 +0,0 @@ -package cask.internal - -import language.experimental.macros -import scala.annotation.StaticAnnotation -import scala.collection.mutable -import scala.reflect.macros.blackbox.Context - -/** - * More or less a minimal version of Autowire's Server that lets you generate - * a set of "routes" from the methods defined in an object, and call them - * using passing in name/args/kwargs via Java reflection, without having to - * generate/compile code or use Scala reflection. This saves us spinning up - * the Scala compiler and greatly reduces the startup time of cached scripts. - */ -object Router{ - class doc(s: String) extends StaticAnnotation - - /** - * Models what is known by the router about a single argument: that it has - * a [[name]], a human-readable [[typeString]] describing what the type is - * (just for logging and reading, not a replacement for a `TypeTag`) and - * possible a function that can compute its default value - */ - case class ArgSig[I, -T, +V, -C](name: String, - typeString: String, - doc: Option[String], - default: Option[T => V]) - (implicit val reads: ArgReader[I, V, C]) - - trait ArgReader[I, +T, -C]{ - def arity: Int - def read(ctx: C, label: String, input: I): T - } - - /** - * What is known about a single endpoint for our routes. It has a [[name]], - * [[argSignatures]] for each argument, and a macro-generated [[invoke0]] - * that performs all the necessary argument parsing and de-serialization. - * - * Realistically, you will probably spend most of your time calling [[invoke]] - * instead, which provides a nicer API to call it that mimmicks the API of - * calling a Scala method. - */ - case class EntryPoint[T, C](name: String, - argSignatures: Seq[Seq[ArgSig[_, T, _, C]]], - doc: Option[String], - invoke0: (T, C, Seq[Map[String, Any]], Seq[Seq[ArgSig[Any, _, _, C]]]) => Result[Any]){ - - val firstArgs = argSignatures.head - .map(x => x.name -> x) - .toMap[String, Router.ArgSig[_, T, _, C]] - - def invoke(target: T, - ctx: C, - paramLists: Seq[Map[String, Any]]): Result[Any] = { - - val missing = mutable.Buffer.empty[Router.ArgSig[_, T, _, C]] - - val unknown = paramLists.head.keys.filter(!firstArgs.contains(_)) - - for(k <- firstArgs.keys) { - if (!paramLists.head.contains(k)) { - val as = firstArgs(k) - if (as.reads.arity != 0 && as.default.isEmpty) missing.append(as) - } - } - - if (missing.nonEmpty || unknown.nonEmpty) Result.Error.MismatchedArguments(missing.toSeq, unknown.toSeq) - else { - try invoke0( - target, - ctx, - paramLists, - argSignatures.asInstanceOf[Seq[Seq[ArgSig[Any, _, _, C]]]] - ) - catch{case e: Throwable => Result.Error.Exception(e)} - } - } - } - - def tryEither[T](t: => T, error: Throwable => Result.ParamError) = { - try Right(t) - catch{ case e: Throwable => Left(error(e))} - } - - /** - * Represents what comes out of an attempt to invoke an [[EntryPoint]]. - * Could succeed with a value, but could fail in many different ways. - */ - sealed trait Result[+T]{ - def map[V](f: T => V): Result[V] - } - object Result{ - - /** - * Invoking the [[EntryPoint]] was totally successful, and returned a - * result - */ - case class Success[T](value: T) extends Result[T]{ - def map[V](f: T => V) = Success(f(value)) - } - - /** - * Invoking the [[EntryPoint]] was not successful - */ - sealed trait Error extends Result[Nothing]{ - def map[V](f: Nothing => V) = this - } - - - object Error{ - - - /** - * Invoking the [[EntryPoint]] failed with an exception while executing - * code within it. - */ - case class Exception(t: Throwable) extends Error - - /** - * Invoking the [[EntryPoint]] failed because the arguments provided - * did not line up with the arguments expected - */ - case class MismatchedArguments(missing: Seq[ArgSig[_, _, _, _]], - unknown: Seq[String]) extends Error - /** - * Invoking the [[EntryPoint]] failed because there were problems - * deserializing/parsing individual arguments - */ - case class InvalidArguments(values: Seq[ParamError]) extends Error - } - - sealed trait ParamError - object ParamError{ - /** - * Something went wrong trying to de-serialize the input parameter; - * the thrown exception is stored in [[ex]] - */ - case class Invalid(arg: ArgSig[_, _, _, _], value: String, ex: Throwable) extends ParamError - /** - * Something went wrong trying to evaluate the default value - * for this input parameter - */ - case class DefaultFailed(arg: ArgSig[_, _, _, _], ex: Throwable) extends ParamError - } - } - - def validate(args: Seq[Either[Seq[Result.ParamError], Any]]): Result[Seq[Any]] = { - val lefts = args.collect{case Left(x) => x}.flatten - - if (lefts.nonEmpty) Result.Error.InvalidArguments(lefts) - else { - val rights = args.collect{case Right(x) => x} - Result.Success(rights) - } - } - - def makeReadCall[I, C](dict: Map[String, I], - ctx: C, - default: => Option[Any], - arg: ArgSig[I, _, _, C]) = { - arg.reads.arity match{ - case 0 => - tryEither( - arg.reads.read(ctx, arg.name, null.asInstanceOf[I]), Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) - case 1 => - dict.get(arg.name) match{ - case None => - tryEither(default.get, Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) - - case Some(x) => - tryEither(arg.reads.read(ctx, arg.name, x), Result.ParamError.Invalid(arg, x.toString, _)).left.map(Seq(_)) - } - } - } -} - - -class Router[C <: Context](val c: C) { - import c.universe._ - def getValsOrMeths(curCls: Type): Iterable[MethodSymbol] = { - def isAMemberOfAnyRef(member: Symbol) = { - // AnyRef is an alias symbol, we go to the real "owner" of these methods - val anyRefSym = c.mirror.universe.definitions.ObjectClass - member.owner == anyRefSym - } - val extractableMembers = for { - member <- curCls.members.toList.reverse - if !isAMemberOfAnyRef(member) - if !member.isSynthetic - if member.isPublic - if member.isTerm - memTerm = member.asTerm - if memTerm.isMethod - if !memTerm.isModule - } yield memTerm.asMethod - - extractableMembers flatMap { case memTerm => - if (memTerm.isSetter || memTerm.isConstructor || memTerm.isGetter) Nil - else Seq(memTerm) - - } - } - - - - def unwrapVarargType(arg: Symbol) = { - val vararg = arg.typeSignature.typeSymbol == definitions.RepeatedParamClass - val unwrappedType = - if (!vararg) arg.typeSignature - else arg.typeSignature.asInstanceOf[TypeRef].args(0) - - (vararg, unwrappedType) - } - - def extractMethod(method: MethodSymbol, - curCls: c.universe.Type, - convertToResultType: c.Tree, - ctx: c.Tree, - argReaders: Seq[c.Tree], - annotDeserializeTypes: Seq[c.Tree]): c.universe.Tree = { - val baseArgSym = TermName(c.freshName()) - - def getDocAnnotation(annotations: List[Annotation]) = { - val (docTrees, remaining) = annotations.partition(_.tpe =:= typeOf[Router.doc]) - val docValues = for { - doc <- docTrees - if doc.scalaArgs.head.isInstanceOf[Literal] - l = doc.scalaArgs.head.asInstanceOf[Literal] - if l.value.value.isInstanceOf[String] - } yield l.value.value.asInstanceOf[String] - (remaining, docValues.headOption) - } - val (_, methodDoc) = getDocAnnotation(method.annotations) - val argValuesSymbol = q"${c.fresh[TermName]("argValues")}" - val argSigsSymbol = q"${c.fresh[TermName]("argSigs")}" - val ctxSymbol = q"${c.fresh[TermName]("ctx")}" - val argData = for(argListIndex <- method.paramLists.indices) yield{ - val annotDeserializeType = annotDeserializeTypes.lift(argListIndex).getOrElse(tq"scala.Any") - val argReader = argReaders.lift(argListIndex).getOrElse(q"cask.main.NoOpParser.instanceAny") - val flattenedArgLists = method.paramss(argListIndex) - def hasDefault(i: Int) = { - val defaultName = s"${method.name}$$default$$${i + 1}" - if (curCls.members.exists(_.name.toString == defaultName)) Some(defaultName) - else None - } - - val defaults = for (i <- flattenedArgLists.indices) yield { - val arg = TermName(c.freshName()) - hasDefault(i).map(defaultName => q"($arg: $curCls) => $arg.${newTermName(defaultName)}") - } - - val readArgSigs = for ( - ((arg, defaultOpt), i) <- flattenedArgLists.zip(defaults).zipWithIndex - ) yield { - - if (arg.typeSignature.typeSymbol == definitions.RepeatedParamClass) c.abort(method.pos, "Varargs are not supported in cask routes") - - val default = defaultOpt match { - case Some(defaultExpr) => q"scala.Some($defaultExpr($baseArgSym))" - case None => q"scala.None" - } - - val (docUnwrappedType, docOpt) = arg.typeSignature match { - case t: AnnotatedType => - import compat._ - val (remaining, docValue) = getDocAnnotation(t.annotations) - if (remaining.isEmpty) (t.underlying, docValue) - else (c.universe.AnnotatedType(remaining, t.underlying), docValue) - - case t => (t, None) - } - - val docTree = docOpt match { - case None => q"scala.None" - case Some(s) => q"scala.Some($s)" - } - - val argSig = - q""" - cask.internal.Router.ArgSig[$annotDeserializeType, $curCls, $docUnwrappedType, $ctx]( - ${arg.name.toString}, - ${docUnwrappedType.toString}, - $docTree, - $defaultOpt - )($argReader[$docUnwrappedType]) - """ - - val reader = q""" - cask.internal.Router.makeReadCall( - $argValuesSymbol($argListIndex), - $ctxSymbol, - $default, - $argSigsSymbol($argListIndex)($i) - ) - """ - - c.internal.setPos(reader, method.pos) - (reader, argSig) - } - - val (readArgs, argSigs) = readArgSigs.unzip - val (argNames, argNameCasts) = flattenedArgLists.map { arg => - val (vararg, unwrappedType) = unwrapVarargType(arg) - ( - pq"${arg.name.toTermName}", - if (!vararg) q"${arg.name.toTermName}.asInstanceOf[$unwrappedType]" - else q"${arg.name.toTermName}.asInstanceOf[Seq[$unwrappedType]]: _*" - - ) - }.unzip - - (argNameCasts, argSigs, argNames, readArgs) - } - - val argNameCasts = argData.map(_._1) - val argSigs = argData.map(_._2) - val argNames = argData.map(_._3) - val readArgs = argData.map(_._4) - var methodCall: c.Tree = q"$baseArgSym.${method.name.toTermName}" - for(argNameCast <- argNameCasts) methodCall = q"$methodCall(..$argNameCast)" - - val res = q""" - cask.internal.Router.EntryPoint[$curCls, $ctx]( - ${method.name.toString}, - ${argSigs.toList}, - ${methodDoc match{ - case None => q"scala.None" - case Some(s) => q"scala.Some($s)" - }}, - ( - $baseArgSym: $curCls, - $ctxSymbol: $ctx, - $argValuesSymbol: Seq[Map[String, Any]], - $argSigsSymbol: scala.Seq[scala.Seq[cask.internal.Router.ArgSig[Any, _, _, $ctx]]] - ) => - cask.internal.Router.validate(Seq(..${readArgs.flatten.toList})).map{ - case Seq(..${argNames.flatten.toList}) => $convertToResultType($methodCall) - } - ) - """ - - c.internal.transform(res){(t, a) => - c.internal.setPos(t, method.pos) - a.default(t) - } - - res - } - -} diff --git a/cask/src/cask/main/Decorators.scala b/cask/src/cask/main/Decorators.scala deleted file mode 100644 index fb795ba..0000000 --- a/cask/src/cask/main/Decorators.scala +++ /dev/null @@ -1,136 +0,0 @@ -package cask.main - -import cask.internal.{Conversion, Router} -import cask.internal.Router.{ArgReader, EntryPoint} -import cask.model.{Request, Response} - -/** - * A [[Decorator]] allows you to annotate a function to wrap it, via - * `wrapFunction`. You can use this to perform additional validation before or - * after the function runs, provide an additional parameter list of params, - * open/commit/rollback database transactions before/after the function runs, - * or even retrying the wrapped function if it fails. - * - * Calls to the wrapped function are done on the `delegate` parameter passed - * to `wrapFunction`, which takes a `Map` representing any additional argument - * lists (if any). - */ -trait Decorator[InnerReturned, Input]{ - final type InputTypeAlias = Input - type InputParser[T] <: ArgReader[Input, T, Request] - final 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 -} -object Decorator{ - /** - * A stack of [[Decorator]]s is invoked recursively: each decorator's `wrapFunction` - * is invoked around the invocation of all inner decorators, with the inner-most - * decorator finally invoking the route's [[EntryPoint.invoke]] function. - * - * Each decorator (and the final `Endpoint`) contributes a dictionary of name-value - * bindings, which are eventually all passed to [[EntryPoint.invoke]]. Each decorator's - * dictionary corresponds to a different argument list on [[EntryPoint.invoke]]. The - * bindings passed from the router are aggregated with those from the `EndPoint` and - * used as the first argument list. - */ - def invoke[T](ctx: Request, - endpoint: Endpoint[_, _], - entryPoint: EntryPoint[T, _], - routes: T, - routeBindings: Map[String, String], - remainingDecorators: List[RawDecorator], - bindings: List[Map[String, Any]]): Router.Result[Any] = try { - remainingDecorators match { - case head :: rest => - head.wrapFunction( - ctx, - args => invoke(ctx, endpoint, entryPoint, routes, routeBindings, rest, args :: bindings) - .asInstanceOf[Router.Result[cask.model.Response.Raw]] - ) - - case Nil => - endpoint.wrapFunction(ctx, { (endpointBindings: Map[String, Any]) => - val mergedEndpointBindings = endpointBindings ++ routeBindings.mapValues(endpoint.wrapPathSegment) - val finalBindings = mergedEndpointBindings :: bindings - - entryPoint - .asInstanceOf[EntryPoint[T, cask.model.Request]] - .invoke(routes, ctx, finalBindings) - .asInstanceOf[Router.Result[Nothing]] - }) - } - // Make sure we wrap any exceptions that bubble up from decorator - // bodies, so outer decorators do not need to worry about their - // delegate throwing on them - }catch{case e: Throwable => Router.Result.Error.Exception(e) } -} - -/** - * A [[RawDecorator]] is a decorator that operates on the raw request and - * response stream, before and after the primary [[Endpoint]] does it's job. - */ -trait RawDecorator extends Decorator[Response.Raw, Any]{ - type OuterReturned = Router.Result[Response.Raw] - type InputParser[T] = NoOpParser[Any, T] -} - - -/** - * An [[HttpEndpoint]] that may return something else than a HTTP response, e.g. - * a websocket endpoint which may instead return a websocket event handler - */ -trait Endpoint[InnerReturned, Input] extends Decorator[InnerReturned, Input]{ - /** - * What is the path that this particular endpoint matches? - */ - val path: String - /** - * Which HTTP methods does this endpoint support? POST? GET? PUT? Or some - * combination of those? - */ - val methods: Seq[String] - - /** - * Whether or not this endpoint allows matching on sub-paths: does - * `@endpoint("/foo")` capture the path "/foo/bar/baz"? Useful to e.g. have - * an endpoint match URLs with paths in a filesystem (real or virtual) to - * serve files - */ - def subpath: Boolean = false - - def convertToResultType[T](t: T) - (implicit f: Conversion[T, InnerReturned]): InnerReturned = { - f.f(t) - } - - /** - * [[HttpEndpoint]]s are unique among decorators in that they alone can bind - * path segments to parameters, e.g. binding `/hello/:world` to `(world: Int)`. - * In order to do so, we need to box up the path segment strings into an - * [[Input]] so they can later be parsed by [[getParamParser]] into an - * instance of the appropriate type. - */ - def wrapPathSegment(s: String): Input - -} - -/** - * Annotates a Cask endpoint that returns a HTTP [[Response]]; similar to a - * [[RawDecorator]] but with additional metadata and capabilities. - */ -trait HttpEndpoint[InnerReturned, Input] extends Endpoint[InnerReturned, Input] { - type OuterReturned = Router.Result[Response.Raw] -} - - -class NoOpParser[Input, T] extends ArgReader[Input, T, Request] { - def arity = 1 - - def read(ctx: Request, label: String, input: Input) = input.asInstanceOf[T] -} -object NoOpParser{ - implicit def instance[Input, T] = new NoOpParser[Input, T] - implicit def instanceAny[T] = new NoOpParser[Any, T] -} \ No newline at end of file diff --git a/cask/src/cask/main/ErrorMsgs.scala b/cask/src/cask/main/ErrorMsgs.scala index 254f4e0..a22bd89 100644 --- a/cask/src/cask/main/ErrorMsgs.scala +++ b/cask/src/cask/main/ErrorMsgs.scala @@ -1,11 +1,12 @@ package cask.main -import cask.internal.{Router, Util} +import cask.internal.Util import cask.internal.Util.literalize +import cask.router.{ArgSig, EntryPoint, Result} object ErrorMsgs { - def getLeftColWidth(items: Seq[Router.ArgSig[_, _, _,_]]) = { + def getLeftColWidth(items: Seq[ArgSig[_, _, _,_]]) = { items.map(_.name.length + 2) match{ case Nil => 0 case x => x.max @@ -13,7 +14,7 @@ object ErrorMsgs { } def renderArg[T](base: T, - arg: Router.ArgSig[_, T, _, _], + arg: ArgSig[_, T, _, _], leftOffset: Int, wrappedWidth: Int): (String, String) = { val suffix = arg.default match{ @@ -33,7 +34,7 @@ object ErrorMsgs { } def formatMainMethodSignature[T](base: T, - main: Router.EntryPoint[T, _], + main: EntryPoint[T, _], leftIndent: Int, leftColWidth: Int) = { // +2 for space on right of left col @@ -56,12 +57,12 @@ object ErrorMsgs { |${argStrings.map(_ + "\n").mkString}""".stripMargin } - def formatInvokeError[T](base: T, route: Router.EntryPoint[T, _], x: Router.Result.Error): String = { + def formatInvokeError[T](base: T, route: EntryPoint[T, _], x: Result.Error): String = { def expectedMsg = formatMainMethodSignature(base: T, route, 0, 0) x match{ - case Router.Result.Error.Exception(x) => Util.stackTraceString(x) - case Router.Result.Error.MismatchedArguments(missing, unknown) => + case Result.Error.Exception(x) => Util.stackTraceString(x) + case Result.Error.MismatchedArguments(missing, unknown) => val missingStr = if (missing.isEmpty) "" else { @@ -88,14 +89,14 @@ object ErrorMsgs { |$expectedMsg |""".stripMargin - case Router.Result.Error.InvalidArguments(x) => + case Result.Error.InvalidArguments(x) => val argumentsStr = Util.pluralize("argument", x.length) val thingies = x.map{ - case Router.Result.ParamError.Invalid(p, v, ex) => + case Result.ParamError.Invalid(p, v, ex) => val literalV = literalize(v) val trace = Util.stackTraceString(ex) s"${p.name}: ${p.typeString} = $literalV failed to parse with $ex\n$trace" - case Router.Result.ParamError.DefaultFailed(p, ex) => + case Result.ParamError.DefaultFailed(p, ex) => val trace = Util.stackTraceString(ex) s"${p.name}'s default value failed to evaluate with $ex\n$trace" } diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala index fddd9b7..6d08c04 100644 --- a/cask/src/cask/main/Main.scala +++ b/cask/src/cask/main/Main.scala @@ -2,9 +2,9 @@ package cask.main import cask.endpoints.{WebsocketResult, WsHandler} import cask.model._ -import cask.internal.Router.EntryPoint -import cask.internal.{DispatchTrie, Router, Util} +import cask.internal.{DispatchTrie, Util} import cask.main +import cask.router.{Decorator, EndpointMetadata, EntryPoint, RawDecorator, Result} import cask.util.Logger import io.undertow.Undertow import io.undertow.server.{HttpHandler, HttpServerExchange} @@ -27,7 +27,7 @@ class MainRoutes extends Main with Routes{ * application-wide properties. */ abstract class Main{ - def mainDecorators: Seq[cask.main.RawDecorator] = Nil + def mainDecorators: Seq[RawDecorator] = Nil def allRoutes: Seq[Routes] def port: Int = 8080 def host: String = "localhost" @@ -45,7 +45,7 @@ abstract class Main{ def handleEndpointError(routes: Routes, metadata: EndpointMetadata[_], - e: Router.Result.Error) = { + e: cask.router.Result.Error) = { Main.defaultHandleError(routes, metadata, e, debugMode) } @@ -64,7 +64,7 @@ object Main{ mainDecorators: Seq[RawDecorator], debugMode: Boolean, handleNotFound: () => Response.Raw, - handleError: (Routes, EndpointMetadata[_], Router.Result.Error) => Response.Raw) + handleError: (Routes, EndpointMetadata[_], Result.Error) => Response.Raw) (implicit log: Logger) extends HttpHandler() { def handleRequest(exchange: HttpServerExchange): Unit = try { // println("Handling Request: " + exchange.getRequestPath) @@ -98,8 +98,8 @@ object Main{ (mainDecorators ++ routes.decorators ++ metadata.decorators).toList, Nil ) match{ - case Router.Result.Success(res) => runner(res) - case e: Router.Result.Error => + case Result.Success(res) => runner(res) + case e: Result.Error => Main.writeResponse( exchange, handleError(routes, metadata, e) @@ -145,17 +145,17 @@ object Main{ def defaultHandleError(routes: Routes, metadata: EndpointMetadata[_], - e: Router.Result.Error, + e: Result.Error, debugMode: Boolean) (implicit log: Logger) = { e match { - case e: Router.Result.Error.Exception => log.exception(e.t) + case e: Result.Error.Exception => log.exception(e.t) case _ => // do nothing } val statusCode = e match { - case _: Router.Result.Error.Exception => 500 - case _: Router.Result.Error.InvalidArguments => 400 - case _: Router.Result.Error.MismatchedArguments => 400 + case _: Result.Error.Exception => 500 + case _: Result.Error.InvalidArguments => 400 + case _: Result.Error.MismatchedArguments => 400 } val str = diff --git a/cask/src/cask/main/Routes.scala b/cask/src/cask/main/Routes.scala index 9be9f50..512860a 100644 --- a/cask/src/cask/main/Routes.scala +++ b/cask/src/cask/main/Routes.scala @@ -1,10 +1,12 @@ package cask.main +import cask.router.RoutesEndpointsMetadata + import language.experimental.macros trait Routes{ - def decorators = Seq.empty[cask.main.RawDecorator] + def decorators = Seq.empty[cask.router.RawDecorator] private[this] var metadata0: RoutesEndpointsMetadata[this.type] = null def caskMetadata = if (metadata0 != null) metadata0 diff --git a/cask/src/cask/main/RoutesEndpointMetadata.scala b/cask/src/cask/main/RoutesEndpointMetadata.scala deleted file mode 100644 index fa93a0c..0000000 --- a/cask/src/cask/main/RoutesEndpointMetadata.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cask.main - -import cask.internal.Router.EntryPoint - -import language.experimental.macros -import scala.reflect.macros.blackbox -case class EndpointMetadata[T](decorators: Seq[RawDecorator], - endpoint: Endpoint[_, _], - entryPoint: EntryPoint[T, _]) -case class RoutesEndpointsMetadata[T](value: EndpointMetadata[T]*) -object RoutesEndpointsMetadata{ - implicit def initialize[T]: RoutesEndpointsMetadata[T] = macro initializeImpl[T] - implicit def initializeImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[RoutesEndpointsMetadata[T]] = { - import c.universe._ - val router = new cask.internal.Router[c.type](c) - - val routeParts = for{ - m <- c.weakTypeOf[T].members - annotations = m.annotations.filter(_.tree.tpe <:< c.weakTypeOf[Decorator[_, _]]) - if annotations.nonEmpty - } yield { - if(!(annotations.last.tree.tpe <:< weakTypeOf[Endpoint[_, _]])) c.abort( - annotations.head.tree.pos, - s"Last annotation applied to a function must be an instance of Endpoint, " + - s"not ${annotations.last.tree.tpe}" - ) - val allEndpoints = annotations.filter(_.tree.tpe <:< weakTypeOf[Endpoint[_, _]]) - if(allEndpoints.length > 1) c.abort( - annotations.last.tree.pos, - s"You can only apply one Endpoint annotation to a function, not " + - s"${allEndpoints.length} in ${allEndpoints.map(_.tree.tpe).mkString(", ")}" - ) - - val annotObjects = - for(annot <- annotations) - yield q"new ${annot.tree.tpe}(..${annot.tree.children.tail})" - - val annotObjectSyms = - for(_ <- annotations.indices) - yield c.universe.TermName(c.freshName("annotObject")) - - val route = router.extractMethod( - m.asInstanceOf[MethodSymbol], - weakTypeOf[T], - q"${annotObjectSyms.last}.convertToResultType", - tq"cask.Request", - annotObjectSyms.reverse.map(annotObjectSym => q"$annotObjectSym.getParamParser"), - annotObjectSyms.reverse.map(annotObjectSym => tq"$annotObjectSym.InputTypeAlias") - ) - - val declarations = - for((sym, obj) <- annotObjectSyms.zip(annotObjects)) - yield q"val $sym = $obj" - - val res = q"""{ - ..$declarations - cask.main.EndpointMetadata( - Seq(..${annotObjectSyms.dropRight(1)}), - ${annotObjectSyms.last}, - $route - ) - }""" - res - } - - c.Expr[RoutesEndpointsMetadata[T]](q"""cask.main.RoutesEndpointsMetadata(..$routeParts)""") - } -} \ No newline at end of file diff --git a/cask/src/cask/package.scala b/cask/src/cask/package.scala index d9e29ba..d34fe26 100644 --- a/cask/src/cask/package.scala +++ b/cask/src/cask/package.scala @@ -39,8 +39,8 @@ package object cask { type Routes = main.Routes type Main = main.Main - type RawDecorator = main.RawDecorator - type HttpEndpoint[InnerReturned, Input] = main.HttpEndpoint[InnerReturned, Input] + type RawDecorator = router.RawDecorator + type HttpEndpoint[InnerReturned, Input] = router.HttpEndpoint[InnerReturned, Input] type WsHandler = cask.endpoints.WsHandler val WsHandler = cask.endpoints.WsHandler diff --git a/cask/src/cask/router/Decorators.scala b/cask/src/cask/router/Decorators.scala new file mode 100644 index 0000000..cd2c1a3 --- /dev/null +++ b/cask/src/cask/router/Decorators.scala @@ -0,0 +1,136 @@ +package cask.router + +import cask.internal.Conversion +import cask.model.{Request, Response} +import cask.router.{ArgReader, EntryPoint, Result} + +/** + * A [[Decorator]] allows you to annotate a function to wrap it, via + * `wrapFunction`. You can use this to perform additional validation before or + * after the function runs, provide an additional parameter list of params, + * open/commit/rollback database transactions before/after the function runs, + * or even retrying the wrapped function if it fails. + * + * Calls to the wrapped function are done on the `delegate` parameter passed + * to `wrapFunction`, which takes a `Map` representing any additional argument + * lists (if any). + */ +trait Decorator[InnerReturned, Input]{ + final type InputTypeAlias = Input + type InputParser[T] <: ArgReader[Input, T, Request] + final type Delegate = Map[String, Input] => Result[InnerReturned] + type OuterReturned <: Result[Any] + def wrapFunction(ctx: Request, delegate: Delegate): OuterReturned + def getParamParser[T](implicit p: InputParser[T]) = p +} +object Decorator{ + /** + * A stack of [[Decorator]]s is invoked recursively: each decorator's `wrapFunction` + * is invoked around the invocation of all inner decorators, with the inner-most + * decorator finally invoking the route's [[EntryPoint.invoke]] function. + * + * Each decorator (and the final `Endpoint`) contributes a dictionary of name-value + * bindings, which are eventually all passed to [[EntryPoint.invoke]]. Each decorator's + * dictionary corresponds to a different argument list on [[EntryPoint.invoke]]. The + * bindings passed from the router are aggregated with those from the `EndPoint` and + * used as the first argument list. + */ + def invoke[T](ctx: Request, + endpoint: Endpoint[_, _], + entryPoint: EntryPoint[T, _], + routes: T, + routeBindings: Map[String, String], + remainingDecorators: List[RawDecorator], + bindings: List[Map[String, Any]]): Result[Any] = try { + remainingDecorators match { + case head :: rest => + head.wrapFunction( + ctx, + args => invoke(ctx, endpoint, entryPoint, routes, routeBindings, rest, args :: bindings) + .asInstanceOf[Result[cask.model.Response.Raw]] + ) + + case Nil => + endpoint.wrapFunction(ctx, { (endpointBindings: Map[String, Any]) => + val mergedEndpointBindings = endpointBindings ++ routeBindings.mapValues(endpoint.wrapPathSegment) + val finalBindings = mergedEndpointBindings :: bindings + + entryPoint + .asInstanceOf[EntryPoint[T, cask.model.Request]] + .invoke(routes, ctx, finalBindings) + .asInstanceOf[Result[Nothing]] + }) + } + // Make sure we wrap any exceptions that bubble up from decorator + // bodies, so outer decorators do not need to worry about their + // delegate throwing on them + }catch{case e: Throwable => Result.Error.Exception(e) } +} + +/** + * A [[RawDecorator]] is a decorator that operates on the raw request and + * response stream, before and after the primary [[Endpoint]] does it's job. + */ +trait RawDecorator extends Decorator[Response.Raw, Any]{ + type OuterReturned = Result[Response.Raw] + type InputParser[T] = NoOpParser[Any, T] +} + + +/** + * An [[HttpEndpoint]] that may return something else than a HTTP response, e.g. + * a websocket endpoint which may instead return a websocket event handler + */ +trait Endpoint[InnerReturned, Input] extends Decorator[InnerReturned, Input]{ + /** + * What is the path that this particular endpoint matches? + */ + val path: String + /** + * Which HTTP methods does this endpoint support? POST? GET? PUT? Or some + * combination of those? + */ + val methods: Seq[String] + + /** + * Whether or not this endpoint allows matching on sub-paths: does + * `@endpoint("/foo")` capture the path "/foo/bar/baz"? Useful to e.g. have + * an endpoint match URLs with paths in a filesystem (real or virtual) to + * serve files + */ + def subpath: Boolean = false + + def convertToResultType[T](t: T) + (implicit f: Conversion[T, InnerReturned]): InnerReturned = { + f.f(t) + } + + /** + * [[HttpEndpoint]]s are unique among decorators in that they alone can bind + * path segments to parameters, e.g. binding `/hello/:world` to `(world: Int)`. + * In order to do so, we need to box up the path segment strings into an + * [[Input]] so they can later be parsed by [[getParamParser]] into an + * instance of the appropriate type. + */ + def wrapPathSegment(s: String): Input + +} + +/** + * Annotates a Cask endpoint that returns a HTTP [[Response]]; similar to a + * [[RawDecorator]] but with additional metadata and capabilities. + */ +trait HttpEndpoint[InnerReturned, Input] extends Endpoint[InnerReturned, Input] { + type OuterReturned = Result[Response.Raw] +} + + +class NoOpParser[Input, T] extends ArgReader[Input, T, Request] { + def arity = 1 + + def read(ctx: Request, label: String, input: Input) = input.asInstanceOf[T] +} +object NoOpParser{ + implicit def instance[Input, T] = new NoOpParser[Input, T] + implicit def instanceAny[T] = new NoOpParser[Any, T] +} \ No newline at end of file diff --git a/cask/src/cask/router/EntryPoint.scala b/cask/src/cask/router/EntryPoint.scala new file mode 100644 index 0000000..6fe44fc --- /dev/null +++ b/cask/src/cask/router/EntryPoint.scala @@ -0,0 +1,51 @@ +package cask.router + + +import scala.collection.mutable + + +/** + * What is known about a single endpoint for our routes. It has a [[name]], + * [[argSignatures]] for each argument, and a macro-generated [[invoke0]] + * that performs all the necessary argument parsing and de-serialization. + * + * Realistically, you will probably spend most of your time calling [[invoke]] + * instead, which provides a nicer API to call it that mimmicks the API of + * calling a Scala method. + */ +case class EntryPoint[T, C](name: String, + argSignatures: Seq[Seq[ArgSig[_, T, _, C]]], + doc: Option[String], + invoke0: (T, C, Seq[Map[String, Any]], Seq[Seq[ArgSig[Any, _, _, C]]]) => Result[Any]){ + + val firstArgs = argSignatures.head + .map(x => x.name -> x) + .toMap[String, ArgSig[_, T, _, C]] + + def invoke(target: T, + ctx: C, + paramLists: Seq[Map[String, Any]]): Result[Any] = { + + val missing = mutable.Buffer.empty[ArgSig[_, T, _, C]] + + val unknown = paramLists.head.keys.filter(!firstArgs.contains(_)) + + for(k <- firstArgs.keys) { + if (!paramLists.head.contains(k)) { + val as = firstArgs(k) + if (as.reads.arity != 0 && as.default.isEmpty) missing.append(as) + } + } + + if (missing.nonEmpty || unknown.nonEmpty) Result.Error.MismatchedArguments(missing.toSeq, unknown.toSeq) + else { + try invoke0( + target, + ctx, + paramLists, + argSignatures.asInstanceOf[Seq[Seq[ArgSig[Any, _, _, C]]]] + ) + catch{case e: Throwable => Result.Error.Exception(e)} + } + } +} diff --git a/cask/src/cask/router/Macros.scala b/cask/src/cask/router/Macros.scala new file mode 100644 index 0000000..de27e5c --- /dev/null +++ b/cask/src/cask/router/Macros.scala @@ -0,0 +1,178 @@ +package cask.router + +import scala.reflect.macros.blackbox + + +class Macros[C <: blackbox.Context](val c: C) { + import c.universe._ + def getValsOrMeths(curCls: Type): Iterable[MethodSymbol] = { + def isAMemberOfAnyRef(member: Symbol) = { + // AnyRef is an alias symbol, we go to the real "owner" of these methods + val anyRefSym = c.mirror.universe.definitions.ObjectClass + member.owner == anyRefSym + } + val extractableMembers = for { + member <- curCls.members.toList.reverse + if !isAMemberOfAnyRef(member) + if !member.isSynthetic + if member.isPublic + if member.isTerm + memTerm = member.asTerm + if memTerm.isMethod + if !memTerm.isModule + } yield memTerm.asMethod + + extractableMembers flatMap { case memTerm => + if (memTerm.isSetter || memTerm.isConstructor || memTerm.isGetter) Nil + else Seq(memTerm) + + } + } + + + + def unwrapVarargType(arg: Symbol) = { + val vararg = arg.typeSignature.typeSymbol == definitions.RepeatedParamClass + val unwrappedType = + if (!vararg) arg.typeSignature + else arg.typeSignature.asInstanceOf[TypeRef].args(0) + + (vararg, unwrappedType) + } + + def extractMethod(method: MethodSymbol, + curCls: c.universe.Type, + convertToResultType: c.Tree, + ctx: c.Tree, + argReaders: Seq[c.Tree], + annotDeserializeTypes: Seq[c.Tree]): c.universe.Tree = { + val baseArgSym = TermName(c.freshName()) + + def getDocAnnotation(annotations: List[Annotation]) = { + val (docTrees, remaining) = annotations.partition(_.tpe =:= typeOf[doc]) + val docValues = for { + doc <- docTrees + if doc.scalaArgs.head.isInstanceOf[Literal] + l = doc.scalaArgs.head.asInstanceOf[Literal] + if l.value.value.isInstanceOf[String] + } yield l.value.value.asInstanceOf[String] + (remaining, docValues.headOption) + } + val (_, methodDoc) = getDocAnnotation(method.annotations) + val argValuesSymbol = q"${c.fresh[TermName]("argValues")}" + val argSigsSymbol = q"${c.fresh[TermName]("argSigs")}" + val ctxSymbol = q"${c.fresh[TermName]("ctx")}" + val argData = for(argListIndex <- method.paramLists.indices) yield{ + val annotDeserializeType = annotDeserializeTypes.lift(argListIndex).getOrElse(tq"scala.Any") + val argReader = argReaders.lift(argListIndex).getOrElse(q"cask.router.NoOpParser.instanceAny") + val flattenedArgLists = method.paramss(argListIndex) + def hasDefault(i: Int) = { + val defaultName = s"${method.name}$$default$$${i + 1}" + if (curCls.members.exists(_.name.toString == defaultName)) Some(defaultName) + else None + } + + val defaults = for (i <- flattenedArgLists.indices) yield { + val arg = TermName(c.freshName()) + hasDefault(i).map(defaultName => q"($arg: $curCls) => $arg.${newTermName(defaultName)}") + } + + val readArgSigs = for ( + ((arg, defaultOpt), i) <- flattenedArgLists.zip(defaults).zipWithIndex + ) yield { + + if (arg.typeSignature.typeSymbol == definitions.RepeatedParamClass) c.abort(method.pos, "Varargs are not supported in cask routes") + + val default = defaultOpt match { + case Some(defaultExpr) => q"scala.Some($defaultExpr($baseArgSym))" + case None => q"scala.None" + } + + val (docUnwrappedType, docOpt) = arg.typeSignature match { + case t: AnnotatedType => + import compat._ + val (remaining, docValue) = getDocAnnotation(t.annotations) + if (remaining.isEmpty) (t.underlying, docValue) + else (c.universe.AnnotatedType(remaining, t.underlying), docValue) + + case t => (t, None) + } + + val docTree = docOpt match { + case None => q"scala.None" + case Some(s) => q"scala.Some($s)" + } + + val argSig = + q""" + cask.router.ArgSig[$annotDeserializeType, $curCls, $docUnwrappedType, $ctx]( + ${arg.name.toString}, + ${docUnwrappedType.toString}, + $docTree, + $defaultOpt + )($argReader[$docUnwrappedType]) + """ + + val reader = q""" + cask.router.Runtime.makeReadCall( + $argValuesSymbol($argListIndex), + $ctxSymbol, + $default, + $argSigsSymbol($argListIndex)($i) + ) + """ + + c.internal.setPos(reader, method.pos) + (reader, argSig) + } + + val (readArgs, argSigs) = readArgSigs.unzip + val (argNames, argNameCasts) = flattenedArgLists.map { arg => + val (vararg, unwrappedType) = unwrapVarargType(arg) + ( + pq"${arg.name.toTermName}", + if (!vararg) q"${arg.name.toTermName}.asInstanceOf[$unwrappedType]" + else q"${arg.name.toTermName}.asInstanceOf[Seq[$unwrappedType]]: _*" + + ) + }.unzip + + (argNameCasts, argSigs, argNames, readArgs) + } + + val argNameCasts = argData.map(_._1) + val argSigs = argData.map(_._2) + val argNames = argData.map(_._3) + val readArgs = argData.map(_._4) + var methodCall: c.Tree = q"$baseArgSym.${method.name.toTermName}" + for(argNameCast <- argNameCasts) methodCall = q"$methodCall(..$argNameCast)" + + val res = q""" + cask.router.EntryPoint[$curCls, $ctx]( + ${method.name.toString}, + ${argSigs.toList}, + ${methodDoc match{ + case None => q"scala.None" + case Some(s) => q"scala.Some($s)" + }}, + ( + $baseArgSym: $curCls, + $ctxSymbol: $ctx, + $argValuesSymbol: Seq[Map[String, Any]], + $argSigsSymbol: scala.Seq[scala.Seq[cask.router.ArgSig[Any, _, _, $ctx]]] + ) => + cask.router.Runtime.validate(Seq(..${readArgs.flatten.toList})).map{ + case Seq(..${argNames.flatten.toList}) => $convertToResultType($methodCall) + } + ) + """ + + c.internal.transform(res){(t, a) => + c.internal.setPos(t, method.pos) + a.default(t) + } + + res + } + +} diff --git a/cask/src/cask/router/Misc.scala b/cask/src/cask/router/Misc.scala new file mode 100644 index 0000000..438ec43 --- /dev/null +++ b/cask/src/cask/router/Misc.scala @@ -0,0 +1,23 @@ +package cask.router + +import scala.annotation.StaticAnnotation + + +class doc(s: String) extends StaticAnnotation + +/** + * Models what is known by the router about a single argument: that it has + * a [[name]], a human-readable [[typeString]] describing what the type is + * (just for logging and reading, not a replacement for a `TypeTag`) and + * possible a function that can compute its default value + */ +case class ArgSig[I, -T, +V, -C](name: String, + typeString: String, + doc: Option[String], + default: Option[T => V]) + (implicit val reads: ArgReader[I, V, C]) + +trait ArgReader[I, +T, -C]{ + def arity: Int + def read(ctx: C, label: String, input: I): T +} diff --git a/cask/src/cask/router/Result.scala b/cask/src/cask/router/Result.scala new file mode 100644 index 0000000..52ef0f8 --- /dev/null +++ b/cask/src/cask/router/Result.scala @@ -0,0 +1,66 @@ +package cask.router + + + + +/** + * Represents what comes out of an attempt to invoke an [[EntryPoint]]. + * Could succeed with a value, but could fail in many different ways. + */ +sealed trait Result[+T]{ + def map[V](f: T => V): Result[V] +} +object Result{ + + /** + * Invoking the [[EntryPoint]] was totally successful, and returned a + * result + */ + case class Success[T](value: T) extends Result[T]{ + def map[V](f: T => V) = Success(f(value)) + } + + /** + * Invoking the [[EntryPoint]] was not successful + */ + sealed trait Error extends Result[Nothing]{ + def map[V](f: Nothing => V) = this + } + + + object Error{ + + + /** + * Invoking the [[EntryPoint]] failed with an exception while executing + * code within it. + */ + case class Exception(t: Throwable) extends Error + + /** + * Invoking the [[EntryPoint]] failed because the arguments provided + * did not line up with the arguments expected + */ + case class MismatchedArguments(missing: Seq[ArgSig[_, _, _, _]], + unknown: Seq[String]) extends Error + /** + * Invoking the [[EntryPoint]] failed because there were problems + * deserializing/parsing individual arguments + */ + case class InvalidArguments(values: Seq[ParamError]) extends Error + } + + sealed trait ParamError + object ParamError{ + /** + * Something went wrong trying to de-serialize the input parameter; + * the thrown exception is stored in [[ex]] + */ + case class Invalid(arg: ArgSig[_, _, _, _], value: String, ex: Throwable) extends ParamError + /** + * Something went wrong trying to evaluate the default value + * for this input parameter + */ + case class DefaultFailed(arg: ArgSig[_, _, _, _], ex: Throwable) extends ParamError + } +} \ No newline at end of file diff --git a/cask/src/cask/router/RoutesEndpointMetadata.scala b/cask/src/cask/router/RoutesEndpointMetadata.scala new file mode 100644 index 0000000..7940641 --- /dev/null +++ b/cask/src/cask/router/RoutesEndpointMetadata.scala @@ -0,0 +1,68 @@ +package cask.router + +import cask.router.EntryPoint + +import language.experimental.macros +import scala.reflect.macros.blackbox +case class EndpointMetadata[T](decorators: Seq[RawDecorator], + endpoint: Endpoint[_, _], + entryPoint: EntryPoint[T, _]) +case class RoutesEndpointsMetadata[T](value: EndpointMetadata[T]*) +object RoutesEndpointsMetadata{ + implicit def initialize[T]: RoutesEndpointsMetadata[T] = macro initializeImpl[T] + implicit def initializeImpl[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[RoutesEndpointsMetadata[T]] = { + import c.universe._ + val router = new cask.router.Macros[c.type](c) + + val routeParts = for{ + m <- c.weakTypeOf[T].members + annotations = m.annotations.filter(_.tree.tpe <:< c.weakTypeOf[Decorator[_, _]]) + if annotations.nonEmpty + } yield { + if(!(annotations.last.tree.tpe <:< weakTypeOf[Endpoint[_, _]])) c.abort( + annotations.head.tree.pos, + s"Last annotation applied to a function must be an instance of Endpoint, " + + s"not ${annotations.last.tree.tpe}" + ) + val allEndpoints = annotations.filter(_.tree.tpe <:< weakTypeOf[Endpoint[_, _]]) + if(allEndpoints.length > 1) c.abort( + annotations.last.tree.pos, + s"You can only apply one Endpoint annotation to a function, not " + + s"${allEndpoints.length} in ${allEndpoints.map(_.tree.tpe).mkString(", ")}" + ) + + val annotObjects = + for(annot <- annotations) + yield q"new ${annot.tree.tpe}(..${annot.tree.children.tail})" + + val annotObjectSyms = + for(_ <- annotations.indices) + yield c.universe.TermName(c.freshName("annotObject")) + + val route = router.extractMethod( + m.asInstanceOf[MethodSymbol], + weakTypeOf[T], + q"${annotObjectSyms.last}.convertToResultType", + tq"cask.Request", + annotObjectSyms.reverse.map(annotObjectSym => q"$annotObjectSym.getParamParser"), + annotObjectSyms.reverse.map(annotObjectSym => tq"$annotObjectSym.InputTypeAlias") + ) + + val declarations = + for((sym, obj) <- annotObjectSyms.zip(annotObjects)) + yield q"val $sym = $obj" + + val res = q"""{ + ..$declarations + cask.router.EndpointMetadata( + Seq(..${annotObjectSyms.dropRight(1)}), + ${annotObjectSyms.last}, + $route + ) + }""" + res + } + + c.Expr[RoutesEndpointsMetadata[T]](q"""cask.router.RoutesEndpointsMetadata(..$routeParts)""") + } +} \ No newline at end of file diff --git a/cask/src/cask/router/Runtime.scala b/cask/src/cask/router/Runtime.scala new file mode 100644 index 0000000..4fbbb48 --- /dev/null +++ b/cask/src/cask/router/Runtime.scala @@ -0,0 +1,39 @@ +package cask.router + +object Runtime{ + + def tryEither[T](t: => T, error: Throwable => Result.ParamError) = { + try Right(t) + catch{ case e: Throwable => Left(error(e))} + } + + + def validate(args: Seq[Either[Seq[Result.ParamError], Any]]): Result[Seq[Any]] = { + val lefts = args.collect{case Left(x) => x}.flatten + + if (lefts.nonEmpty) Result.Error.InvalidArguments(lefts) + else { + val rights = args.collect{case Right(x) => x} + Result.Success(rights) + } + } + + def makeReadCall[I, C](dict: Map[String, I], + ctx: C, + default: => Option[Any], + arg: ArgSig[I, _, _, C]) = { + arg.reads.arity match{ + case 0 => + tryEither( + arg.reads.read(ctx, arg.name, null.asInstanceOf[I]), Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) + case 1 => + dict.get(arg.name) match{ + case None => + tryEither(default.get, Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) + + case Some(x) => + tryEither(arg.reads.read(ctx, arg.name, x), Result.ParamError.Invalid(arg, x.toString, _)).left.map(Seq(_)) + } + } + } +} \ No newline at end of file -- cgit v1.2.3