diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala index 93a1990e..7b58770e 100644 --- a/sjsonnet/src/sjsonnet/Evaluator.scala +++ b/sjsonnet/src/sjsonnet/Evaluator.scala @@ -53,7 +53,7 @@ class Evaluator(parseCache: collection.mutable.Map[String, fastparse.Parsed[Expr case BinaryOp(_, lhs, Expr.BinaryOp.`in`, Super(offset)) => - val key = visitExpr(lhs, scope).asInstanceOf[Val.Str] + val key = visitExpr(lhs, scope).cast[Val.Str] Val.bool(scope.super0.get.value0.contains(key.value)) case $(offset) => scope.dollar @@ -85,7 +85,7 @@ class Evaluator(parseCache: collection.mutable.Map[String, fastparse.Parsed[Expr case None => Evaluator.fail("Assertion failed", scope.currentFile, offset, wd) case Some(msg) => Evaluator.fail( - "Assertion failed: " + visitExpr(msg, scope).asInstanceOf[Val.Str].value, + "Assertion failed: " + visitExpr(msg, scope).cast[Val.Str].value, scope.currentFile, offset, wd @@ -114,7 +114,7 @@ class Evaluator(parseCache: collection.mutable.Map[String, fastparse.Parsed[Expr ) case Apply(offset, value, Args(args)) => val lhs = visitExpr(value, scope) - try lhs.asInstanceOf[Val.Func].apply( + try lhs.cast[Val.Func].apply( args.map{case (k, v) => (k, Lazy(visitExpr(v, scope)))}, scope.currentFile.last, extVars, @@ -143,7 +143,7 @@ class Evaluator(parseCache: collection.mutable.Map[String, fastparse.Parsed[Expr case Lookup(offset, value, index) => if (value.isInstanceOf[Super]){ - val key = visitExpr(index, scope).asInstanceOf[Val.Str] + val key = visitExpr(index, scope).cast[Val.Str] scope.super0.get.value(key.value, scope.currentFile, scope.currentRoot, offset, wd, extVars).force }else (visitExpr(value, scope), visitExpr(index, scope)) match { case (v: Val.Arr, i: Val.Num) => @@ -165,22 +165,30 @@ class Evaluator(parseCache: collection.mutable.Map[String, fastparse.Parsed[Expr visitExpr(value, scope) match{ case Val.Arr(a) => - val range = start.fold(0)(visitExpr(_, scope).asInstanceOf[Val.Num].value.toInt) until end.fold(a.length)(visitExpr(_, scope).asInstanceOf[Val.Num].value.toInt) by stride.fold(1)(visitExpr(_, scope).asInstanceOf[Val.Num].value.toInt) + val range = + start.fold(0)(visitExpr(_, scope).cast[Val.Num].value.toInt) until + end.fold(a.length)(visitExpr(_, scope).cast[Val.Num].value.toInt) by + stride.fold(1)(visitExpr(_, scope).cast[Val.Num].value.toInt) Val.Arr(range.dropWhile(_ < 0).takeWhile(_ < a.length).map(a)) case Val.Str(s) => - val range = start.fold(0)(visitExpr(_, scope).asInstanceOf[Val.Num].value.toInt) until end.fold(s.length)(visitExpr(_, scope).asInstanceOf[Val.Num].value.toInt) by stride.fold(1)(visitExpr(_, scope).asInstanceOf[Val.Num].value.toInt) + val range = + start.fold(0)(visitExpr(_, scope).cast[Val.Num].value.toInt) until + end.fold(s.length)(visitExpr(_, scope).cast[Val.Num].value.toInt) by + stride.fold(1)(visitExpr(_, scope).cast[Val.Num].value.toInt) Val.Str(range.dropWhile(_ < 0).takeWhile(_ < s.length).map(s).mkString) + case x => Evaluator.fail("Can only slice array or string, not " + x.prettyName, scope.currentFile, offset, wd) } case Function(offset, params, body) => visitMethod(scope, body, params, offset) case IfElse(offset, cond, then, else0) => visitExpr(cond, scope) match{ case Val.True => visitExpr(then, scope) case Val.False => else0.fold[Val](Val.Null)(visitExpr(_, scope)) + case v => Evaluator.fail("Need boolean, found " + v.prettyName, scope.currentFile, offset, wd) } case Comp(offset, value, first, rest) => Val.Arr(visitComp(first :: rest.toList, Seq(scope)).map(s => Lazy(visitExpr(value, s)))) case ObjExtend(offset, value, ext) => { - val original = visitExpr(value, scope).asInstanceOf[Val.Obj] + val original = visitExpr(value, scope).cast[Val.Obj] val extension = visitObjBody(ext, scope) Evaluator.mergeObjects(original, extension) } @@ -349,7 +357,7 @@ class Evaluator(parseCache: collection.mutable.Map[String, fastparse.Parsed[Expr case None => Evaluator.fail("Assertion failed", scope.currentFile, value.offset, wd) case Some(msg) => Evaluator.fail( - "Assertion failed: " + visitExpr(msg, newScope).asInstanceOf[Val.Str].value, + "Assertion failed: " + visitExpr(msg, newScope).cast[Val.Str].value, scope.currentFile, value.offset, wd diff --git a/sjsonnet/src/sjsonnet/Format.scala b/sjsonnet/src/sjsonnet/Format.scala index acb4a457..2880c571 100644 --- a/sjsonnet/src/sjsonnet/Format.scala +++ b/sjsonnet/src/sjsonnet/Format.scala @@ -94,7 +94,7 @@ object Format{ case _ => val value = formatted.label match{ - case None => Materializer(values.asInstanceOf[Val.Arr].value(i).force, extVars, wd) + case None => Materializer(values.cast[Val.Arr].value(i).force, extVars, wd) case Some(key) => values match{ case v: Val.Arr => Materializer(v.value(i).force, extVars, wd) diff --git a/sjsonnet/src/sjsonnet/Std.scala b/sjsonnet/src/sjsonnet/Std.scala index 2dd0659b..6a3555f6 100644 --- a/sjsonnet/src/sjsonnet/Std.scala +++ b/sjsonnet/src/sjsonnet/Std.scala @@ -129,7 +129,7 @@ object Std { } }, builtin("codepoint", "str"){ (wd, extVars, v1: Val) => - v1.asInstanceOf[Val.Str].value.charAt(0).toInt + v1.cast[Val.Str].value.charAt(0).toInt }, builtin("length", "x"){ (wd, extVars, v1: Val) => v1 match{ @@ -137,6 +137,7 @@ object Std { case Val.Arr(s) => s.length case o: Val.Obj => o.getVisibleKeys().count(!_._2) case o: Val.Func => o.params.args.length + case _ => throw new DelegateError("Cannot get length of " + v1.prettyName) } }, builtin("objectHas", "o", "f"){ (wd, extVars, v1: Val.Obj, v2: String) => @@ -147,8 +148,7 @@ object Std { }, builtin("objectFields", "o"){ (wd, extVars, v1: Val.Obj) => Val.Arr( - v1.asInstanceOf[Val.Obj] - .getVisibleKeys() + v1.getVisibleKeys() .collect{case (k, false) => k} .toSeq .sorted @@ -157,8 +157,7 @@ object Std { }, builtin("objectFieldsAll", "o"){ (wd, extVars, v1: Val.Obj) => Val.Arr( - v1.asInstanceOf[Val.Obj] - .getVisibleKeys() + v1.getVisibleKeys() .collect{case (k, _) => k} .toSeq .sorted @@ -177,10 +176,17 @@ object Std { } }, builtin("lines", "arr"){ (wd, extVars, v1: Val.Arr) => + v1.value.map(_.force).foreach{ + case _: Val.Str | Val.Null => // donothing + case x => throw new DelegateError("Cannot call .lines on " + x.prettyName) + } Materializer.apply(v1, extVars, wd).asInstanceOf[ujson.Js.Arr] .value .filter(_ != ujson.Js.Null) - .map{case ujson.Js.Str(s) => s + "\n"} + .map{ + case ujson.Js.Str(s) => s + "\n" + case _ => ??? /* we ensure it's all strings above */ + } .mkString }, builtin("format", "str", "vals"){ (wd, extVars, v1: String, v2: Val) => @@ -376,7 +382,16 @@ object Std { builtin("join", "sep", "arr"){ (wd, extVars, sep: Val, arr: Val.Arr) => val res: Val = sep match{ case Val.Str(s) => - Val.Str(arr.value.map(_.force).filter(_ != Val.Null).map{case Val.Str(x) => x}.mkString(s)) + Val.Str( + arr.value + .map(_.force) + .filter(_ != Val.Null) + .map{ + case Val.Str(x) => x + case x => throw new DelegateError("Cannot join " + x.prettyName) + } + .mkString(s) + ) case Val.Arr(sep) => val out = collection.mutable.Buffer.empty[Lazy] for(x <- arr.value){ @@ -385,9 +400,11 @@ object Std { case Val.Arr(v) => if (out.nonEmpty) out.appendAll(sep) out.appendAll(v) + case x => throw new DelegateError("Cannot join " + x.prettyName) } } Val.Arr(out) + case x => throw new DelegateError("Cannot join " + x.prettyName) } res }, @@ -397,6 +414,7 @@ object Std { x.force match{ case Val.Null => // do nothing case Val.Arr(v) => out.appendAll(v) + case x => throw new DelegateError("Cannot call flattenArrays on " + x) } } Val.Arr(out) @@ -406,7 +424,12 @@ object Std { def sect(x: ujson.Js.Obj) = { x.value.flatMap{ case (k, ujson.Js.Str(v)) => Seq(k + " = " + v) - case (k, ujson.Js.Arr(vs)) => vs.map{case ujson.Js.Str(v) => k + " = " + v} + case (k, ujson.Js.Arr(vs)) => + vs.map{ + case ujson.Js.Str(v) => k + " = " + v + case x => throw new DelegateError("Cannot call manifestIni on " + x.getClass) + } + case (k, x) => throw new DelegateError("Cannot call manifestIni on " + x.getClass) } } val lines = materialized.obj.get("main").fold(Iterable[String]())(x => sect(x.asInstanceOf[ujson.Js.Obj])) ++ @@ -473,11 +496,16 @@ object Std { case ujson.Js.Str(s) => s case ujson.Js.Arr(Seq(ujson.Js.Str(t), attrs: ujson.Js.Obj, children@_*)) => tag(t)( - attrs.value.map { case (k, ujson.Js.Str(v)) => attr(k) := v }.toSeq, + attrs.value.map { + case (k, ujson.Js.Str(v)) => attr(k) := v + case (k, v) => throw new DelegateError("Cannot call manifestXmlJsonml on " + v.getClass) + }.toSeq, children.map(rec) ) case ujson.Js.Arr(Seq(ujson.Js.Str(t), children@_*)) => tag(t)(children.map(rec)) + case x => + throw new DelegateError("Cannot call manifestXmlJsonml on " + x.getClass) } } @@ -487,7 +515,8 @@ object Std { builtin("base64", "v"){ (wd, extVars, v: Val) => v match{ case Val.Str(value) => Base64.getEncoder().encodeToString(value.getBytes) - case Val.Arr(bytes) => Base64.getEncoder().encodeToString(bytes.map(_.force.asInstanceOf[Val.Num].value.toByte).toArray) + case Val.Arr(bytes) => Base64.getEncoder().encodeToString(bytes.map(_.force.cast[Val.Num].value.toByte).toArray) + case x => throw new DelegateError("Cannot base64 encode " + x.prettyName) } }, @@ -503,14 +532,15 @@ object Std { Val.Arr( if (vs.forall(_.force.isInstanceOf[Val.Str])){ - vs.map(_.force.asInstanceOf[Val.Str]).sortBy(_.value).map(Lazy(_)) + vs.map(_.force.cast[Val.Str]).sortBy(_.value).map(Lazy(_)) }else if (vs.forall(_.force.isInstanceOf[Val.Num])){ - vs.map(_.force.asInstanceOf[Val.Num]).sortBy(_.value).map(Lazy(_)) + vs.map(_.force.cast[Val.Num]).sortBy(_.value).map(Lazy(_)) }else { ??? } ) case Val.Str(s) => Val.Arr(s.sorted.map(c => Lazy(Val.Str(c.toString)))) + case x => throw new DelegateError("Cannot sort " + x.prettyName) } }, builtin("uniq", "arr"){ (wd, extVars, arr: Val.Arr) => @@ -527,7 +557,9 @@ object Std { vs0.map(_.asInstanceOf[ujson.Js.Str]).sortBy(_.value) }else if (vs0.forall(_.isInstanceOf[ujson.Js.Num])){ vs0.map(_.asInstanceOf[ujson.Js.Num]).sortBy(_.value) - }else ??? + }else { + throw new DelegateError("Every element of the input must be of the same type, string or number") + } val out = collection.mutable.Buffer.empty[ujson.Js] for(v <- vs) if (out.isEmpty || out.last != v) out.append(v) @@ -544,7 +576,9 @@ object Std { vs0.map(_.asInstanceOf[ujson.Js.Str]).sortBy(_.value) }else if (vs0.forall(_.isInstanceOf[ujson.Js.Num])){ vs0.map(_.asInstanceOf[ujson.Js.Num]).sortBy(_.value) - }else ??? + }else { + throw new DelegateError("Every element of the input must be of the same type, string or number") + } val out = collection.mutable.Buffer.empty[ujson.Js] for(v <- vs) if (out.isEmpty || out.last != v) out.append(v) @@ -567,7 +601,9 @@ object Std { vs0.map(_.asInstanceOf[ujson.Js.Str]).sortBy(_.value) }else if (vs0.forall(_.isInstanceOf[ujson.Js.Num])){ vs0.map(_.asInstanceOf[ujson.Js.Num]).sortBy(_.value) - }else ??? + }else { + throw new DelegateError("Every element of the input must be of the same type, string or number") + } val out = collection.mutable.Buffer.empty[ujson.Js] for(v <- vs) if (out.isEmpty || out.last != v) out.append(v) @@ -587,7 +623,9 @@ object Std { vs0.map(_.asInstanceOf[ujson.Js.Str]).sortBy(_.value) }else if (vs0.forall(_.isInstanceOf[ujson.Js.Num])){ vs0.map(_.asInstanceOf[ujson.Js.Num]).sortBy(_.value) - }else ??? + }else { + throw new DelegateError("Every element of the input must be of the same type, string or number") + } val out = collection.mutable.Buffer.empty[ujson.Js] for(v <- vs) if (out.isEmpty || out.last != v) out.append(v) diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index 82de53d1..1f50f399 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -4,6 +4,8 @@ import ammonite.ops.Path import sjsonnet.Expr.Member.Visibility import sjsonnet.Expr.Params +import scala.reflect.ClassTag + object Lazy{ def apply(calc0: => Val) = new Lazy(calc0) } @@ -12,13 +14,33 @@ class Lazy(calc0: => Val){ } sealed trait Val{ def prettyName: String + def cast[T: ClassTag: PrettyNamed] = + if (implicitly[ClassTag[T]].runtimeClass.isInstance(this)) this.asInstanceOf[T] + else throw new DelegateError( + "Expected " + implicitly[PrettyNamed[T]].s + ", found " + prettyName + ) +} +class PrettyNamed[T](val s: String) +object PrettyNamed{ + implicit def boolName: PrettyNamed[Val.Bool] = new PrettyNamed("boolean") + implicit def nullName: PrettyNamed[Val.Null.type] = new PrettyNamed("null") + implicit def strName: PrettyNamed[Val.Str] = new PrettyNamed("string") + implicit def numName: PrettyNamed[Val.Num] = new PrettyNamed("number") + implicit def arrName: PrettyNamed[Val.Arr] = new PrettyNamed("array") + implicit def objName: PrettyNamed[Val.Obj] = new PrettyNamed("object") + implicit def funName: PrettyNamed[Val.Func] = new PrettyNamed("function") } object Val{ def bool(b: Boolean) = if (b) True else False - case object True extends Val{ + sealed trait Bool extends Val{ + def value: Boolean + } + case object True extends Bool{ + def value = true def prettyName = "boolean" } - case object False extends Val{ + case object False extends Bool{ + def value = false def prettyName = "boolean" } case object Null extends Val{