Skip to content

Commit

Permalink
Re-implement builtin pseudofunctions
Browse files Browse the repository at this point in the history
  • Loading branch information
danslapman committed Dec 9, 2022
1 parent 59f977b commit 9ad1d73
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 19 deletions.
61 changes: 61 additions & 0 deletions backend/mockingbird/src/main/resources/prelude.js
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 {
Expand Down Expand Up @@ -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)) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down Expand Up @@ -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)}"
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`, поддерживаются все псевдофункции из списка выше

Expand Down

0 comments on commit 9ad1d73

Please sign in to comment.