From 603dfa8946f8c78580568613cd268ad05c6c38f6 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Mon, 23 Jul 2018 21:49:11 +0800 Subject: First pass at specializing `ParamReader` for `JsonReader` and `FormReader` --- build.sc | 3 +- cask/src/cask/Endpoints.scala | 31 +++--- cask/src/cask/FormEndpoint.scala | 58 +++++++++++ cask/src/cask/FormValue.scala | 23 +++++ cask/src/cask/JsonEndpoint.scala | 43 ++++++++ cask/src/cask/Main.scala | 6 +- cask/src/cask/ParamReader.scala | 47 --------- cask/src/cask/QueryParamReader.scala | 63 ++++++++++++ cask/src/cask/Router.scala | 153 ++++++++--------------------- cask/src/cask/Routes.scala | 33 +++++-- cask/src/cask/Status.scala | 1 + cask/test/src/test/cask/FormJsonPost.scala | 19 ++++ 12 files changed, 296 insertions(+), 184 deletions(-) create mode 100644 cask/src/cask/FormEndpoint.scala create mode 100644 cask/src/cask/FormValue.scala create mode 100644 cask/src/cask/JsonEndpoint.scala delete mode 100644 cask/src/cask/ParamReader.scala create mode 100644 cask/src/cask/QueryParamReader.scala create mode 100644 cask/test/src/test/cask/FormJsonPost.scala diff --git a/build.sc b/build.sc index 7c3c1ba..0862ad9 100644 --- a/build.sc +++ b/build.sc @@ -5,7 +5,8 @@ object cask extends ScalaModule{ def ivyDeps = Agg( ivy"org.scala-lang:scala-reflect:$scalaVersion", ivy"io.undertow:undertow-core:2.0.11.Final", - ivy"com.github.scopt::scopt:3.5.0" + ivy"com.github.scopt::scopt:3.5.0", + ivy"com.lihaoyi::upickle:0.6.6" ) object test extends Tests{ def forkArgs = Seq("--illegal-access=deny") diff --git a/cask/src/cask/Endpoints.scala b/cask/src/cask/Endpoints.scala index 8363104..b0bc207 100644 --- a/cask/src/cask/Endpoints.scala +++ b/cask/src/cask/Endpoints.scala @@ -2,35 +2,37 @@ package cask import cask.Router.EntryPoint import io.undertow.server.HttpServerExchange + import collection.JavaConverters._ trait Endpoint[R]{ + type InputType val path: String def subpath: Boolean = false def wrapMethodOutput(t: R): Any - def parseMethodInput[T](implicit p: ParamReader[T]) = p - def handle(exchange: HttpServerExchange, remaining: Seq[String], bindings: Map[String, String], routes: Routes, - entryPoint: EntryPoint[Routes, (HttpServerExchange, Seq[String])]): Router.Result[Response] + entryPoint: EntryPoint[InputType, Routes, (HttpServerExchange, Seq[String])]): Router.Result[BaseResponse] } trait WebEndpoint extends Endpoint[Response]{ + type InputType = Seq[String] def wrapMethodOutput(t: Response) = t + def parseMethodInput[T](implicit p: QueryParamReader[T]) = p def handle(exchange: HttpServerExchange, remaining: Seq[String], bindings: Map[String, String], routes: Routes, - entryPoint: EntryPoint[Routes, (HttpServerExchange, Seq[String])]): Router.Result[Response] = { + entryPoint: EntryPoint[Seq[String], Routes, (HttpServerExchange, Seq[String])]): Router.Result[Response] = { val allBindings = - bindings.toSeq ++ - exchange.getQueryParameters - .asScala - .toSeq - .flatMap{case (k, vs) => vs.asScala.map((k, _))} + bindings.map{case (k, v) => (k, Seq(v))} ++ + exchange.getQueryParameters + .asScala + .toSeq + .map{case (k, vs) => (k, vs.asScala.toSeq)} - entryPoint.invoke(routes, (exchange, remaining), allBindings.map{case (k, v) => (k, Some(v))}) + entryPoint.invoke(routes, (exchange, remaining), allBindings) .asInstanceOf[Router.Result[Response]] } } @@ -39,17 +41,18 @@ class post(val path: String, override val subpath: Boolean = false) extends WebE class put(val path: String, override val subpath: Boolean = false) extends WebEndpoint class route(val path: String, val methods: Seq[String], override val subpath: Boolean = false) extends WebEndpoint class static(val path: String) extends Endpoint[String] { + type InputType = Seq[String] override def subpath = true def wrapOutput(t: String) = t - + def parseMethodInput[T](implicit p: QueryParamReader[T]) = p def wrapMethodOutput(t: String) = t def handle(exchange: HttpServerExchange, remaining: Seq[String], bindings: Map[String, String], routes: Routes, - entryPoint: EntryPoint[Routes, (HttpServerExchange, Seq[String])]): Router.Result[Response] = { - entryPoint.invoke(routes, (exchange, remaining), Nil).asInstanceOf[Router.Result[String]] match{ + entryPoint: EntryPoint[Seq[String], Routes, (HttpServerExchange, Seq[String])]): Router.Result[Response] = { + entryPoint.invoke(routes, (exchange, remaining), Map()).asInstanceOf[Router.Result[String]] match{ case Router.Result.Success(s) => val relPath = java.nio.file.Paths.get( s + "/" + remaining.mkString("/") @@ -65,4 +68,4 @@ class static(val path: String) extends Endpoint[String] { } } -} +} \ No newline at end of file diff --git a/cask/src/cask/FormEndpoint.scala b/cask/src/cask/FormEndpoint.scala new file mode 100644 index 0000000..0d6f2f2 --- /dev/null +++ b/cask/src/cask/FormEndpoint.scala @@ -0,0 +1,58 @@ +package cask + +import cask.Router.EntryPoint +import io.undertow.server.HttpServerExchange +import io.undertow.server.handlers.form.FormParserFactory +import collection.JavaConverters._ + +sealed trait FormReader[T] extends Router.ArgReader[Seq[FormValue], T, (HttpServerExchange, Seq[String])] +object FormReader{ + implicit def paramFormReader[T: QueryParamReader] = new FormReader[T]{ + def arity = implicitly[QueryParamReader[T]].arity + + def read(ctx: (HttpServerExchange, Seq[String]), input: Seq[FormValue]) = { + implicitly[QueryParamReader[T]].read(ctx, input.map(_.value)) + } + } + implicit def formValueReader = new FormReader[FormValue]{ + def arity = 1 + def read(ctx: (HttpServerExchange, Seq[String]), input: Seq[FormValue]) = input.head + } + implicit def formValuesReader = new FormReader[Seq[FormValue]]{ + def arity = 1 + def read(ctx: (HttpServerExchange, Seq[String]), input: Seq[FormValue]) = input + } + implicit def formValueFileReader = new FormReader[FormValue.File]{ + def arity = 1 + def read(ctx: (HttpServerExchange, Seq[String]), input: Seq[FormValue]) = input.head.asFile.get + } + implicit def formValuesFileReader = new FormReader[Seq[FormValue.File]]{ + def arity = 1 + def read(ctx: (HttpServerExchange, Seq[String]), input: Seq[FormValue]) = input.map(_.asFile.get) + } +} +class postForm(val path: String, override val subpath: Boolean = false) extends Endpoint[Response]{ + type InputType = Seq[FormValue] + def wrapMethodOutput(t: Response) = t + def parseMethodInput[T](implicit p: FormReader[T]) = p + def handle(exchange: HttpServerExchange, + remaining: Seq[String], + bindings: Map[String, String], + routes: Routes, + entryPoint: EntryPoint[Seq[FormValue], Routes, (HttpServerExchange, Seq[String])]): Router.Result[Response] = { + + val formData = FormParserFactory.builder().build().createParser(exchange).parseBlocking() + val formDataBindings = + formData + .iterator() + .asScala + .map(k => (k, formData.get(k).asScala.map(FormValue.fromUndertow).toSeq)) + + + val pathBindings = + bindings.map{case (k, v) => (k, Seq(new FormValue.Plain(v, new io.undertow.util.HeaderMap())))} + + entryPoint.invoke(routes, (exchange, remaining), pathBindings ++ formDataBindings) + .asInstanceOf[Router.Result[Response]] + } +} diff --git a/cask/src/cask/FormValue.scala b/cask/src/cask/FormValue.scala new file mode 100644 index 0000000..60a2d39 --- /dev/null +++ b/cask/src/cask/FormValue.scala @@ -0,0 +1,23 @@ +package cask + +object FormValue{ + def fromUndertow(from: io.undertow.server.handlers.form.FormData.FormValue) = { + if (!from.isFile) Plain(from.getValue, from.getHeaders) + else File(from.getValue, from.getFileName, from.getPath, from.getHeaders) + } + case class Plain(value: String, + headers: io.undertow.util.HeaderMap) extends FormValue + + case class File(value: String, + fileName: String, + filePath: java.nio.file.Path, + headers: io.undertow.util.HeaderMap) extends FormValue +} +sealed trait FormValue{ + def value: String + def headers: io.undertow.util.HeaderMap + def asFile: Option[FormValue.File] = this match{ + case p: FormValue.Plain => None + case p: FormValue.File => Some(p) + } +} diff --git a/cask/src/cask/JsonEndpoint.scala b/cask/src/cask/JsonEndpoint.scala new file mode 100644 index 0000000..58fd596 --- /dev/null +++ b/cask/src/cask/JsonEndpoint.scala @@ -0,0 +1,43 @@ +package cask + +import cask.Router.EntryPoint +import io.undertow.server.HttpServerExchange + + +sealed trait JsReader[T] extends Router.ArgReader[ujson.Js.Value, T, (HttpServerExchange, Seq[String])] +object JsReader{ + implicit def defaultJsReader[T: upickle.default.Reader] = new JsReader[T]{ + def arity = 1 + + def read(ctx: (HttpServerExchange, Seq[String]), input: ujson.Js.Value): T = { + implicitly[upickle.default.Reader[T]].apply(input) + } + } + + implicit def paramReader[T: ParamReader] = new JsReader[T] { + override def arity = 0 + + override def read(ctx: (HttpServerExchange, Seq[String]), v: ujson.Js.Value) = { + implicitly[ParamReader[T]].read(ctx, Nil) + } + } +} +class postJson(val path: String, override val subpath: Boolean = false) extends Endpoint[Response]{ + type InputType = ujson.Js.Value + def wrapMethodOutput(t: Response) = t + def parseMethodInput[T](implicit p: JsReader[T]) = p + def handle(exchange: HttpServerExchange, + remaining: Seq[String], + bindings: Map[String, String], + routes: Routes, + entryPoint: EntryPoint[ujson.Js.Value, Routes, (HttpServerExchange, Seq[String])]): Router.Result[Response] = { + + val js = ujson.read(new String(exchange.getInputStream.readAllBytes())).asInstanceOf[ujson.Js.Obj] + + js.obj + val allBindings = bindings.mapValues(ujson.Js.Str(_)) + + entryPoint.invoke(routes, (exchange, remaining), js.obj.toMap ++ allBindings) + .asInstanceOf[Router.Result[Response]] + } +} diff --git a/cask/src/cask/Main.scala b/cask/src/cask/Main.scala index 6ee928e..188c24c 100644 --- a/cask/src/cask/Main.scala +++ b/cask/src/cask/Main.scala @@ -37,7 +37,7 @@ abstract class BaseMain{ ) } - def writeResponse(exchange: HttpServerExchange, response: Response) = { + def writeResponse(exchange: HttpServerExchange, response: BaseResponse) = { response.headers.foreach{case (k, v) => exchange.getResponseHeaders.put(new HttpString(k), v) } @@ -54,11 +54,11 @@ abstract class BaseMain{ case Some(((routes, metadata), bindings, remaining)) => val result = metadata.metadata.handle( exchange, remaining, bindings, routes, - metadata.entryPoint.asInstanceOf[EntryPoint[Routes, (HttpServerExchange, Seq[String])]] + metadata.entryPoint.asInstanceOf[EntryPoint[metadata.metadata.InputType, cask.Routes, (HttpServerExchange, Seq[String])]] ) result match{ - case Router.Result.Success(response: Response) => writeResponse(exchange, response) + case Router.Result.Success(response) => writeResponse(exchange, response) case Router.Result.Error.Exception(e) => println(e) e.printStackTrace() diff --git a/cask/src/cask/ParamReader.scala b/cask/src/cask/ParamReader.scala deleted file mode 100644 index ac3614f..0000000 --- a/cask/src/cask/ParamReader.scala +++ /dev/null @@ -1,47 +0,0 @@ -package cask - -import io.undertow.server.HttpServerExchange -import io.undertow.server.handlers.form.{FormData, FormParserFactory} - -abstract class ParamReader[T] - extends Router.ArgReader[T, (HttpServerExchange, Seq[String])]{ - def arity: Int - def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): T -} -object ParamReader{ - class SimpleParam[T](f: String => T) extends ParamReader[T]{ - def arity = 1 - def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): T = f(v.head) - } - class NilParam[T](f: (HttpServerExchange, Seq[String]) => T) extends ParamReader[T]{ - def arity = 0 - def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): T = f(ctx._1, ctx._2) - } - implicit object StringParam extends SimpleParam[String](x => x) - implicit object BooleanParam extends SimpleParam[Boolean](_.toBoolean) - implicit object ByteParam extends SimpleParam[Byte](_.toByte) - implicit object ShortParam extends SimpleParam[Short](_.toShort) - implicit object IntParam extends SimpleParam[Int](_.toInt) - implicit object LongParam extends SimpleParam[Long](_.toLong) - implicit object DoubleParam extends SimpleParam[Double](_.toDouble) - implicit object FloatParam extends SimpleParam[Float](_.toFloat) - implicit def SeqParam[T: ParamReader] = new ParamReader[Seq[T]]{ - def arity = 1 - def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): Seq[T] = { - v.map(x => implicitly[ParamReader[T]].read(ctx, Seq(x))) - } - } - - implicit object HttpExchangeParam extends NilParam[HttpServerExchange]((server, remaining) => server) - implicit object SubpathParam extends NilParam[Subpath]((server, remaining) => new Subpath(remaining)) - implicit object CookieParam extends NilParam[Cookies]((server, remaining) => { - import collection.JavaConverters._ - new Cookies(server.getRequestCookies.asScala.toMap.map{case (k, v) => (k, Cookie.fromUndertow(v))}) - }) - implicit object FormDataParam extends NilParam[FormData]((server, remaining) => - FormParserFactory.builder().build().createParser(server).parseBlocking() - ) -} - -class Subpath(val value: Seq[String]) -class Cookies(val value: Map[String, Cookie]) \ No newline at end of file diff --git a/cask/src/cask/QueryParamReader.scala b/cask/src/cask/QueryParamReader.scala new file mode 100644 index 0000000..1d34b14 --- /dev/null +++ b/cask/src/cask/QueryParamReader.scala @@ -0,0 +1,63 @@ +package cask + +import io.undertow.server.HttpServerExchange +import io.undertow.server.handlers.form.{FormData, FormParserFactory} + + +abstract class ParamReader[T] + extends Router.ArgReader[Seq[String], T, (HttpServerExchange, Seq[String])]{ + def arity: Int + def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): T +} +object ParamReader{ + class NilParam[T](f: (HttpServerExchange, Seq[String]) => T) extends ParamReader[T]{ + def arity = 0 + def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): T = f(ctx._1, ctx._2) + } + implicit object HttpExchangeParam extends NilParam[HttpServerExchange]((server, remaining) => server) + implicit object SubpathParam extends NilParam[Subpath]((server, remaining) => new Subpath(remaining)) + implicit object CookieParam extends NilParam[Cookies]((server, remaining) => { + import collection.JavaConverters._ + new Cookies(server.getRequestCookies.asScala.toMap.map{case (k, v) => (k, Cookie.fromUndertow(v))}) + }) + implicit object FormDataParam extends NilParam[FormData]((server, remaining) => + FormParserFactory.builder().build().createParser(server).parseBlocking() + ) +} +abstract class QueryParamReader[T] + extends Router.ArgReader[Seq[String], T, (HttpServerExchange, Seq[String])]{ + def arity: Int + def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): T +} +object QueryParamReader{ + class SimpleParam[T](f: String => T) extends QueryParamReader[T]{ + def arity = 1 + def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): T = f(v.head) + } + + implicit object StringParam extends SimpleParam[String](x => x) + implicit object BooleanParam extends SimpleParam[Boolean](_.toBoolean) + implicit object ByteParam extends SimpleParam[Byte](_.toByte) + implicit object ShortParam extends SimpleParam[Short](_.toShort) + implicit object IntParam extends SimpleParam[Int](_.toInt) + implicit object LongParam extends SimpleParam[Long](_.toLong) + implicit object DoubleParam extends SimpleParam[Double](_.toDouble) + implicit object FloatParam extends SimpleParam[Float](_.toFloat) + implicit def SeqParam[T: QueryParamReader] = new QueryParamReader[Seq[T]]{ + def arity = 1 + def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]): Seq[T] = { + v.map(x => implicitly[QueryParamReader[T]].read(ctx, Seq(x))) + } + } + implicit def paramReader[T: ParamReader] = new QueryParamReader[T] { + override def arity = 0 + + override def read(ctx: (HttpServerExchange, Seq[String]), v: Seq[String]) = { + implicitly[ParamReader[T]].read(ctx, v) + } + } + +} + +class Subpath(val value: Seq[String]) +class Cookies(val value: Map[String, Cookie]) \ No newline at end of file diff --git a/cask/src/cask/Router.scala b/cask/src/cask/Router.scala index 649160e..90d5245 100644 --- a/cask/src/cask/Router.scala +++ b/cask/src/cask/Router.scala @@ -24,17 +24,15 @@ object Router{ * (just for logging and reading, not a replacement for a `TypeTag`) and * possible a function that can compute its default value */ - case class ArgSig[-T, +V, -C](name: String, - typeString: String, - doc: Option[String], - default: Option[T => V]) - (implicit val reads: ArgReader[V, C]) + 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]) - type AnyArgReader = ArgReader[Any, Nothing] - - trait ArgReader[+T, -C]{ + trait ArgReader[I, +T, -C]{ def arity: Int - def read(ctx: C, input: Seq[String]): T + def read(ctx: C, input: I): T } def stripDashes(s: String) = { @@ -52,87 +50,14 @@ object Router{ * 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[ArgSig[T, _, C]], - doc: Option[String], - varargs: Boolean, - invoke0: (T, C, Map[String, Seq[String]], Seq[String]) => Result[Any]){ - def invoke(target: T, ctx: C, groupedArgs: Seq[(String, Option[String])]): Result[Any] = { - var remainingArgSignatures = argSignatures.toList.filter(_.reads.arity > 0) - - val accumulatedKeywords = mutable.Map.empty[ArgSig[T, _, C], mutable.Buffer[String]] - val keywordableArgs = if (varargs) argSignatures.dropRight(1) else argSignatures - - for(arg <- keywordableArgs) accumulatedKeywords(arg) = mutable.Buffer.empty - - val leftoverArgs = mutable.Buffer.empty[String] - - val lookupArgSig = Map(argSignatures.map(x => (x.name, x)):_*) - - var incomplete: Option[ArgSig[T, _, C]] = None - - for(group <- groupedArgs){ - - group match{ - case (value, None) => - if (value(0) == '-' && !varargs){ - lookupArgSig.get(stripDashes(value)) match{ - case None => leftoverArgs.append(value) - case Some(sig) => incomplete = Some(sig) - } - - } else remainingArgSignatures match { - case Nil => leftoverArgs.append(value) - case last :: Nil if varargs => leftoverArgs.append(value) - case next :: rest => - accumulatedKeywords(next).append(value) - remainingArgSignatures = rest - } - case (rawKey, Some(value)) => - val key = stripDashes(rawKey) - lookupArgSig.get(key) match{ - case Some(x) if accumulatedKeywords.contains(x) => - if (accumulatedKeywords(x).nonEmpty && varargs){ - leftoverArgs.append(rawKey, value) - }else{ - accumulatedKeywords(x).append(value) - remainingArgSignatures = remainingArgSignatures.filter(_.name != key) - } - case _ => - leftoverArgs.append(rawKey, value) - } - } - } - - val missing0 = remainingArgSignatures - .filter(_.default.isEmpty) - - val missing = if(varargs) { - missing0.filter(_ != argSignatures.last) - } else { - missing0.filter(x => incomplete != Some(x)) - } - - if ( - incomplete.nonEmpty || - missing.nonEmpty || - (leftoverArgs.nonEmpty && !varargs) - ){ - Result.Error.MismatchedArguments( - missing = missing, - unknown = leftoverArgs, - duplicate = Nil, - incomplete = incomplete - - ) - } else { - val mapping = accumulatedKeywords - .map{case (k, single) => (k.name, single)} - .toMap - - try invoke0(target, ctx, mapping, leftoverArgs) - catch{case e: Throwable => Result.Error.Exception(e)} - } + case class EntryPoint[I, T, C](name: String, + argSignatures: Seq[ArgSig[I, T, _, C]], + doc: Option[String], + varargs: Boolean, + invoke0: (T, C, Map[String, I]) => Result[Any]){ + def invoke(target: T, ctx: C, args: Map[String, I]): Result[Any] = { + try invoke0(target, ctx, args) + catch{case e: Throwable => Result.Error.Exception(e)} } } @@ -141,14 +66,15 @@ object Router{ catch{ case e: Throwable => Left(error(e))} } - def read[C] - (dict: Map[String, Seq[String]], + def read[I, C] + (dict: Map[String, I], default: => Option[Any], - arg: ArgSig[_, _, C], - thunk: Seq[String] => Any): FailMaybe = { + arg: ArgSig[I, _, _, C], + thunk: I => Any): FailMaybe = { arg.reads.arity match{ case 0 => - tryEither(thunk(null), Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) + tryEither( + thunk(null.asInstanceOf[I]), Result.ParamError.DefaultFailed(arg, _)).left.map(Seq(_)) case 1 => dict.get(arg.name) match{ case None => @@ -193,10 +119,10 @@ object Router{ * Invoking the [[EntryPoint]] failed because the arguments provided * did not line up with the arguments expected */ - case class MismatchedArguments(missing: Seq[ArgSig[_, _, _]], + case class MismatchedArguments(missing: Seq[ArgSig[_, _, _, _]], unknown: Seq[String], - duplicate: Seq[(ArgSig[_, _, _], Seq[String])], - incomplete: Option[ArgSig[_, _, _]]) extends Error + duplicate: Seq[(ArgSig[_, _, _, _], Seq[String])], + incomplete: Option[ArgSig[_, _, _, _]]) extends Error /** * Invoking the [[EntryPoint]] failed because there were problems * deserializing/parsing individual arguments @@ -210,12 +136,12 @@ object Router{ * Something went wrong trying to de-serialize the input parameter; * the thrown exception is stored in [[ex]] */ - case class Invalid(arg: ArgSig[_, _, _], value: Seq[String], ex: Throwable) extends ParamError + case class Invalid(arg: ArgSig[_, _, _, _], value: Any, 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 + case class DefaultFailed(arg: ArgSig[_, _, _, _], ex: Throwable) extends ParamError } } @@ -233,18 +159,18 @@ object Router{ } } - def makeReadCall[C] - (dict: Map[String, Seq[String]], + def makeReadCall[I, C] + (dict: Map[String, I], ctx: C, default: => Option[Any], - arg: ArgSig[_, _, C]) = { - read(dict, default, arg, arg.reads.read(ctx, _)) + arg: ArgSig[I, _, _, C]) = { + read[I, C](dict, default, arg, arg.reads.read(ctx, _)) } } -class Router [C <: Context](val c: C) { +class Router[C <: Context](val c: C) { import c.universe._ def getValsOrMeths(curCls: Type): Iterable[MethodSymbol] = { def isAMemberOfAnyRef(member: Symbol) = { @@ -274,7 +200,8 @@ class Router [C <: Context](val c: C) { curCls: c.universe.Type, wrapOutput: c.Tree => c.Tree, ctx: c.Type, - argReader: c.Tree): c.universe.Tree = { + argReader: c.Tree, + annotDeserializeType: c.Tree): c.universe.Tree = { val baseArgSym = TermName(c.freshName()) val flattenedArgLists = meth.paramss.flatten def hasDefault(i: Int) = { @@ -340,7 +267,7 @@ class Router [C <: Context](val c: C) { } val argSig = q""" - cask.Router.ArgSig[$curCls, $docUnwrappedType, $ctx]( + cask.Router.ArgSig[$annotDeserializeType, $curCls, $docUnwrappedType, $ctx]( ${arg.name.toString}, ${docUnwrappedType.toString + (if(vararg) "*" else "")}, $docTree, @@ -378,15 +305,15 @@ class Router [C <: Context](val c: C) { if (meth.paramLists.isEmpty) q"$baseArgSym.${meth.name.toTermName}" else q"$baseArgSym.${meth.name.toTermName}(..$argNameCasts)" val res = q""" - cask.Router.EntryPoint[$curCls, $ctx]( + cask.Router.EntryPoint[$annotDeserializeType, $curCls, $ctx]( ${meth.name.toString}, scala.Seq(..$argSigs), ${methodDoc match{ - case None => q"scala.None" - case Some(s) => q"scala.Some($s)" - }}, + case None => q"scala.None" + case Some(s) => q"scala.Some($s)" + }}, ${varargs.contains(true)}, - ($baseArgSym: $curCls, ctx: $ctx, $argListSymbol: Map[String, Seq[String]], $extrasSymbol: Seq[String]) => + ($baseArgSym: $curCls, ctx: $ctx, $argListSymbol: Map[String, $annotDeserializeType]) => cask.Router.validate(Seq(..$readArgs)) match{ case cask.Router.Result.Success(Seq(..$argNames)) => cask.Router.Result.Success( @@ -394,7 +321,7 @@ class Router [C <: Context](val c: C) { ) case x: cask.Router.Result.Error => x } - ) + ).asInstanceOf[cask.Router.EntryPoint[Any, $curCls, $ctx]] """ c.internal.transform(res){(t, a) => diff --git a/cask/src/cask/Routes.scala b/cask/src/cask/Routes.scala index 4883e34..e539312 100644 --- a/cask/src/cask/Routes.scala +++ b/cask/src/cask/Routes.scala @@ -1,17 +1,28 @@ package cask import language.experimental.macros -import java.io.OutputStream +import java.io.{InputStream, OutputStream, OutputStreamWriter, StringWriter} import cask.Router.EntryPoint import io.undertow.server.HttpServerExchange import scala.reflect.macros.blackbox.Context -import java.io.InputStream + +case class Request(cookies: Map[String, Cookie], + data: InputStream, + queryParams: Map[String, Seq[String]], + headers: Map[String, Seq[String]]) case class Response(data: Response.Data, statusCode: Int = 200, headers: Seq[(String, String)] = Nil, - cookies: Seq[Cookie] = Nil) + cookies: Seq[Cookie] = Nil) extends BaseResponse + +trait BaseResponse{ + def data: Response.Data + def statusCode: Int + def headers: Seq[(String, String)] + def cookies: Seq[Cookie] +} object Response{ implicit def dataResponse[T](t: T)(implicit c: T => Data) = Response(t) trait Data{ @@ -27,12 +38,18 @@ object Response{ implicit class StreamData(b: InputStream) extends Data{ def write(out: OutputStream) = b.transferTo(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 Routes{ case class EndpointMetadata[T](metadata: Endpoint[_], - entryPoint: EntryPoint[T, (HttpServerExchange, Seq[String])]) + entryPoint: EntryPoint[_, T, (HttpServerExchange, Seq[String])]) case class RoutesEndpointsMetadata[T](value: EndpointMetadata[T]*) object RoutesEndpointsMetadata{ implicit def initialize[T] = macro initializeImpl[T] @@ -51,13 +68,17 @@ object Routes{ weakTypeOf[T], (t: router.c.universe.Tree) => q"$annotObjectSym.wrapMethodOutput($t)", c.weakTypeOf[(io.undertow.server.HttpServerExchange, Seq[String])], - q"$annotObjectSym.parseMethodInput" + q"$annotObjectSym.parseMethodInput", + tq"$annotObjectSym.InputType" ) q"""{ val $annotObjectSym = $annotObject - cask.Routes.EndpointMetadata($annotObjectSym, $route) + cask.Routes.EndpointMetadata( + $annotObjectSym, + $route + ) }""" } diff --git a/cask/src/cask/Status.scala b/cask/src/cask/Status.scala index a1915b1..f3f5434 100644 --- a/cask/src/cask/Status.scala +++ b/cask/src/cask/Status.scala @@ -46,6 +46,7 @@ object Status { 420 -> EnhanceYourCalm, 429 -> TooManyRequests, 451 -> UnavailableForLegalReasons, + 500 -> InternalServerError, 501 -> NotImplemented, 502 -> BadGateway, 503 -> ServiceUnavailable, diff --git a/cask/test/src/test/cask/FormJsonPost.scala b/cask/test/src/test/cask/FormJsonPost.scala new file mode 100644 index 0000000..2874a52 --- /dev/null +++ b/cask/test/src/test/cask/FormJsonPost.scala @@ -0,0 +1,19 @@ +package test.cask + +import cask.FormValue +import io.undertow.server.HttpServerExchange + +object FormJsonPost extends cask.MainRoutes{ + @cask.postJson("/json") + def jsonEndpoint(x: HttpServerExchange, value1: ujson.Js.Value, value2: Seq[Int]) = { + "OK " + value1 + " " + value2 + } + + @cask.postForm("/form") + def formEndpoint(x: HttpServerExchange, value1: FormValue, value2: Seq[Int]) = { + "OK " + value1 + " " + value2 + } + + initialize() +} + -- cgit v1.2.3