summaryrefslogtreecommitdiff
path: root/cask
diff options
context:
space:
mode:
authorLi Haoyi <haoyi.sg@gmail.com>2018-07-25 18:46:43 +0800
committerLi Haoyi <haoyi.sg@gmail.com>2018-07-25 18:48:25 +0800
commite52c6f57ef42b54a355d0976cb43f6762280e855 (patch)
treec3cc0d8a971c7f8b112886c725bb759c3471151d /cask
parent5c8a2030c048f96a7ded0c2f701e1612b53a2046 (diff)
downloadcask-e52c6f57ef42b54a355d0976cb43f6762280e855.tar.gz
cask-e52c6f57ef42b54a355d0976cb43f6762280e855.tar.bz2
cask-e52c6f57ef42b54a355d0976cb43f6762280e855.zip
Basic invocation error renderer copied from Ammonite
Diffstat (limited to 'cask')
-rw-r--r--cask/src/cask/internal/Router.scala22
-rw-r--r--cask/src/cask/internal/Util.scala25
-rw-r--r--cask/src/cask/main/Main.scala125
-rw-r--r--cask/test/src/test/cask/ExampleTests.scala30
4 files changed, 172 insertions, 30 deletions
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)"
}
}
}