diff --git a/sjsonnet/src/sjsonnet/Util.scala b/sjsonnet/src/sjsonnet/Util.scala index f55c8e0cc..d79c8e71e 100644 --- a/sjsonnet/src/sjsonnet/Util.scala +++ b/sjsonnet/src/sjsonnet/Util.scala @@ -173,6 +173,39 @@ object Util { override def compare(x: String, y: String): Int = compareStringsByCodepoint(x, y) } + def compareJsonnetStd(v1: Val, v2: Val, ev: EvalScope): Int = { + val t1 = v1.prettyName + val t2 = v2.prettyName + if (t1 != t2) { + Error.fail("Comparison requires matching types. Got " + t1 + " and " + t2) + } + + v1 match { + case arr1: Val.Arr => + compareJsonnetStdArrays(arr1, v2.asInstanceOf[Val.Arr], ev) + case _: Val.Func | _: Val.Obj | _: Val.Bool => + Error.fail("Values of type " + t1 + " are not comparable.") + case _: Val.Null => + Error.fail("binary operator < does not operate on null.") + case _ => + val cmp = ev.compare(v1, v2) + if (cmp < 0) -1 else if (cmp > 0) 1 else 0 + } + } + + def compareJsonnetStdArrays(arr1: Val.Arr, arr2: Val.Arr, ev: EvalScope): Int = { + val len1 = arr1.length + val len2 = arr2.length + val minLen = math.min(len1, len2) + var i = 0 + while (i < minLen) { + val cmp = compareJsonnetStd(arr1.eval(i).value, arr2.eval(i).value, ev) + if (cmp != 0) return cmp + i += 1 + } + java.lang.Integer.compare(len1, len2) + } + val isWindows: Boolean = { // This is normally non-null on the JVM, but it might be null in ScalaJS hence the Option: Option(System.getProperty("os.name")).exists(_.toLowerCase.startsWith("windows")) diff --git a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala index 51780b1bc..726ade9ac 100644 --- a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala @@ -8,34 +8,43 @@ import scala.collection.mutable object ArrayModule extends AbstractFunctionModule { def name = "array" + private val DefaultKeyF = Val.Null(dummyPos) + private val DefaultOnEmpty = Val.Null(dummyPos) + + @inline private def isDefaultKeyF(v: Val): Boolean = v.asInstanceOf[AnyRef] eq DefaultKeyF + @inline private def isDefaultOnEmpty(v: Val): Boolean = + v.asInstanceOf[AnyRef] eq DefaultOnEmpty + + private def applyArrayKey(keyF: Val, elem: Val, pos: Position, ev: EvalScope): Val = + if (isDefaultKeyF(keyF)) elem + else keyF.asFunc.apply1(elem, pos.noOffset)(ev, TailstrictModeDisabled) + private object MinArray extends Val.Builtin( "minArray", Array("arr", "keyF", "onEmpty"), - Array(null, Val.False(dummyPos), Val.False(dummyPos)) + Array(null, DefaultKeyF, DefaultOnEmpty) ) { override def evalRhs(args: Array[? <: Eval], ev: EvalScope, pos: Position): Val = { val arr = args(0).value.asArr val keyF = args(1).value - val onEmpty = args(2) + val onEmpty = args(2).value if (arr.length == 0) { - if (onEmpty.value.isInstanceOf[Val.False]) { + if (isDefaultOnEmpty(onEmpty)) { Error.fail("Expected at least one element in array. Got none") } else { - onEmpty.value + onEmpty } - } else if (keyF.isInstanceOf[Val.False]) { - arr.asStrictArray.min(ev) } else { val strict = arr.asStrictArray - val func = keyF.asInstanceOf[Val.Func] var bestIdx = 0 - var bestVal = func.apply1(strict(0), pos.fileScope.noOffsetPos)(ev, TailstrictModeDisabled) + var bestKey = applyArrayKey(keyF, strict(0), pos, ev) + Util.compareJsonnetStd(bestKey, bestKey, ev) var i = 1 while (i < strict.length) { - val v = func.apply1(strict(i), pos.fileScope.noOffsetPos)(ev, TailstrictModeDisabled) - if (ev.compare(v, bestVal) < 0) { - bestVal = v + val v = applyArrayKey(keyF, strict(i), pos, ev) + if (Util.compareJsonnetStd(v, bestKey, ev) < 0) { + bestKey = v bestIdx = i } i += 1 @@ -49,30 +58,28 @@ object ArrayModule extends AbstractFunctionModule { extends Val.Builtin( "maxArray", Array("arr", "keyF", "onEmpty"), - Array(null, Val.False(dummyPos), Val.False(dummyPos)) + Array(null, DefaultKeyF, DefaultOnEmpty) ) { override def evalRhs(args: Array[? <: Eval], ev: EvalScope, pos: Position): Val = { val arr = args(0).value.asArr val keyF = args(1).value - val onEmpty = args(2) + val onEmpty = args(2).value if (arr.length == 0) { - if (onEmpty.value.isInstanceOf[Val.False]) { + if (isDefaultOnEmpty(onEmpty)) { Error.fail("Expected at least one element in array. Got none") } else { - onEmpty.value + onEmpty } - } else if (keyF.isInstanceOf[Val.False]) { - arr.asStrictArray.max(ev) } else { val strict = arr.asStrictArray - val func = keyF.asInstanceOf[Val.Func] var bestIdx = 0 - var bestVal = func.apply1(strict(0), pos.fileScope.noOffsetPos)(ev, TailstrictModeDisabled) + var bestKey = applyArrayKey(keyF, strict(0), pos, ev) + Util.compareJsonnetStd(bestKey, bestKey, ev) var i = 1 while (i < strict.length) { - val v = func.apply1(strict(i), pos.fileScope.noOffsetPos)(ev, TailstrictModeDisabled) - if (ev.compare(v, bestVal) > 0) { - bestVal = v + val v = applyArrayKey(keyF, strict(i), pos, ev) + if (Util.compareJsonnetStd(v, bestKey, ev) > 0) { + bestKey = v bestIdx = i } i += 1 @@ -205,7 +212,10 @@ object ArrayModule extends AbstractFunctionModule { private object MapWithIndex extends Val.Builtin2("mapWithIndex", "func", "arr") { def evalRhs(_func: Eval, _arr: Eval, ev: EvalScope, pos: Position): Val = { val func = _func.value.asFunc - val arr = _arr.value.asArr.asLazyArray + val arr = _arr.value match { + case Val.Str(_, str) => stringChars(pos, str).asLazyArray + case v => v.asArr.asLazyArray + } val a = new Array[Eval](arr.length) val noOff = pos.noOffset var i = 0 @@ -243,9 +253,9 @@ object ArrayModule extends AbstractFunctionModule { out.sizeHint(arr.length * 4) // Rough size hint for (x <- arr) { x.value match { - case Val.Null(_) => // do nothing - case v: Val.Arr => out ++= v.asLazyArray - case x => Error.fail("Cannot call flattenArrays on " + x) + case v: Val.Arr => out ++= v.asLazyArray + case x => + Error.fail("binary operator + requires matching types, got array and " + x.prettyName) } } Val.Arr(pos, out.result()) @@ -254,7 +264,11 @@ object ArrayModule extends AbstractFunctionModule { private object FlattenDeepArrays extends Val.Builtin1("flattenDeepArray", "value") { def evalRhs(value: Eval, ev: EvalScope, pos: Position): Val = { - val lazyArray = value.value.asArr.asLazyArray + val value0 = value.value + if (!value0.isInstanceOf[Val.Arr]) { + return Val.Arr(pos, Array[Eval](value0)) + } + val lazyArray = value0.asInstanceOf[Val.Arr].asLazyArray val out = new mutable.ArrayBuilder.ofRef[Eval] out.sizeHint(lazyArray.length) val q = new java.util.ArrayDeque[Eval](lazyArray.length) @@ -653,6 +667,9 @@ object ArrayModule extends AbstractFunctionModule { Val.Arr(pos, b.result()) }, builtin("repeat", "what", "count") { (pos, ev, what: Val, count: Int) => + if (count < 0) { + Error.fail("makeArray requires size >= 0, got " + count) + } val res: Val = what match { case Val.Str(_, str) => val builder = new StringBuilder(str.length * count) @@ -726,14 +743,20 @@ object ArrayModule extends AbstractFunctionModule { ) } }, - builtin("removeAt", "arr", "idx") { (_, _, arr: Val.Arr, idx: Int) => - if (!(0 <= idx && idx < arr.length)) { - Error.fail("index out of bounds: 0 <= " + idx + " < " + arr.length) + builtin("removeAt", "arr", "idx") { (_, _, arr: Val.Arr, idx: Val) => + val removeIdx = idx match { + case n: Val.Num => + val d = n.asDouble + if (d.isWhole && d >= 0 && d < arr.length) d.toInt else -1 + case _ => -1 + } + if (removeIdx == -1) arr + else { + Val.Arr( + arr.pos, + arr.asLazyArray.slice(0, removeIdx) ++ arr.asLazyArray.slice(removeIdx + 1, arr.length) + ) } - Val.Arr( - arr.pos, - arr.asLazyArray.slice(0, idx) ++ arr.asLazyArray.slice(idx + 1, arr.length) - ) }, builtin("sum", "arr") { (_, _, arr: Val.Arr) => val a = arr.asLazyArray diff --git a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala index e78b9fe7f..bbba12dd5 100644 --- a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala @@ -9,18 +9,19 @@ import scala.collection.Searching.* object SetModule extends AbstractFunctionModule { def name = "set" - private object Set_ extends Val.Builtin2("set", "arr", "keyF", Array(null, Val.False(dummyPos))) { + private val DefaultKeyF = Val.Null(dummyPos) + + @inline private def isDefaultKeyF(v: Val): Boolean = v.asInstanceOf[AnyRef] eq DefaultKeyF + + private object Set_ extends Val.Builtin2("set", "arr", "keyF", Array(null, DefaultKeyF)) { def evalRhs(arr: Eval, keyF: Eval, ev: EvalScope, pos: Position): Val = { uniqArr(pos, ev, sortArr(pos, ev, arr.value, keyF.value), keyF.value) } } private def applyKeyFunc(elem: Val, keyF: Val, pos: Position, ev: EvalScope): Val = { - keyF match { - case keyFFunc: Val.Func => - keyFFunc.apply1(elem, pos.noOffset)(ev, TailstrictModeDisabled).value - case _ => elem - } + if (isDefaultKeyF(keyF)) elem + else keyF.asFunc.apply1(elem, pos.noOffset)(ev, TailstrictModeDisabled).value } private def toArrOrString(arg: Val, pos: Position, ev: EvalScope) = { @@ -46,6 +47,7 @@ object SetModule extends AbstractFunctionModule { keyF: Val, arr: mutable.IndexedSeq[? <: Eval], toFind: Val): Boolean = { + if (arr.isEmpty) return false val appliedX = applyKeyFunc(toFind, keyF, pos, ev) arr .search(appliedX)((toFind: Eval, value: Eval) => { @@ -70,8 +72,8 @@ object SetModule extends AbstractFunctionModule { while (i < arrValue.length) { val v = arrValue(i) val vKey = - if (keyF.isInstanceOf[Val.False]) v.value - else keyF.asInstanceOf[Val.Func].apply1(v, pos.noOffset)(ev, TailstrictModeDisabled) + if (isDefaultKeyF(keyF)) v.value + else keyF.asFunc.apply1(v, pos.noOffset)(ev, TailstrictModeDisabled) if (lastAddedKey == null || !ev.equal(vKey, lastAddedKey)) { out.+=(v) lastAddedKey = vKey @@ -88,7 +90,7 @@ object SetModule extends AbstractFunctionModule { arr } else { val keyFFunc = - if (keyF == null || keyF.isInstanceOf[Val.False]) null else keyF.asInstanceOf[Val.Func] + if (keyF == null || isDefaultKeyF(keyF)) null else keyF.asFunc Val.Arr( pos, if (keyFFunc != null) { @@ -211,13 +213,13 @@ object SetModule extends AbstractFunctionModule { (pos, ev, indexable: Val, index: Option[Int], _end: Option[Int], _step: Option[Int]) => Util.slice(pos, ev, indexable, index, _end, _step) }, - builtinWithDefaults("uniq", "arr" -> null, "keyF" -> Val.False(dummyPos)) { (args, pos, ev) => + builtinWithDefaults("uniq", "arr" -> null, "keyF" -> DefaultKeyF) { (args, pos, ev) => uniqArr(pos, ev, args(0), args(1)) }, - builtinWithDefaults("sort", "arr" -> null, "keyF" -> Val.False(dummyPos)) { (args, pos, ev) => + builtinWithDefaults("sort", "arr" -> null, "keyF" -> DefaultKeyF) { (args, pos, ev) => sortArr(pos, ev, args(0), args(1)) }, - builtinWithDefaults("setUnion", "a" -> null, "b" -> null, "keyF" -> Val.False(dummyPos)) { + builtinWithDefaults("setUnion", "a" -> null, "b" -> null, "keyF" -> DefaultKeyF) { (args, pos, ev) => val keyF = args(2) validateSet(ev, pos, keyF, args(0)) @@ -274,7 +276,7 @@ object SetModule extends AbstractFunctionModule { Val.Arr(pos, out.result()) } }, - builtinWithDefaults("setInter", "a" -> null, "b" -> null, "keyF" -> Val.False(dummyPos)) { + builtinWithDefaults("setInter", "a" -> null, "b" -> null, "keyF" -> DefaultKeyF) { (args, pos, ev) => val keyF = args(2) validateSet(ev, pos, keyF, args(0)) @@ -314,7 +316,7 @@ object SetModule extends AbstractFunctionModule { Val.Arr(pos, out.result()) }, - builtinWithDefaults("setDiff", "a" -> null, "b" -> null, "keyF" -> Val.False(dummyPos)) { + builtinWithDefaults("setDiff", "a" -> null, "b" -> null, "keyF" -> DefaultKeyF) { (args, pos, ev) => val keyF = args(2) validateSet(ev, pos, keyF, args(0)) @@ -362,7 +364,7 @@ object SetModule extends AbstractFunctionModule { Val.Arr(pos, out.result()) }, - builtinWithDefaults("setMember", "x" -> null, "arr" -> null, "keyF" -> Val.False(dummyPos)) { + builtinWithDefaults("setMember", "x" -> null, "arr" -> null, "keyF" -> DefaultKeyF) { (args, pos, ev) => val keyF = args(2) validateSet(ev, pos, keyF, args(1)) diff --git a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala index f4a7c9762..c87d3b54c 100644 --- a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala @@ -431,6 +431,12 @@ object StringModule extends AbstractFunctionModule { } } + private def stdToString(v: Val)(implicit ev: EvalScope): String = + v match { + case Val.Str(_, s) => s + case other => Materializer.stringify(other)(ev) + } + private def stringChars(pos: Position, str: String): Val.Arr = { val chars = new Array[Eval](str.codePointCount(0, str.length)) var charIndex = 0 @@ -460,6 +466,23 @@ object StringModule extends AbstractFunctionModule { builtin(Split), builtin(SplitLimit), builtin(SplitLimitR), + builtin("resolvePath", "f", "r") { (_, _, f: String, r: String) => + val parts = f.split("/", -1) + val prefixCount = parts.length - 1 + if (prefixCount <= 0) r + else { + val out = new java.lang.StringBuilder(f.length + r.length) + var i = 0 + while (i < prefixCount) { + if (i > 0) out.append('/') + out.append(parts(i)) + i += 1 + } + out.append('/') + out.append(r) + out.toString + } + }, builtin(StringChars), builtin(ParseInt), builtin(ParseOctal), @@ -493,8 +516,15 @@ object StringModule extends AbstractFunctionModule { } } }, - builtin("isEmpty", "str") { (_, _, str: String) => - str.isEmpty + builtin("isEmpty", "str") { (_, _, value: Val) => + value match { + case Val.Str(_, s) => s.isEmpty + case a: Val.Arr => a.length == 0 + case o: Val.Obj => o.visibleKeyNames.isEmpty + case f: Val.Func => f.params.names.isEmpty + case x => + Error.fail("length operates on strings, objects, and arrays, got " + x.prettyName) + } }, builtin("trim", "str") { (_, _, str: String) => StripUtils.unspecializedStrip(str, whiteSpaces, true, true) @@ -511,16 +541,17 @@ object StringModule extends AbstractFunctionModule { out.toString } }, - builtin("escapeStringPython", "str") { (pos, ev, str: String) => + builtin("escapeStringPython", "str") { (pos, ev, str: Val) => val out = new java.io.StringWriter() - BaseRenderer.escape(out, str, unicode = true) + BaseRenderer.escape(out, stdToString(str)(ev), unicode = true) out.toString }, - builtin("escapeStringXML", "str") { (_, _, str: String) => + builtin("escapeStringXML", "str") { (_, ev, str: Val) => + val string = stdToString(str)(ev) val out = new java.io.StringWriter() var i = 0 - while (i < str.length) { - str.charAt(i) match { + while (i < string.length) { + string.charAt(i) match { case '<' => out.write("<") case '>' => out.write(">") case '&' => out.write("&") @@ -532,11 +563,11 @@ object StringModule extends AbstractFunctionModule { } out.toString }, - builtin("escapeStringBash", "str_") { (pos, ev, str: String) => - "'" + str.replace("'", """'"'"'""") + "'" + builtin("escapeStringBash", "str_") { (pos, ev, str: Val) => + "'" + stdToString(str)(ev).replace("'", """'"'"'""") + "'" }, - builtin("escapeStringDollars", "str_") { (pos, ev, str: String) => - str.replace("$", "$$") + builtin("escapeStringDollars", "str_") { (pos, ev, str: Val) => + stdToString(str)(ev).replace("$", "$$") } ) } diff --git a/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala b/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala index 9da70bbc5..eac2cf219 100644 --- a/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/TypeModule.scala @@ -60,6 +60,19 @@ object TypeModule extends AbstractFunctionModule { } } + private object Compare extends Val.Builtin2("__compare", "v1", "v2") { + def evalRhs(v1: Eval, v2: Eval, ev: EvalScope, pos: Position): Val = + Val.cachedNum(pos, Util.compareJsonnetStd(v1.value, v2.value, ev).toDouble) + } + + private object CompareArray extends Val.Builtin2("__compare_array", "arr1", "arr2") { + def evalRhs(arr1: Eval, arr2: Eval, ev: EvalScope, pos: Position): Val = + Val.cachedNum( + pos, + Util.compareJsonnetStdArrays(arr1.value.asArr, arr2.value.asArr, ev).toDouble + ) + } + val functions: Seq[(String, Val.Func)] = Seq( builtin(AssertEqual), builtin(IsString), @@ -94,6 +107,20 @@ object TypeModule extends AbstractFunctionModule { ) } } + }, + builtin(Compare), + builtin(CompareArray), + builtin("__array_less", "arr1", "arr2") { (_, ev, arr1: Val.Arr, arr2: Val.Arr) => + Util.compareJsonnetStdArrays(arr1, arr2, ev) == -1 + }, + builtin("__array_greater", "arr1", "arr2") { (_, ev, arr1: Val.Arr, arr2: Val.Arr) => + Util.compareJsonnetStdArrays(arr1, arr2, ev) == 1 + }, + builtin("__array_less_or_equal", "arr1", "arr2") { (_, ev, arr1: Val.Arr, arr2: Val.Arr) => + Util.compareJsonnetStdArrays(arr1, arr2, ev) <= 0 + }, + builtin("__array_greater_or_equal", "arr1", "arr2") { (_, ev, arr1: Val.Arr, arr2: Val.Arr) => + Util.compareJsonnetStdArrays(arr1, arr2, ev) >= 0 } ) } diff --git a/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden index 7208b6c5a..f38106a09 100644 --- a/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden @@ -1,3 +1,2 @@ -sjsonnet.Error: [std.isEmpty] Wrong parameter type: expected String, got number +sjsonnet.Error: [std.isEmpty] length operates on strings, objects, and arrays, got number at [].(builtinIsEmpty2.jsonnet:1:12) - diff --git a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala index ab906b7db..27804cca9 100644 --- a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala +++ b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala @@ -197,7 +197,7 @@ object Std0150FunctionsTests extends TestSuite { assert( evalErr("""std.isEmpty(10)""") .startsWith( - "sjsonnet.Error: [std.isEmpty] Wrong parameter type: expected String, got number" + "sjsonnet.Error: [std.isEmpty] length operates on strings, objects, and arrays, got number" ) ) } diff --git a/sjsonnet/test/src/sjsonnet/StdLibOfficialCompatibilityTests.scala b/sjsonnet/test/src/sjsonnet/StdLibOfficialCompatibilityTests.scala new file mode 100644 index 000000000..12fb7b2ea --- /dev/null +++ b/sjsonnet/test/src/sjsonnet/StdLibOfficialCompatibilityTests.scala @@ -0,0 +1,110 @@ +package sjsonnet + +import utest._ +import TestUtils.{eval, evalErr} + +object StdLibOfficialCompatibilityTests extends TestSuite { + def tests: Tests = Tests { + test("mapWithIndex accepts strings") { + eval("""std.mapWithIndex(function(i, x) i + std.codepoint(x), "ab")""") ==> + ujson.Arr(97, 99) + } + + test("flatten arrays follows std.jsonnet fold semantics") { + eval("""std.flattenArrays([[1], [], [2]])""") ==> ujson.Arr(1, 2) + assert(evalErr("""std.flattenArrays([[1], null, [2]])""").contains("array and null")) + } + + test("flattenDeepArray wraps scalar values") { + eval("""std.flattenDeepArray(1)""") ==> ujson.Arr(1) + eval("""std.flattenDeepArray(null)""") ==> ujson.Arr(ujson.Null) + eval("""std.flattenDeepArray([1, [2, [3]]])""") ==> ujson.Arr(1, 2, 3) + } + + test("repeat reports official negative count error") { + assert(evalErr("""std.repeat("a", -1)""").contains("makeArray requires size >= 0, got -1")) + } + + test("removeAt filters by exact index equality") { + eval("""std.removeAt([1, 2, 3], 1)""") ==> ujson.Arr(1, 3) + eval("""std.removeAt([1, 2, 3], 1.5)""") ==> ujson.Arr(1, 2, 3) + eval("""std.removeAt([1, 2, 3], -1)""") ==> ujson.Arr(1, 2, 3) + eval("""std.removeAt([1, 2, 3], 9)""") ==> ujson.Arr(1, 2, 3) + eval("""std.removeAt([1, 2, 3], "1")""") ==> ujson.Arr(1, 2, 3) + } + + test("isEmpty delegates to std.length") { + eval("""std.isEmpty("")""") ==> ujson.True + eval("""std.isEmpty([])""") ==> ujson.True + eval("""std.isEmpty({})""") ==> ujson.True + eval("""std.isEmpty(function() 1)""") ==> ujson.True + eval("""std.isEmpty(function(a, b) a)""") ==> ujson.False + assert(evalErr("""std.isEmpty(10)""").contains("length operates on strings")) + } + + test("escape string helpers stringify non-string inputs") { + eval("""std.escapeStringPython(10)""") ==> ujson.Str("\"10\"") + eval("""std.escapeStringBash(10)""") ==> ujson.Str("'10'") + eval("""std.escapeStringDollars("$a")""") ==> ujson.Str("$$a") + eval("""std.escapeStringDollars(10)""") ==> ujson.Str("10") + eval("""std.escapeStringXML(10)""") ==> ujson.Str("10") + } + + test("resolvePath is available") { + eval("""std.resolvePath("a/b/c.jsonnet", "d.jsonnet")""") ==> ujson.Str("a/b/d.jsonnet") + eval("""std.resolvePath("c.jsonnet", "d.jsonnet")""") ==> ujson.Str("d.jsonnet") + eval("""std.resolvePath("/c.jsonnet", "d.jsonnet")""") ==> ujson.Str("/d.jsonnet") + } + + test("hidden comparison helpers match std.jsonnet") { + eval( + """[ + | std.__compare(1, 2), + | std.__compare("b", "a"), + | std.__compare([1], [1, 2]), + | std.__array_less([1], [2]), + | std.__array_greater_or_equal([1, 2], [1]), + |] + |""".stripMargin + ) ==> ujson.Arr(-1, 1, -1, true, true) + assert(evalErr("""std.__compare(false, false)""").contains("boolean")) + assert(evalErr("""std.__compare(null, null)""").contains("null")) + } + + test("minArray and maxArray distinguish explicit false from defaults") { + eval("""[std.minArray([], onEmpty=false), std.maxArray([], onEmpty=false)]""") ==> + ujson.Arr(false, false) + assert(evalErr("""std.minArray([1], keyF=false)""").contains("boolean")) + assert(evalErr("""std.maxArray([1], keyF=false)""").contains("boolean")) + assert(evalErr("""std.minArray([true])""").contains("boolean")) + assert(evalErr("""std.maxArray([null])""").contains("null")) + } + + test("set keyF defaults do not collide with explicit false") { + eval( + """{ + | sortOne: std.sort([1], keyF=false), + | uniqOne: std.uniq([1], keyF=false), + | setOne: std.set([1], keyF=false), + | setUnionEmpty: std.setUnion([], [1], keyF=false), + | setInterEmpty: std.setInter([], [1], keyF=false), + | setDiffEmpty: std.setDiff([], [1], keyF=false), + | setMemberEmpty: std.setMember(1, [], keyF=false), + |} + |""".stripMargin + ) ==> ujson.Obj( + "sortOne" -> ujson.Arr(1), + "uniqOne" -> ujson.Arr(1), + "setOne" -> ujson.Arr(1), + "setUnionEmpty" -> ujson.Arr(1), + "setInterEmpty" -> ujson.Arr(), + "setDiffEmpty" -> ujson.Arr(), + "setMemberEmpty" -> ujson.False + ) + assert(evalErr("""std.sort([2, 1], keyF=false)""").contains("boolean")) + assert(evalErr("""std.uniq([1, 1], keyF=false)""").contains("boolean")) + assert(evalErr("""std.setUnion([1], [2], keyF=false)""").contains("boolean")) + assert(evalErr("""std.setMember(1, [1], keyF=false)""").contains("boolean")) + } + } +}