From e52c6f57ef42b54a355d0976cb43f6762280e855 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 25 Jul 2018 18:46:43 +0800 Subject: Basic invocation error renderer copied from Ammonite --- cask/src/cask/internal/Router.scala | 22 ++--- cask/src/cask/internal/Util.scala | 25 ++++++ cask/src/cask/main/Main.scala | 125 +++++++++++++++++++++++++++-- cask/test/src/test/cask/ExampleTests.scala | 30 ++++--- 4 files changed, 172 insertions(+), 30 deletions(-) (limited to 'cask') diff --git a/cask/src/cask/internal/Router.scala b/cask/src/cask/internal/Router.scala index a77284e..9302e45 100644 --- a/cask/src/cask/internal/Router.scala +++ b/cask/src/cask/internal/Router.scala @@ -1,11 +1,7 @@ package cask.internal - -import io.undertow.server.HttpServerExchange - import language.experimental.macros import scala.annotation.StaticAnnotation -import scala.collection.mutable import scala.reflect.macros.blackbox.Context /** @@ -56,8 +52,14 @@ object Router{ 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)} + val unknown = args.keySet -- argSignatures.map(_.name).toSet + val missing = argSignatures.filter(as => as.reads.arity != 0 && !args.contains(as.name) && as.default.isEmpty) + + if (missing.nonEmpty || unknown.nonEmpty) Result.Error.MismatchedArguments(missing, unknown.toSeq) + else { + try invoke0(target, ctx, args) + catch{case e: Throwable => Result.Error.Exception(e)} + } } } @@ -99,9 +101,7 @@ object Router{ * did not line up with the arguments expected */ case class MismatchedArguments(missing: Seq[ArgSig[_, _, _, _]], - unknown: Seq[String], - duplicate: Seq[(ArgSig[_, _, _, _], Seq[String])], - incomplete: Option[ArgSig[_, _, _, _]]) extends Error + unknown: Seq[String]) extends Error /** * Invoking the [[EntryPoint]] failed because there were problems * deserializing/parsing individual arguments @@ -115,7 +115,7 @@ 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: Any, ex: Throwable) extends ParamError + case class Invalid(arg: ArgSig[_, _, _, _], value: String, ex: Throwable) extends ParamError /** * Something went wrong trying to evaluate the default value * for this input parameter @@ -152,7 +152,7 @@ object Router{ 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, _)).left.map(Seq(_)) + tryEither(arg.reads.read(ctx, arg.name, x), Result.ParamError.Invalid(arg, x.toString, _)).left.map(Seq(_)) } } diff --git a/cask/src/cask/internal/Util.scala b/cask/src/cask/internal/Util.scala index 84c8d52..59bc2ce 100644 --- a/cask/src/cask/internal/Util.scala +++ b/cask/src/cask/internal/Util.scala @@ -1,6 +1,31 @@ package cask.internal object Util { + def pluralize(s: String, n: Int) = { + if (n == 1) s else s + "s" + } def splitPath(p: String) = p.dropWhile(_ == '/').reverse.dropWhile(_ == '/').reverse.split('/').filter(_.nonEmpty) + + def softWrap(s: String, leftOffset: Int, maxWidth: Int) = { + val oneLine = s.lines.mkString(" ").split(' ') + + lazy val indent = " " * leftOffset + + val output = new StringBuilder(oneLine.head) + var currentLineWidth = oneLine.head.length + for(chunk <- oneLine.tail){ + val addedWidth = currentLineWidth + chunk.length + 1 + if (addedWidth > maxWidth){ + output.append("\n" + indent) + output.append(chunk) + currentLineWidth = chunk.length + } else{ + currentLineWidth = addedWidth + output.append(' ') + output.append(chunk) + } + } + output.mkString + } } diff --git a/cask/src/cask/main/Main.scala b/cask/src/cask/main/Main.scala index 045c2ed..680dd04 100644 --- a/cask/src/cask/main/Main.scala +++ b/cask/src/cask/main/Main.scala @@ -7,7 +7,7 @@ import io.undertow.Undertow import io.undertow.server.{HttpHandler, HttpServerExchange} import io.undertow.server.handlers.BlockingHandler import io.undertow.util.HttpString - +import fastparse.utils.Utils.literalize class MainRoutes extends BaseMain with Routes{ def allRoutes = Seq(this) @@ -47,6 +47,111 @@ abstract class BaseMain{ response.data.write(exchange.getOutputStream) } + def getLeftColWidth(items: Seq[Router.ArgSig[_, _, _,_]]) = { + items.map(_.name.length + 2) match{ + case Nil => 0 + case x => x.max + } + } + + def renderArg[T](base: T, + arg: Router.ArgSig[_, T, _, _], + leftOffset: Int, + wrappedWidth: Int): (String, String) = { + val suffix = arg.default match{ + case Some(f) => " (default " + f(base) + ")" + case None => "" + } + val docSuffix = arg.doc match{ + case Some(d) => ": " + d + case None => "" + } + val wrapped = Util.softWrap( + arg.typeString + suffix + docSuffix, + leftOffset, + wrappedWidth - leftOffset + ) + (arg.name, wrapped) + } + + def formatMainMethodSignature[T](base: T, + main: Router.EntryPoint[_, T, _], + leftIndent: Int, + leftColWidth: Int) = { + // +2 for space on right of left col + val args = main.argSignatures.map(renderArg(base, _, leftColWidth + leftIndent + 2 + 2, 80)) + + val leftIndentStr = " " * leftIndent + val argStrings = + for((lhs, rhs) <- args) + yield { + val lhsPadded = lhs.padTo(leftColWidth, ' ') + val rhsPadded = rhs.lines.mkString("\n") + s"$leftIndentStr $lhsPadded $rhsPadded" + } + val mainDocSuffix = main.doc match{ + case Some(d) => "\n" + leftIndentStr + Util.softWrap(d, leftIndent, 80) + case None => "" + } + + s"""$leftIndentStr${main.name}$mainDocSuffix + |${argStrings.map(_ + "\n").mkString}""".stripMargin + } + + def formatInvokeError[T](base: T, route: Router.EntryPoint[_, T, _], x: Router.Result.Error): String = { + def expectedMsg = formatMainMethodSignature(base: T, route, 0, 0) + + x match{ + case Router.Result.Error.Exception(x) => ??? + case Router.Result.Error.MismatchedArguments(missing, unknown) => + val missingStr = + if (missing.isEmpty) "" + else { + val chunks = + for (x <- missing) + yield x.name + ": " + x.typeString + + val argumentsStr = Util.pluralize("argument", chunks.length) + s"Missing $argumentsStr: (${chunks.mkString(", ")})\n" + } + + + val unknownStr = + if (unknown.isEmpty) "" + else { + val argumentsStr = Util.pluralize("argument", unknown.length) + s"Unknown $argumentsStr: " + unknown.map(literalize(_)).mkString(" ") + "\n" + } + + + s"""$missingStr$unknownStr + |Arguments provided did not match expected signature: + | + |$expectedMsg + |""".stripMargin + + case Router.Result.Error.InvalidArguments(x) => + val argumentsStr = Util.pluralize("argument", x.length) + val thingies = x.map{ + case Router.Result.ParamError.Invalid(p, v, ex) => + val literalV = literalize(v) + + s"${p.name}: ${p.typeString} = $literalV failed to parse with $ex" + case Router.Result.ParamError.DefaultFailed(p, ex) => + s"${p.name}'s default value failed to evaluate with $ex" + } + + s"""The following $argumentsStr failed to parse: + | + |${thingies.mkString("\n")} + | + |expected signature: + | + |$expectedMsg + |""".stripMargin + + } + } lazy val defaultHandler = new HttpHandler() { def handleRequest(exchange: HttpServerExchange): Unit = { routeTrie.lookup(Util.splitPath(exchange.getRequestPath).toList, Map()) match{ @@ -61,13 +166,17 @@ abstract class BaseMain{ result match{ case Router.Result.Success(response) => writeResponse(exchange, response) - case Router.Result.Error.Exception(e) => - println(e) - e.printStackTrace() - writeResponse(exchange, handleError(500)) - case err: Router.Result.Error => - println(err) - writeResponse(exchange, handleError(400)) + case e: Router.Result.Error => + + writeResponse(exchange, + Response( + formatInvokeError( + routes, + metadata.entryPoint.asInstanceOf[EntryPoint[_, cask.main.Routes, _]], + e + ), + statusCode = 500) + ) } diff --git a/cask/test/src/test/cask/ExampleTests.scala b/cask/test/src/test/cask/ExampleTests.scala index ffc4ef2..f521660 100644 --- a/cask/test/src/test/cask/ExampleTests.scala +++ b/cask/test/src/test/cask/ExampleTests.scala @@ -14,6 +14,7 @@ object ExampleTests extends TestSuite{ server.stop() res } + val tests = Tests{ 'MinimalApplication - test(MinimalApplication){ host => val success = requests.get(host) @@ -30,25 +31,32 @@ object ExampleTests extends TestSuite{ successInfo.text().contains("my-query-param"), successInfo.text().contains("my-query-value") ) - successInfo.statusCode ==> 200 } 'VariableRoutes - test(VariableRoutes){ host => val noIndexPage = requests.get(host) noIndexPage.statusCode ==> 404 - val userPage = requests.get(host + "/user/lihaoyi") - userPage.text() ==> "User lihaoyi" - userPage.statusCode ==> 200 + requests.get(host + "/user/lihaoyi").text() ==> "User lihaoyi" + + requests.get(host + "/user").statusCode ==> 404 + - val badUserPage = requests.get(host + "/user") - badUserPage.statusCode ==> 404 + requests.get(host + "/post/123?query=xyz&query=abc") ==> + "Post 123 ArrayBuffer(xyz, abc)" - val postPage = requests.get(host + "/post/123?query=xyz&query=abc") - postPage.text() ==> "Post 123 ArrayBuffer(xyz, abc)" - userPage.statusCode ==> 200 + requests.get(host + "/post/123").text() ==> + """Missing argument: (query: Seq[String]) + | + |Arguments provided did not match expected signature: + | + |showPost + | postId Int + | query Seq[String] + | + |""".stripMargin - val badPostPage = requests.get(host + "/post/123") - badPostPage.text() + requests.get(host + "/path/one/two/three").text() ==> + "Subpath List(one, two, three)" } } } -- cgit v1.2.3