diff --git a/project/build.properties b/project/build.properties index 30409871..081fdbbc 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.4 +sbt.version=1.10.0 diff --git a/readme.md b/readme.md index 9981edc6..ff329f22 100644 --- a/readme.md +++ b/readme.md @@ -280,6 +280,14 @@ to ensure the output bytecode remains compatible with users on older JVMs. ## Changelog +### 0.4.11 +- Implement `std.isEmpty`, `std.xor`, `std.xnor`, `std.trim`, + `std.equalsIgnoreCase`, `std.sha1`, `std.sha256`, `std.sha512`, `std.sha3` [#204](https://github.com/databricks/sjsonnet/pull/210) +- fix: std.manifestJsonMinified and empty arrays/objects [#207](https://github.com/databricks/sjsonnet/pull/207) +- fix: Use different chars for synthetic paths. [#208](https://github.com/databricks/sjsonnet/pull/208) +- Fix sorting algorithm to work for all array types [#211](https://github.com/databricks/sjsonnet/pull/211) +- Add better error handling for format [#212](https://github.com/databricks/sjsonnet/pull/212) + ### 0.4.10 - Implement `std.get` [#202](https://github.com/databricks/sjsonnet/pull/202), diff --git a/sjsonnet/src-js/sjsonnet/Platform.scala b/sjsonnet/src-js/sjsonnet/Platform.scala index 81805fb7..45c2ede3 100644 --- a/sjsonnet/src-js/sjsonnet/Platform.scala +++ b/sjsonnet/src-js/sjsonnet/Platform.scala @@ -19,6 +19,18 @@ object Platform { def md5(s: String): String = { throw new Exception("MD5 not implemented in Scala.js") } + def sha1(s: String): String = { + throw new Exception("SHA1 not implemented in Scala.js") + } + def sha256(s: String): String = { + throw new Exception("SHA256 not implemented in Scala.js") + } + def sha512(s: String): String = { + throw new Exception("SHA512 not implemented in Scala.js") + } + def sha3(s: String): String = { + throw new Exception("SHA3 not implemented in Scala.js") + } def hashFile(file: File): String = { throw new Exception("hashFile not implemented in Scala.js") } diff --git a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala index c5d5b050..f9d30b98 100644 --- a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala +++ b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala @@ -179,7 +179,7 @@ object SjsonnetMain { std: Val.Obj = new Std().Std): Either[String, String] = { val (jsonnetCode, path) = - if (config.exec.value) (file, wd / "") + if (config.exec.value) (file, wd / "\uFE64exec\uFE65") else { val p = os.Path(file, wd) (os.read(p), p) diff --git a/sjsonnet/src-jvm/sjsonnet/Platform.scala b/sjsonnet/src-jvm/sjsonnet/Platform.scala index a1ed1336..160e211f 100644 --- a/sjsonnet/src-jvm/sjsonnet/Platform.scala +++ b/sjsonnet/src-jvm/sjsonnet/Platform.scala @@ -15,11 +15,13 @@ object Platform { def gzipBytes(b: Array[Byte]): String = { val outputStream: ByteArrayOutputStream = new ByteArrayOutputStream(b.length) val gzip: GZIPOutputStream = new GZIPOutputStream(outputStream) - gzip.write(b) - gzip.close() - val gzippedBase64: String = Base64.getEncoder.encodeToString(outputStream.toByteArray) - outputStream.close() - gzippedBase64 + try { + gzip.write(b) + Base64.getEncoder.encodeToString(outputStream.toByteArray) + } finally { + gzip.close() + outputStream.close() + } } def gzipString(s: String): String = { @@ -34,11 +36,13 @@ object Platform { // Set compression to specified level val level = compressionLevel.getOrElse(LZMA2Options.PRESET_DEFAULT) val xz: XZOutputStream = new XZOutputStream(outputStream, new LZMA2Options(level)) - xz.write(b) - xz.close() - val xzedBase64: String = Base64.getEncoder.encodeToString(outputStream.toByteArray) - outputStream.close() - xzedBase64 + try { + xz.write(b) + Base64.getEncoder.encodeToString(outputStream.toByteArray) + } finally { + xz.close() + outputStream.close() + } } def xzString(s: String, compressionLevel: Option[Int]): String = { @@ -51,13 +55,24 @@ object Platform { new JSONObject(yaml).toString() } - def md5(s: String): String = { - java.security.MessageDigest.getInstance("MD5") + private def computeHash(algorithm: String, s: String) = { + java.security.MessageDigest.getInstance(algorithm) .digest(s.getBytes("UTF-8")) .map { b => String.format("%02x", Integer.valueOf(b & 0xff)) } .mkString } + def md5(s: String): String = computeHash("MD5", s) + + def sha1(s: String): String = computeHash("SHA-1", s) + + def sha256(s: String): String = computeHash("SHA-256", s) + + def sha512(s: String): String = computeHash("SHA-512", s) + + // Same as go-jsonnet https://github.com/google/go-jsonnet/blob/2b4d7535f540f128e38830492e509a550eb86d57/builtins.go#L959 + def sha3(s: String): String = computeHash("SHA3-512", s) + private val xxHashFactory = XXHashFactory.fastestInstance() def hashFile(file: File): String = { @@ -78,6 +93,6 @@ object Platform { fis.close() } - hash.getValue().toString + hash.getValue.toString } } diff --git a/sjsonnet/src-native/sjsonnet/Platform.scala b/sjsonnet/src-native/sjsonnet/Platform.scala index 8e9c7d3e..0a88ce0f 100644 --- a/sjsonnet/src-native/sjsonnet/Platform.scala +++ b/sjsonnet/src-native/sjsonnet/Platform.scala @@ -19,6 +19,18 @@ object Platform { def md5(s: String): String = { throw new Exception("MD5 not implemented in Scala Native") } + def sha1(s: String): String = { + throw new Exception("SHA1 not implemented in Scala Native") + } + def sha256(s: String): String = { + throw new Exception("SHA256 not implemented in Scala Native") + } + def sha512(s: String): String = { + throw new Exception("SHA512 not implemented in Scala Native") + } + def sha3(s: String): String = { + throw new Exception("SHA3 not implemented in Scala Native") + } def hashFile(file: File): String = { // File hashes in Scala Native are just the file content diff --git a/sjsonnet/src/sjsonnet/Expr.scala b/sjsonnet/src/sjsonnet/Expr.scala index a475e4c8..1944992e 100644 --- a/sjsonnet/src/sjsonnet/Expr.scala +++ b/sjsonnet/src/sjsonnet/Expr.scala @@ -1,7 +1,6 @@ package sjsonnet -import java.util.{Arrays, BitSet} -import scala.collection.mutable +import java.util.Arrays /** * [[Expr]]s are the parsed syntax trees of a Jsonnet program. They model the diff --git a/sjsonnet/src/sjsonnet/Format.scala b/sjsonnet/src/sjsonnet/Format.scala index 134e82d5..eec5ef9e 100644 --- a/sjsonnet/src/sjsonnet/Format.scala +++ b/sjsonnet/src/sjsonnet/Format.scala @@ -102,7 +102,9 @@ object Format{ val cooked0 = formatted.conversion match{ case '%' => widenRaw(formatted, "%") case _ => - + if (values.isInstanceOf[Val.Arr] && i >= values.cast[Val.Arr].length) { + Error.fail("Too few values to format: %d, expected at least %d".format(values.cast[Val.Arr].length, i + 1)) + } val raw = formatted.label match{ case None => values.cast[Val.Arr].force(i) case Some(key) => @@ -117,9 +119,12 @@ object Format{ } i += 1 value match{ - case ujson.Str(s) => widenRaw(formatted, s) + case ujson.Str(s) => + if (formatted.conversion != 's' && formatted.conversion != 'c') + Error.fail("Format required a number at %d, got string".format(i)) + widenRaw(formatted, s) case ujson.Num(s) => - formatted.conversion match{ + formatted.conversion match { case 'd' | 'i' | 'u' => formatInteger(formatted, s) case 'o' => formatOctal(formatted, s) case 'x' => formatHexadecimal(formatted, s) @@ -133,20 +138,33 @@ object Format{ case 's' => if (s.toLong == s) widenRaw(formatted, s.toLong.toString) else widenRaw(formatted, s.toString) + case _ => Error.fail("Format required a %s at %d, got string".format(raw.prettyName, i)) + } + case ujson.Bool(s) => + formatted.conversion match { + case 'd' | 'i' | 'u' => formatInteger(formatted, s.compareTo(false)) + case 'o' => formatOctal(formatted, s.compareTo(false)) + case 'x' => formatHexadecimal(formatted, s.compareTo(false)) + case 'X' => formatHexadecimal(formatted, s.compareTo(false)).toUpperCase + case 'e' => formatExponent(formatted, s.compareTo(false)).toLowerCase + case 'E' => formatExponent(formatted, s.compareTo(false)) + case 'f' | 'F' => formatFloat(formatted, s.compareTo(false)) + case 'g' => formatGeneric(formatted, s.compareTo(false)).toLowerCase + case 'G' => formatGeneric(formatted, s.compareTo(false)) + case 'c' => widenRaw(formatted, Character.forDigit(s.compareTo(false), 10).toString) + case 's' => widenRaw(formatted, s.toString) + case _ => Error.fail("Format required a %s at %d, got string".format(raw.prettyName, i)) } - case ujson.True => widenRaw(formatted, "true") - case ujson.False => widenRaw(formatted, "false") case v => widenRaw(formatted, v.toString) } - } - output.append(cooked0) output.append(literal) - - } + if (values.isInstanceOf[Val.Arr] && i < values.cast[Val.Arr].length) { + Error.fail("Too many values to format: %d, expected %d".format(values.cast[Val.Arr].length, i)) + } output.toString() } diff --git a/sjsonnet/src/sjsonnet/Interpreter.scala b/sjsonnet/src/sjsonnet/Interpreter.scala index 74e0afb9..6de9806d 100644 --- a/sjsonnet/src/sjsonnet/Interpreter.scala +++ b/sjsonnet/src/sjsonnet/Interpreter.scala @@ -40,7 +40,7 @@ class Interpreter(extVars: Map[String, String], def parseVar(k: String, v: String) = { - resolver.parse(wd / s"<$k>", StaticResolvedFile(v))(evaluator).fold(throw _, _._1) + resolver.parse(wd / s"\uFE64$k\uFE65", StaticResolvedFile(v))(evaluator).fold(throw _, _._1) } lazy val evaluator: Evaluator = createEvaluator( diff --git a/sjsonnet/src/sjsonnet/Renderer.scala b/sjsonnet/src/sjsonnet/Renderer.scala index c3636408..dc75792e 100644 --- a/sjsonnet/src/sjsonnet/Renderer.scala +++ b/sjsonnet/src/sjsonnet/Renderer.scala @@ -191,7 +191,7 @@ class PythonRenderer(out: Writer = new java.io.StringWriter(), } } -/** Renderer used by std.manifestJson and std.manifestJsonEx */ +/** Renderer used by std.manifestJson, std.manifestJsonMinified, and std.manifestJsonEx */ case class MaterializeJsonRenderer(indent: Int = 4, escapeUnicode: Boolean = false, out: StringWriter = new StringWriter()) extends BaseCharRenderer(out, indent, escapeUnicode) { @@ -201,10 +201,9 @@ case class MaterializeJsonRenderer(indent: Int = 4, escapeUnicode: Boolean = fal depth += 1 // account for rendering differences of whitespaces in ujson and jsonnet manifestJson - if (length == 0) elemBuilder.append('\n') else renderIndent() + if (length == 0 && indent != -1) elemBuilder.append('\n') else renderIndent() def subVisitor: MaterializeJsonRenderer = MaterializeJsonRenderer.this - def visitValue(v: StringWriter, index: Int): Unit = { flushBuffer() commaBuffered = true @@ -225,12 +224,11 @@ case class MaterializeJsonRenderer(indent: Int = 4, escapeUnicode: Boolean = fal elemBuilder.append('{') depth += 1 // account for rendering differences of whitespaces in ujson and jsonnet manifestJson - if (length == 0) elemBuilder.append('\n') else renderIndent() + if (length == 0 && indent != -1) elemBuilder.append('\n') else renderIndent() def subVisitor: MaterializeJsonRenderer = MaterializeJsonRenderer.this def visitKey(index: Int): MaterializeJsonRenderer = MaterializeJsonRenderer.this - def visitKeyValue(s: Any): Unit = { elemBuilder.append(':') if (indent != -1) elemBuilder.append(' ') diff --git a/sjsonnet/src/sjsonnet/Std.scala b/sjsonnet/src/sjsonnet/Std.scala index 097a702a..e1ae89db 100644 --- a/sjsonnet/src/sjsonnet/Std.scala +++ b/sjsonnet/src/sjsonnet/Std.scala @@ -638,8 +638,7 @@ class Std { } private object SetInter extends Val.Builtin3("a", "b", "keyF", Array(null, null, Val.False(dummyPos))) { - def isStr(a: Val.Arr) = a.forall(_.isInstanceOf[Val.Str]) - def isNum(a: Val.Arr) = a.forall(_.isInstanceOf[Val.Num]) + private def isStr(a: Val.Arr) = a.forall(_.isInstanceOf[Val.Str]) override def specialize(args: Array[Expr]): (Val.Builtin, Array[Expr]) = args match { case Array(a: Val.Arr, b) if isStr(a) => (new Spec1Str(a), Array(b)) @@ -1253,9 +1252,36 @@ class Std { } }, "all" -> All, - "any" -> Any + "any" -> Any, + builtin("isEmpty", "str") { (_, _, str: String) => + str.isEmpty + }, + builtin("trim", "str") { (_, _, str: String) => + str.trim + }, + builtin("equalsIgnoreCase", "str1", "str2") { (_, _, str1: String, str2: String) => + str1.equalsIgnoreCase(str2) + }, + builtin("xor", "bool1", "bool2") { (_, _, bool1: Boolean, bool2: Boolean) => + bool1 ^ bool2 + }, + builtin("xnor", "bool1", "bool2") { (_, _, bool1: Boolean, bool2: Boolean) => + !(bool1 ^ bool2) + }, + builtin("sha1", "str") { (_, _, str: String) => + Platform.sha1(str) + }, + builtin("sha256", "str") { (_, _, str: String) => + Platform.sha256(str) + }, + builtin("sha512", "str") { (_, _, str: String) => + Platform.sha512(str) + }, + builtin("sha3", "str") { (_, _, str: String) => + Platform.sha3(str) + }, ) - val Std = Val.Obj.mk( + val Std: Val.Obj = Val.Obj.mk( null, functions.toSeq .map{ @@ -1340,10 +1366,10 @@ class Std { } } - def uniqArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { + private def uniqArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { val arrValue = arr match { - case arr: Val.Arr => arr.asLazyArray - case str: Val.Str => stringChars(pos, str.value).asLazyArray + case arr: Val.Arr => arr + case str: Val.Str => stringChars(pos, str.asString) case _ => Error.fail("Argument must be either array or string") } @@ -1377,39 +1403,50 @@ class Std { new Val.Arr(pos, out.toArray) } - def sortArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { - arr match{ - case vs: Val.Arr => - new Val.Arr( - pos, - - if (vs.forall(_.isInstanceOf[Val.Str])){ - vs.asStrictArray.map(_.cast[Val.Str]).sortBy(_.value) - }else if (vs.forall(_.isInstanceOf[Val.Num])) { - vs.asStrictArray.map(_.cast[Val.Num]).sortBy(_.value) - }else if (vs.forall(_.isInstanceOf[Val.Obj])){ - if (keyF == null || keyF.isInstanceOf[Val.False]) { - Error.fail("Unable to sort array of objects without key function") - } else { - val objs = vs.asStrictArray.map(_.cast[Val.Obj]) - - val keyFFunc = keyF.asInstanceOf[Val.Func] - val keys = objs.map((v) => keyFFunc(Array(v), null, pos.noOffset)(ev)) - - if (keys.forall(_.isInstanceOf[Val.Str])){ - objs.sortBy((v) => keyFFunc(Array(v), null, pos.noOffset)(ev).cast[Val.Str].value) - } else if (keys.forall(_.isInstanceOf[Val.Num])) { - objs.sortBy((v) => keyFFunc(Array(v), null, pos.noOffset)(ev).cast[Val.Num].value) - } else { - Error.fail("Cannot sort with key values that are " + keys(0).prettyName + "s") - } - } - }else { - ??? - } - ) - case Val.Str(pos, s) => new Val.Arr(pos, s.sorted.map(c => Val.Str(pos, c.toString)).toArray) - case x => Error.fail("Cannot sort " + x.prettyName) + private def sortArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { + val vs = arr match { + case arr: Val.Arr => arr + case str: Val.Str => stringChars(pos, str.asString) + case _ => Error.fail("Cannot sort " + arr.prettyName) + } + if (vs.length <= 1) { + arr + } else { + val keyFFunc = if (keyF == null || keyF.isInstanceOf[Val.False]) null else keyF.asInstanceOf[Val.Func] + new Val.Arr(pos, if (keyFFunc != null) { + val keys = new Val.Arr(pos.noOffset, vs.asStrictArray.map((v) => keyFFunc(Array(v), null, pos.noOffset)(ev))) + val keyTypes = keys.iterator.map(_.getClass).toSet + if (keyTypes.size != 1) { + Error.fail("Cannot sort with key values that are not all the same type") + } + + if (keyTypes.contains(classOf[Val.Str])) { + vs.asStrictArray.sortBy((v) => keyFFunc(Array(v), null, pos.noOffset)(ev).cast[Val.Str].asString) + } else if (keyTypes.contains(classOf[Val.Num])) { + vs.asStrictArray.sortBy((v) => keyFFunc(Array(v), null, pos.noOffset)(ev).cast[Val.Num].asDouble) + } else if (keyTypes.contains(classOf[Val.Bool])) { + vs.asStrictArray.sortBy((v) => keyFFunc(Array(v), null, pos.noOffset)(ev).cast[Val.Bool].asBoolean) + } else { + Error.fail("Cannot sort with key values that are " + keys.force(0).prettyName + "s") + } + } else { + val keyTypes = vs.iterator.map(_.getClass).toSet + if (keyTypes.size != 1) { + Error.fail("Cannot sort with values that are not all the same type") + } + + if (keyTypes.contains(classOf[Val.Str])) { + vs.asStrictArray.map(_.cast[Val.Str]).sortBy(_.asString) + } else if (keyTypes.contains(classOf[Val.Num])) { + vs.asStrictArray.map(_.cast[Val.Num]).sortBy(_.asDouble) + } else if (keyTypes.contains(classOf[Val.Bool])) { + vs.asStrictArray.map(_.cast[Val.Bool]).sortBy(_.asBoolean) + } else if (keyTypes.contains(classOf[Val.Obj])) { + Error.fail("Unable to sort array of objects without key function") + } else { + Error.fail("Cannot sort array of " + vs.force(0).prettyName) + } + }) } } diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index 7359aa8a..840596da 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -59,8 +59,10 @@ object PrettyNamed{ 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 boolName: PrettyNamed[Val.Bool] = new PrettyNamed("boolean") implicit def objName: PrettyNamed[Val.Obj] = new PrettyNamed("object") implicit def funName: PrettyNamed[Val.Func] = new PrettyNamed("function") + implicit def nullName: PrettyNamed[Val.Null] = new PrettyNamed("null") } object Val{ @@ -69,7 +71,7 @@ object Val{ override def asBoolean: Boolean = this.isInstanceOf[True] } - def bool(pos: Position, b: Boolean) = if (b) True(pos) else False(pos) + def bool(pos: Position, b: Boolean): Bool = if (b) True(pos) else False(pos) case class True(pos: Position) extends Bool { def prettyName = "boolean" @@ -92,11 +94,11 @@ object Val{ class Arr(val pos: Position, private val value: Array[? <: Lazy]) extends Literal { def prettyName = "array" + override def asArr: Arr = this def length: Int = value.length - def force(i: Int) = value(i).force + def force(i: Int): Val = value(i).force - def asLazy(i: Int): Lazy = value(i) def asLazyArray: Array[Lazy] = value.asInstanceOf[Array[Lazy]] def asStrictArray: Array[Val] = value.map(_.force) @@ -104,7 +106,7 @@ object Val{ new Arr(newPos, value ++ rhs.value) def iterator: Iterator[Val] = value.iterator.map(_.force) - def foreach[U](f: Val => U) = { + def foreach[U](f: Val => U): Unit = { var i = 0 while(i < value.length) { f(value(i).force) diff --git a/sjsonnet/test/src-jvm/sjsonnet/StdShasTests.scala b/sjsonnet/test/src-jvm/sjsonnet/StdShasTests.scala new file mode 100644 index 00000000..e300e485 --- /dev/null +++ b/sjsonnet/test/src-jvm/sjsonnet/StdShasTests.scala @@ -0,0 +1,22 @@ +package sjsonnet + +import sjsonnet.TestUtils.eval +import utest._ + +object StdShasTests extends TestSuite { + + def tests: Tests = Tests { + test { + eval("std.sha1('')") ==> ujson.Str("da39a3ee5e6b4b0d3255bfef95601890afd80709") + eval("std.sha256('')") ==> ujson.Str("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + eval("std.sha512('')") ==> ujson.Str("cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") + eval("std.sha3('')") ==> ujson.Str("a69f73cca23a9ac5c8b567dc185a756e97c982164fe25859e0d1dcc1475c80a615b2123af1f5f94c11e3e9402c3ac558f500199d95b6d3e301758586281dcd26") + } + test { + eval("std.sha1('foo')") ==> ujson.Str("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33") + eval("std.sha256('foo')") ==> ujson.Str("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae") + eval("std.sha512('foo')") ==> ujson.Str("f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7") + eval("std.sha3('foo')") ==> ujson.Str("4bca2b137edc580fe50a88983ef860ebaca36c857b1f492839d6d7392452a63c82cbebc68e3b70a2a1480b4bb5d437a7cba6ecf9d89f9ff3ccd14cd6146ea7e7") + } + } +} diff --git a/sjsonnet/test/src/sjsonnet/FormatTests.scala b/sjsonnet/test/src/sjsonnet/FormatTests.scala index 77dfe910..e021a777 100644 --- a/sjsonnet/test/src/sjsonnet/FormatTests.scala +++ b/sjsonnet/test/src/sjsonnet/FormatTests.scala @@ -23,6 +23,15 @@ object FormatTests extends TestSuite{ assert(formatted == expected) } + def checkErr(fmt: String, jsonStr: String, expectedErr: String) = { + try { + check(fmt, jsonStr, "") + } catch { + case e: Error => + assert(e.getMessage == expectedErr) + } + } + def tests = Tests{ test("hash"){ // # @@ -290,6 +299,10 @@ object FormatTests extends TestSuite{ // apparently you can pass in positional parameters to named interpolations check("XXX%(ignored_lols)sXXX %s", """[1.1, 2]""", "XXX1.1XXX 2") + + checkErr("%s %s %s %s %s", """["foo"]""", "Too few values to format: 1, expected at least 2") + checkErr("%s %s", """["foo", "bar", "baz"]""", "Too many values to format: 3, expected 2") + checkErr("%d", """["foo"]""", "Format required a number at 1, got string") } } } diff --git a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala index f401a372..85691505 100644 --- a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala +++ b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala @@ -1,7 +1,7 @@ package sjsonnet import utest._ -import TestUtils.eval +import TestUtils.{eval, evalErr} object Std0150FunctionsTests extends TestSuite { def tests = Tests { @@ -49,8 +49,8 @@ object Std0150FunctionsTests extends TestSuite { } test("manifestJsonMinified"){ - eval("""std.manifestJsonMinified( { x: [1, 2, 3, true, false, null, "string\nstring"], y: { a: 1, b: 2, c: [1, 2] }, })""") ==> - ujson.Str("{\"x\":[1,2,3,true,false,null,\"string\\nstring\"],\"y\":{\"a\":1,\"b\":2,\"c\":[1,2]}}") + eval("""std.manifestJsonMinified( { x: [1, 2, 3, true, false, null, "string\nstring", []], y: { a: 1, b: 2, c: [1, 2], d: {} }, })""") ==> + ujson.Str("{\"x\":[1,2,3,true,false,null,\"string\\nstring\",[]],\"y\":{\"a\":1,\"b\":2,\"c\":[1,2],\"d\":{}}}") } test("manifestXmlJsonml"){ @@ -180,5 +180,46 @@ object Std0150FunctionsTests extends TestSuite { eval("""std.all([false, true, false])""") ==> ujson.Bool(false) eval("""std.all([false, false, false])""") ==> ujson.Bool(false) } + + test("isEmpty") { + eval("""std.isEmpty("")""") ==> ujson.Bool(true) + eval("""std.isEmpty("non-empty string")""") ==> ujson.Bool(false) + assert( + evalErr("""std.isEmpty(10)""") + .startsWith("sjsonnet.Error: Wrong parameter type: expected String, got number") + ) + } + + test("trim") { + eval("""std.trim("already trimmed string")""") ==> ujson.Str("already trimmed string") + eval("""std.trim(" string with spaces on both ends ")""") ==> ujson.Str("string with spaces on both ends") + eval("""std.trim("string with newline character at end\n")""") ==> ujson.Str("string with newline character at end") + eval("""std.trim("string with tabs at end\t\t")""") ==> ujson.Str("string with tabs at end") + assert( + evalErr("""std.trim(10)""").startsWith("sjsonnet.Error: Wrong parameter type: expected String, got number")) + } + + test("xnor") { + eval("""std.xnor(false, true)""") ==> ujson.False + eval("""std.xnor(false, false)""") ==> ujson.True + assert( + evalErr("""std.xnor("false", false)""") + .startsWith("sjsonnet.Error: Wrong parameter type: expected Boolean, got string") + ) + } + + test("xor") { + eval("""std.xor(false, true)""") ==> ujson.True + eval("""std.xor(true, true)""") ==> ujson.False + assert( + evalErr("""std.xor("false", false)""") + .startsWith("sjsonnet.Error: Wrong parameter type: expected Boolean, got string") + ) + } + + test("equalsIgnoreCase") { + eval("""std.equalsIgnoreCase("hello", "HELLO")""") ==> ujson.True + eval("""std.equalsIgnoreCase("hello", "world")""") ==> ujson.False + } } } diff --git a/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala b/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala index 229632ff..1070915b 100644 --- a/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala +++ b/sjsonnet/test/src/sjsonnet/StdWithKeyFTests.scala @@ -1,7 +1,7 @@ package sjsonnet import utest._ -import TestUtils.eval +import TestUtils.{eval, evalErr} object StdWithKeyFTests extends TestSuite { def tests = Tests { @@ -47,7 +47,16 @@ object StdWithKeyFTests extends TestSuite { """) ==> ujson.True } test("stdSortWithKeyF") { - eval("std.sort([\"c\", \"a\", \"b\"])").toString() ==> """["a","b","c"]""" + eval("""std.sort(["a","b","c"])""").toString() ==> """["a","b","c"]""" + eval("""std.sort([1, 2, 3])""").toString() ==> """[1,2,3]""" + eval("""std.sort([1,2,3], keyF=function(x) -x)""").toString() ==> """[3,2,1]""" + eval("""std.sort([1,2,3], function(x) -x)""").toString() ==> """[3,2,1]""" + assert( + evalErr("""std.sort([1,2,3], keyF=function(x) error "foo")""").startsWith("sjsonnet.Error: foo")) + assert( + evalErr("""std.sort([1,2, error "foo"])""").startsWith("sjsonnet.Error: foo")) + assert( + evalErr("""std.sort([1, [error "foo"]])""").startsWith("sjsonnet.Error: Cannot sort with values that are not all the same type")) eval( """local arr = [