From 9ad1d73571b16928a8f6d6afa2f372f457aa3dc1 Mon Sep 17 00:00:00 2001 From: Daniel Slapman Date: Fri, 9 Dec 2022 15:30:47 +0100 Subject: [PATCH] Re-implement builtin pseudofunctions --- .../mockingbird/src/main/resources/prelude.js | 61 +++++++++++++++++++ .../tcb/utils/sandboxing/GraalJsSandbox.scala | 42 ++++++++----- .../utils/transformation/json/package.scala | 19 ++++++ .../tcb/utils/transformation/package.scala | 1 + .../json/JsonTransformationsSpec.scala | 56 +++++++++++++++++ .../tinkoff/tcb/utils/resource/package.scala | 16 +++++ readme.md | 6 +- 7 files changed, 182 insertions(+), 19 deletions(-) create mode 100644 backend/mockingbird/src/main/resources/prelude.js create mode 100644 backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala diff --git a/backend/mockingbird/src/main/resources/prelude.js b/backend/mockingbird/src/main/resources/prelude.js new file mode 100644 index 00000000..bb872b78 --- /dev/null +++ b/backend/mockingbird/src/main/resources/prelude.js @@ -0,0 +1,61 @@ +function randomInt(lbound, rbound) { + if (typeof rbound === "undefined") + return Math.floor(Math.random() * lbound); + var min = Math.ceil(lbound); + var max = Math.floor(rbound); + return Math.floor(Math.random() * (max - min) + min); +} + +function randomLong(lbound, rbound) { + return randomInt(lbound, rbound); +} + +function randomString(param1, param2, param3) { + if (typeof param2 === "undefined" || typeof param3 === "undefined") { + var result = ''; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + var charactersLength = characters.length; + for ( var i = 0; i < param1; i++ ) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; + } + + var result = ''; + var charactersLength = param1.length; + for ( var i = 0; i < randomInt(param2, param3); i++ ) { + result += param1.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +function randomNumericString(length) { + return randomString('0123456789', length, length + 1); +} + +// https://stackoverflow.com/a/8809472/3819595 +function UUID() { // Public Domain/MIT + var d = new Date().getTime();//Timestamp + var d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now()*1000)) || 0;//Time in microseconds since page-load or 0 if unsupported + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16;//random number between 0 and 16 + if(d > 0){//Use timestamp until depleted + r = (d + r)%16 | 0; + d = Math.floor(d/16); + } else {//Use microseconds since page-load if supported + r = (d2 + r)%16 | 0; + d2 = Math.floor(d2/16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +} + +function now(pattern) { + var format = java.time.format.DateTimeFormatter.ofPattern(pattern); + return java.time.LocalDateTime.now().format(format); +} + +function today(pattern) { + var format = java.time.format.DateTimeFormatter.ofPattern(pattern); + return java.time.LocalDate.now().format(format); +} \ No newline at end of file diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala index f99be0be..cedb9d3a 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/GraalJsSandbox.scala @@ -9,41 +9,51 @@ import org.graalvm.polyglot.* import ru.tinkoff.tcb.utils.instances.predicate.or.* -class GraalJsSandbox(classAccessRules: List[ClassAccessRule] = GraalJsSandbox.DefaultAccess) { +class GraalJsSandbox( + classAccessRules: List[ClassAccessRule] = GraalJsSandbox.DefaultAccess, + prelude: Option[String] = None +) { private val accessRule = classAccessRules.asInstanceOf[List[String => Boolean]].combineAll + private val preludeSource = prelude.map(Source.create("js", _)) + def eval[T: ClassTag](code: String, environment: Map[String, Any] = Map.empty): Try[T] = Using( Context .newBuilder("js") .allowHostAccess(HostAccess.ALL) .allowHostClassLookup((t: String) => accessRule(t)) + .option("engine.WarnInterpreterOnly", "false") .build() ) { context => context.getBindings("js").pipe { bindings => for ((key, value) <- environment) bindings.putMember(key, value) } + preludeSource.foreach(context.eval) context.eval("js", code).as(classTag[T].runtimeClass.asInstanceOf[Class[T]]) } } object GraalJsSandbox { val DefaultAccess: List[ClassAccessRule] = List( - ClassAccessRule.StartsWith("java.lang.Byte"), - ClassAccessRule.StartsWith("java.lang.Boolean"), - ClassAccessRule.StartsWith("java.lang.Double"), - ClassAccessRule.StartsWith("java.lang.Float"), - ClassAccessRule.StartsWith("java.lang.Integer"), - ClassAccessRule.StartsWith("java.lang.Long"), - ClassAccessRule.StartsWith("java.lang.Math"), - ClassAccessRule.StartsWith("java.lang.Short"), - ClassAccessRule.StartsWith("java.lang.String"), - ClassAccessRule.StartsWith("java.math.BigDecimal"), - ClassAccessRule.StartsWith("java.math.BigInteger"), - ClassAccessRule.StartsWith("java.util.List"), - ClassAccessRule.StartsWith("java.util.Map"), - ClassAccessRule.StartsWith("java.util.Random"), - ClassAccessRule.StartsWith("java.util.Set") + ClassAccessRule.Exact("java.lang.Byte"), + ClassAccessRule.Exact("java.lang.Boolean"), + ClassAccessRule.Exact("java.lang.Double"), + ClassAccessRule.Exact("java.lang.Float"), + ClassAccessRule.Exact("java.lang.Integer"), + ClassAccessRule.Exact("java.lang.Long"), + ClassAccessRule.Exact("java.lang.Math"), + ClassAccessRule.Exact("java.lang.Short"), + ClassAccessRule.Exact("java.lang.String"), + ClassAccessRule.Exact("java.math.BigDecimal"), + ClassAccessRule.Exact("java.math.BigInteger"), + ClassAccessRule.Exact("java.time.LocalDate"), + ClassAccessRule.Exact("java.time.LocalDateTime"), + ClassAccessRule.Exact("java.time.format.DateTimeFormatter"), + ClassAccessRule.Exact("java.util.List"), + ClassAccessRule.Exact("java.util.Map"), + ClassAccessRule.Exact("java.util.Random"), + ClassAccessRule.Exact("java.util.Set") ) } diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala index 515ad7f3..de4af261 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/json/package.scala @@ -1,5 +1,9 @@ package ru.tinkoff.tcb.utils.transformation +import java.lang as jl +import java.math as jm +import scala.util.Failure +import scala.util.Success import scala.util.control.TailCalls import scala.util.control.TailCalls.TailRec @@ -12,6 +16,7 @@ import ru.tinkoff.tcb.utils.circe.* import ru.tinkoff.tcb.utils.circe.optics.JsonOptic import ru.tinkoff.tcb.utils.json.json2StringFolder import ru.tinkoff.tcb.utils.regex.OneOrMore +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox import ru.tinkoff.tcb.utils.transformation.xml.nodeTemplater package object json { @@ -97,6 +102,20 @@ package object json { .getOrElse(js) }.result + def eval2(implicit sandbox: GraalJsSandbox): Json = + transformValues { + case js @ JsonString(CodeRx(code)) => + (sandbox.eval[AnyRef](code) match { + case Success(str: String) => Option(Json.fromString(str)) + case Success(bd: jm.BigDecimal) => Option(Json.fromBigDecimal(bd)) + case Success(i: jl.Integer) => Option(Json.fromInt(i.intValue())) + case Success(l: jl.Long) => Option(Json.fromLong(l.longValue())) + case Success(other) => throw new Exception(s"${other.getClass.getCanonicalName}: $other") + case Failure(exception) => throw exception + }).getOrElse(js) + case JsonString(other) => throw new Exception(other) + }.result + def patch(values: Json, schema: Map[JsonOptic, String]): Json = jsonTemplater(values).pipe { templater => schema.foldLeft(j) { case (acc, (optic, defn)) => diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala index c3a87bb2..db16b13f 100644 --- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala +++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/package.scala @@ -11,6 +11,7 @@ import ru.tinkoff.tcb.utils.time.* package object transformation { val SubstRx: Regex = """\$\{(.*?)\}""".r val FunRx: Regex = """%\{.*?\}""".r + val CodeRx: Regex = """%\{(.*?)\}""".r val RandStr: Regex = """%\{randomString\((\d+)\)\}""".r val RandAlphabetStr: Regex = """%\{randomString\(\"(.*?)\",\s*(\d+),\s*(\d+)\)\}""".r diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala index eb2ecd3a..f560d9f7 100644 --- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala +++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/json/JsonTransformationsSpec.scala @@ -13,6 +13,8 @@ import org.scalatest.matchers.should.Matchers import ru.tinkoff.tcb.utils.circe.* import ru.tinkoff.tcb.utils.circe.optics.JLens import ru.tinkoff.tcb.utils.circe.optics.JsonOptic +import ru.tinkoff.tcb.utils.resource.readStr +import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValues { test("Fill template") { @@ -186,6 +188,60 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue noException should be thrownBy dFormatter.parse(f) } + test("JavaScript eval") { + val datePattern = "yyyy-MM-dd" + val dFormatter = DateTimeFormatter.ofPattern(datePattern) + val pattern = "yyyy-MM-dd'T'HH:mm:ss" + val formatter = DateTimeFormatter.ofPattern(pattern) + + val prelude = readStr("prelude.js") + implicit val sandbox: GraalJsSandbox = new GraalJsSandbox(prelude = Option(prelude)) + + val template = Json.obj( + "a" := "%{randomString(10)}", + "ai" := "%{randomString(\"ABCDEF1234567890\", 4, 6)}", + "b" := "%{randomInt(5)}", + "bi" := "%{randomInt(3, 8)}", + "c" := "%{randomLong(5)}", + "ci" := "%{randomLong(3, 8)}", + "d" := "%{UUID()}", + "e" := s"%{now(\"$pattern\")}", + "f" := s"%{today('$datePattern')}" + ) + + val res = template.eval2 + + (res \\ "a").headOption.flatMap(_.asString).value should have length 10 + + info((res \\ "ai").headOption.flatMap(_.asString).value) + (res \\ "ai").headOption.flatMap(_.asString).value should fullyMatch regex """[ABCDEF1234567890]{4,5}""" + + val b = (res \\ "b").headOption.flatMap(_.asNumber).flatMap(_.toInt).value + b should be >= 0 + b should be < 5 + + val bi = (res \\ "bi").headOption.flatMap(_.asNumber).flatMap(_.toInt).value + bi should be >= 3 + bi should be < 8 + + val c = (res \\ "c").headOption.flatMap(_.asNumber).flatMap(_.toLong).value + c should be >= 0L + c should be < 5L + + val ci = (res \\ "ci").headOption.flatMap(_.asNumber).flatMap(_.toLong).value + ci should be >= 3L + ci should be < 8L + + val d = (res \\ "d").headOption.flatMap(_.asString).value + noException should be thrownBy UUID.fromString(d) + + val e = (res \\ "e").headOption.flatMap(_.asString).value + noException should be thrownBy formatter.parse(e) + + val f = (res \\ "f").headOption.flatMap(_.asString).value + noException should be thrownBy dFormatter.parse(f) + } + test("Formatted eval") { val template = Json.obj( "fmt" := "%{randomInt(10)}: %{randomLong(10)} | %{randomString(12)}" diff --git a/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala new file mode 100644 index 00000000..56ce9b56 --- /dev/null +++ b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/package.scala @@ -0,0 +1,16 @@ +package ru.tinkoff.tcb.utils + +import scala.io.Source +import scala.util.Using + +package object resource { + def getResPath(fileName: String): String = getClass.getResource(s"/$fileName").getPath + + def readBytes(fileName: String): Array[Byte] = { + val path = getResPath(fileName) + import java.nio.file.{Files, Paths} + Files.readAllBytes(Paths.get(path)) + } + + def readStr(fileName: String): String = Using.resource(Source.fromFile(getResPath(fileName)))(_.mkString) +} diff --git a/readme.md b/readme.md index 4bbc1dc8..697f28d6 100644 --- a/readme.md +++ b/readme.md @@ -145,9 +145,9 @@ State аккумулятивно дописывается. Разрешено п * `%{randomInt(m,n)}` - подстановка случайного Int в диапазоне [m, n) * `%{randomLong(n)}` - подстановка случайного Long в диапазоне [0, n) * `%{randomLong(m,n)}` - подстановка случайного Long в диапазоне [m, n) -* `%{UUID}` - подстановка случайного UUID -* `%{now(yyyy-MM-dd'T'HH:mm:ss)}` - текущее время в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) -* `%{today(yyyy-MM-dd)}` - текущая дата в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +* `%{UUID()}` - подстановка случайного UUID +* `%{now("yyyy-MM-dd'T'HH:mm:ss")}` - текущее время в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) +* `%{today("yyyy-MM-dd")}` - текущая дата в заданном [формате](https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) Можно определять строки со сложным форматом: `%{randomInt(10)}: %{randomLong(10)} | %{randomString(12)}`, поддерживаются все псевдофункции из списка выше