diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala
index a1feb3e2..24b745c1 100644
--- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala
+++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/api/PublicApiHandler.scala
@@ -132,8 +132,8 @@ final class PublicApiHandler(
stub.response
.applyIf(_.isTemplate)(
HttpStubResponse.jsonBody
- .updateF(_.substitute(data).substitute(xdata))
- .andThen(HttpStubResponse.xmlBody.updateF(_.substitute(data).substitute(xdata)))
+ .updateF(_.substitute(data).map(_.substitute(xdata)).use(_.pure[Id]))
+ .andThen(HttpStubResponse.xmlBody.updateF(_.substitute(data).map(_.substitute(xdata)).useAsIs))
)
.applyIf(HttpStubResponse.headers.getOption(_).exists(_.values.exists(_.isTemplate)))(
HttpStubResponse.headers.updateF(_.view.mapValues(_.substitute(data, xdata)).toMap)
@@ -250,7 +250,7 @@ final class PublicApiHandler(
.filterNot(h => proxyConfig.excludedResponseHeaders(h.name))
.map(h => h.name -> h.value)
.toMap,
- jsonResponse.patch(data, patch).noSpaces,
+ jsonResponse.patch(data, patch).use(_.noSpaces),
delay
)
case Left(error) =>
diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala
index 1b3faaab..dba0d435 100644
--- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala
+++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/grpc/GrpcRequestHandler.scala
@@ -62,13 +62,13 @@ class GrpcRequestHandlerImpl(
response <- stub.response match {
case FillResponse(rdata, delay) =>
ZIO.when(delay.isDefined)(ZIO.sleep(Duration.fromScala(delay.get))) *>
- ZIO.attemptBlocking(responseSchema.parseFromJson(rdata.substitute(data), stub.responseClass))
+ ZIO.attemptBlocking(responseSchema.parseFromJson(rdata.substitute(data).useAsIs, stub.responseClass))
case GProxyResponse(endpoint, patch, delay) =>
for {
_ <- ZIO.when(delay.isDefined)(ZIO.sleep(Duration.fromScala(delay.get)))
binaryResp <- proxyCall(endpoint, bytes)
jsonResp <- responseSchema.convertMessageToJson(binaryResp, stub.responseClass)
- patchedJsonResp = jsonResp.patch(data, patch)
+ patchedJsonResp = jsonResp.patch(data, patch).useAsIs
patchedBinaryResp = responseSchema.parseFromJson(patchedJsonResp, stub.responseClass)
} yield patchedBinaryResp
}
diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala
index cb159a34..e51c7c05 100644
--- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala
+++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/misc/Substitute.scala
@@ -19,11 +19,11 @@ import ru.tinkoff.tcb.utils.transformation.xml.*
object Substitute {
implicit def jsonSJson(implicit sandbox: GraalJsSandbox): Substitute[Json, Json] = (a: Json, b: Json) =>
- a.substitute(b)
+ a.substitute(b).useAsIs
implicit def jsonSNode(implicit sandbox: GraalJsSandbox): Substitute[Json, KNode] = (a: Json, b: KNode) =>
a.substitute(b)
implicit def nodeSJson(implicit sandbox: GraalJsSandbox): Substitute[Node, Json] = (a: Node, b: Json) =>
- a.substitute(b)
+ a.substitute(b).useAsIs
implicit def nodeSnode(implicit sandbox: GraalJsSandbox): Substitute[Node, KNode] = (a: Node, b: KNode) =>
a.substitute(b)
}
diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala
index 1db58341..7b461b6d 100644
--- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala
+++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/mockingbird/scenario/ScenarioEngine.scala
@@ -141,9 +141,9 @@ final class ScenarioEngine(
.method(Method(request.method.entryName), uri"$requestUrl")
.pipe(r =>
request match {
- case JsonCallbackRequest(_, _, _, body) => r.body(body.substitute(data).substitute(xdata).noSpaces)
+ case JsonCallbackRequest(_, _, _, body) => r.body(body.substitute(data).map(_.substitute(xdata)).use(_.noSpaces))
case XMLCallbackRequest(_, _, _, body) =>
- r.body(body.toNode.substitute(data).substitute(xdata).mkString)
+ r.body(body.toNode.substitute(data).map(_.substitute(xdata)).use(_.mkString))
case _ => r
}
)
@@ -178,18 +178,18 @@ final class ScenarioEngine(
out match {
case RawOutput(payload, _) => payload
case JsonOutput(payload, _, isT) =>
- if (isT) payload.substitute(data).substitute(xdata).noSpaces else payload.noSpaces
+ if (isT) payload.substitute(data).map(_.substitute(xdata)).use(_.noSpaces) else payload.noSpaces
case XmlOutput(payload, _, isT) =>
- if (isT) payload.toNode.substitute(data).substitute(xdata).mkString else payload.asString
+ if (isT) payload.toNode.substitute(data).map(_.substitute(xdata)).use(_.mkString) else payload.asString
}
)
} { drb =>
val bodyJson = out match {
case RawOutput(payload, _) => Json.fromString(payload)
- case JsonOutput(payload, _, isT) => if (isT) payload.substitute(data).substitute(xdata) else payload
+ case JsonOutput(payload, _, isT) => if (isT) payload.substitute(data).map(_.substitute(xdata)).useAsIs else payload
case XmlOutput(payload, _, isT) =>
if (isT)
- Json.fromString(payload.toNode.substitute(data).substitute(xdata).mkString)
+ Json.fromString(payload.toNode.substitute(data).map(_.substitute(xdata)).use(_.mkString))
else Json.fromString(payload.asString)
}
@@ -201,7 +201,7 @@ final class ScenarioEngine(
"_message" := bodyJson.asString.map(b64Enc).getOrElse(b64Enc(bodyJson.noSpaces))
)
else Json.obj("_message" := bodyJson)
- )
+ ).useAsIs
)
}
)
diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CodeRunner.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CodeRunner.scala
new file mode 100644
index 00000000..b45571de
--- /dev/null
+++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/sandboxing/CodeRunner.scala
@@ -0,0 +1,9 @@
+package ru.tinkoff.tcb.utils.sandboxing
+
+import io.circe.Json
+
+import scala.util.Try
+
+trait CodeRunner {
+ def eval(code: String): Try[Json]
+}
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 6b106b3c..3a4d655e 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
@@ -2,11 +2,13 @@ package ru.tinkoff.tcb.utils.sandboxing
import scala.util.Try
import scala.util.Using
+import scala.util.chaining.*
import io.circe.Json
import org.graalvm.polyglot.*
import ru.tinkoff.tcb.mockingbird.config.JsSandboxConfig
+import ru.tinkoff.tcb.utils.resource.Resource
import ru.tinkoff.tcb.utils.sandboxing.conversion.*
class GraalJsSandbox(
@@ -32,6 +34,22 @@ class GraalJsSandbox(
preludeSource.foreach(context.eval)
context.eval("js", code).toJson
}.flatten
+
+ def makeRunner(environment: Map[String, GValue]): Resource[CodeRunner] =
+ Resource.make(
+ Context
+ .newBuilder("js")
+ .allowHostAccess(HostAccess.ALL)
+ .allowHostClassLookup((t: String) => allowedClasses(t))
+ .option("engine.WarnInterpreterOnly", "false")
+ .build().tap { context =>
+ context.getBindings("js").pipe { bindings =>
+ for ((key, value) <- environment.view.mapValues(_.unwrap))
+ bindings.putMember(key, value)
+ }
+ preludeSource.foreach(context.eval)
+ }
+ )(_.close()).map(ctx => (code: String) => ctx.eval("js", code).toJson)
}
object GraalJsSandbox {
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 1eff3b94..d50a0baf 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
@@ -13,6 +13,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.resource.Resource
import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox
import ru.tinkoff.tcb.utils.sandboxing.conversion.circe2js
import ru.tinkoff.tcb.utils.transformation.xml.nodeTemplater
@@ -31,32 +32,34 @@ package object json {
}
}
- def jsonTemplater(values: Json)(implicit sandbox: GraalJsSandbox): PartialFunction[String, Json] = {
+ def jsonTemplater(values: Json)(implicit sandbox: GraalJsSandbox): Resource[PartialFunction[String, Json]] = {
lazy val jsData = values.asObject.map(_.toMap.view.mapValues(_.foldWith(circe2js)).toMap).getOrElse(Map())
- {
- case JOptic(None, optic) if optic.validate(values) =>
- optic.get(values)
- case JOptic(Some(":"), optic) if optic.validate(values) =>
- optic.get(values).pipe(j => castToString.applyOrElse(j, (_: Json) => j))
- case JOptic(Some("~"), optic) if optic.validate(values) =>
- optic.get(values).pipe(j => castFromString.applyOrElse(j, (_: Json) => j))
- case str @ JORxs() =>
- Json.fromString(
- JORx.replaceSomeIn(
- str,
- m =>
- JsonOptic
- .fromPathString(m.group(2))
- .getOpt(values)
- .map(_.foldWith(json2StringFolder))
+ sandbox.makeRunner(jsData).map[PartialFunction[String, Json]] { runner =>
+ {
+ case JOptic(None, optic) if optic.validate(values) =>
+ optic.get(values)
+ case JOptic(Some(":"), optic) if optic.validate(values) =>
+ optic.get(values).pipe(j => castToString.applyOrElse(j, (_: Json) => j))
+ case JOptic(Some("~"), optic) if optic.validate(values) =>
+ optic.get(values).pipe(j => castFromString.applyOrElse(j, (_: Json) => j))
+ case str @ JORxs() =>
+ Json.fromString(
+ JORx.replaceSomeIn(
+ str,
+ m =>
+ JsonOptic
+ .fromPathString(m.group(2))
+ .getOpt(values)
+ .map(_.foldWith(json2StringFolder))
+ )
)
- )
- case CodeRx(code) =>
- sandbox.eval(code, jsData) match {
- case Success(value) => value
- case Failure(exception) => throw exception
- }
+ case CodeRx(code) =>
+ runner.eval(code) match {
+ case Success(value) => value
+ case Failure(exception) => throw exception
+ }
+ }
}
}
@@ -85,8 +88,8 @@ package object json {
def transformValues(f: PartialFunction[Json, Json]): TailRec[Json] =
transformValues(j => f.applyOrElse(j, (_: Json) => j))
- def substitute(values: Json)(implicit sandbox: GraalJsSandbox): Json =
- jsonTemplater(values).pipe { templater =>
+ def substitute(values: Json)(implicit sandbox: GraalJsSandbox): Resource[Json] =
+ jsonTemplater(values).map { templater =>
transformValues { case js @ JsonString(str) =>
templater.applyOrElse(str, (_: String) => js)
}.result
@@ -107,8 +110,8 @@ package object json {
}
}.result
- def patch(values: Json, schema: Map[JsonOptic, String])(implicit sandbox: GraalJsSandbox): Json =
- jsonTemplater(values).pipe { templater =>
+ def patch(values: Json, schema: Map[JsonOptic, String])(implicit sandbox: GraalJsSandbox): Resource[Json] =
+ jsonTemplater(values).map { templater =>
schema.foldLeft(j) { case (acc, (optic, defn)) =>
templater.lift.apply(defn).fold(acc)(optic.set(_)(acc))
}
diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/string/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/string/package.scala
index ba08f992..10dcf832 100644
--- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/string/package.scala
+++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/string/package.scala
@@ -13,12 +13,12 @@ package object string {
def substitute(jvalues: Json, xvalues: Node)(implicit sandbox: GraalJsSandbox): String =
if (SubstRx.findFirstIn(s).isDefined || CodeRx.findFirstIn(s).isDefined)
- Json.fromString(s).substitute(jvalues).substitute(xvalues).asString.getOrElse(s)
+ Json.fromString(s).substitute(jvalues).map(_.substitute(xvalues)).useAsIs.asString.getOrElse(s)
else s
def substitute(values: Json)(implicit sandbox: GraalJsSandbox): String =
if (SubstRx.findFirstIn(s).isDefined || CodeRx.findFirstIn(s).isDefined)
- Json.fromString(s).substitute(values).asString.getOrElse(s)
+ Json.fromString(s).substitute(values).useAsIs.asString.getOrElse(s)
else s
}
}
diff --git a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/xml/package.scala b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/xml/package.scala
index 6373307f..c84bdf1d 100644
--- a/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/xml/package.scala
+++ b/backend/mockingbird/src/main/scala/ru/tinkoff/tcb/utils/transformation/xml/package.scala
@@ -21,6 +21,7 @@ import kantan.xpath.implicits.*
import ru.tinkoff.tcb.utils.json.json2StringFolder
import ru.tinkoff.tcb.utils.regex.OneOrMore
+import ru.tinkoff.tcb.utils.resource.Resource
import ru.tinkoff.tcb.utils.sandboxing.GraalJsSandbox
import ru.tinkoff.tcb.utils.transformation.json.jsonTemplater
import ru.tinkoff.tcb.xpath.*
@@ -102,8 +103,8 @@ package object xml {
}.result
}
- def substitute(values: Json)(implicit sandbox: GraalJsSandbox): Node =
- jsonTemplater(values).pipe { templater =>
+ def substitute(values: Json)(implicit sandbox: GraalJsSandbox): Resource[Node] =
+ jsonTemplater(values).map { templater =>
transform {
case elem: Elem =>
elem.attributes.foldLeft(elem)((e, attr) =>
@@ -135,27 +136,29 @@ package object xml {
def patchFromValues(jValues: Json, xValues: Node, schema: Map[XmlZoom, String])(implicit
sandbox: GraalJsSandbox
- ): Node = {
- val jt = jsonTemplater(jValues)
- val nt = nodeTemplater({xValues})
-
- schema
- .foldLeft({n}: Node) { case (acc, (zoom, defn)) =>
- defn match {
- case jp if jt.isDefinedAt(jp) =>
- (zoom ==> Replace(_.map(_.transform { case Text(_) => Text(jt(jp).foldWith(json2StringFolder)) }.result)))
- .transform[Try](acc)
- .map(_.head)
- .getOrElse(acc)
- case xp if nt.isDefinedAt(xp) =>
- (zoom ==> Replace(_.map(_.transform { case Text(_) => Text(nt(xp)) }.result)))
- .transform[Try](acc)
- .map(_.head)
- .getOrElse(acc)
- case _ => acc
+ ): Resource[Node] = {
+ for {
+ jt <- jsonTemplater(jValues)
+ nt = nodeTemplater({xValues})
+ } yield {
+ schema
+ .foldLeft({n}: Node) { case (acc, (zoom, defn)) =>
+ defn match {
+ case jp if jt.isDefinedAt(jp) =>
+ (zoom ==> Replace(_.map(_.transform { case Text(_) => Text(jt(jp).foldWith(json2StringFolder)) }.result)))
+ .transform[Try](acc)
+ .map(_.head)
+ .getOrElse(acc)
+ case xp if nt.isDefinedAt(xp) =>
+ (zoom ==> Replace(_.map(_.transform { case Text(_) => Text(nt(xp)) }.result)))
+ .transform[Try](acc)
+ .map(_.head)
+ .getOrElse(acc)
+ case _ => acc
+ }
}
- }
- .pipe(_.child.head)
+ .pipe(_.child.head)
+ }
}
}
}
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 760a1d6e..34ddf621 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
@@ -66,11 +66,11 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue
"composite" := "Main topic: Some description"
)
- val sut = template.substitute(values)
+ val sut = template.substitute(values).useAsIs
sut shouldBe expected
- val sut2 = template2.substitute(values)
+ val sut2 = template2.substitute(values).useAsIs
sut2 shouldBe expected
}
@@ -82,7 +82,7 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue
"value" := "${description}"
)
- val sut = template.substitute(Json.obj())
+ val sut = template.substitute(Json.obj()).useAsIs
sut shouldBe template
}
@@ -92,7 +92,7 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue
val template = Json.obj("value" := "${message}")
- val sut = template.substitute(Json.obj("message" := Json.obj("peka" := "name")))
+ val sut = template.substitute(Json.obj("message" := Json.obj("peka" := "name"))).useAsIs
sut shouldBe Json.obj("value" := Json.obj("peka" := "name"))
}
@@ -114,7 +114,7 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue
"n" := 45.99
)
- val sut = template.substitute(values)
+ val sut = template.substitute(values).useAsIs
sut shouldBe Json.obj(
"a" := "true",
@@ -140,7 +140,7 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue
"n" := "45.99"
)
- val sut = template.substitute(values)
+ val sut = template.substitute(values).useAsIs
sut shouldBe Json.obj(
"a" := true,
@@ -267,7 +267,7 @@ class JsonTransformationsSpec extends AnyFunSuite with Matchers with OptionValue
JsonOptic.fromPathString("o3.client") -> "${name} ${surname}"
)
- val sut = target.patch(source, schema)
+ val sut = target.patch(source, schema).useAsIs
sut.get(JLens \ "a2" \ 4).asString.value shouldBe "nondesc"
sut.get(JLens \ "o3" \ "client").asString.value shouldBe "Peka Kekovsky"
diff --git a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/xml/XmlTransformationSpec.scala b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/xml/XmlTransformationSpec.scala
index 568aa574..c4a60d7d 100644
--- a/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/xml/XmlTransformationSpec.scala
+++ b/backend/mockingbird/src/test/scala/ru/tinkoff/tcb/utils/transformation/xml/XmlTransformationSpec.scala
@@ -56,7 +56,7 @@ class XmlTransformationSpec extends AnyFunSuite with Matchers {
)
)
- val sut = template.substitute(data)
+ val sut = template.substitute(data).useAsIs
sut shouldBe test42test_42
}
@@ -138,7 +138,7 @@ class XmlTransformationSpec extends AnyFunSuite with Matchers {
XmlZoom.fromXPath("/root/second").toOption.get -> "${/data/value}"
)
- val sut = target.patchFromValues(source, xSource, schema)
+ val sut = target.patchFromValues(source, xSource, schema).useAsIs
info(sut.toString())
diff --git a/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/Resource.scala b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/Resource.scala
new file mode 100644
index 00000000..8163dc37
--- /dev/null
+++ b/backend/utils/src/main/scala/ru/tinkoff/tcb/utils/resource/Resource.scala
@@ -0,0 +1,42 @@
+package ru.tinkoff.tcb.utils.resource
+
+/*
+ This implementation is taken from
+ https://bszwej.medium.com/composable-resource-management-in-scala-ce902bda48b2
+ */
+
+trait Resource[R] {
+ def use[U](f: R => U): U
+
+ def useAsIs: R = use(identity)
+}
+
+object Resource {
+ def make[R](acquire: => R)(close: R => Unit): Resource[R] =
+ new Resource[R] {
+ override def use[U](f: R => U): U = {
+ val resource = acquire
+ try {
+ f(resource)
+ } finally {
+ close(resource)
+ }
+ }
+ }
+
+ implicit val resourceMonad: Monad[Resource] =
+ new Monad[Resource] with StackSafeMonad[Resource] {
+ override def pure[R](r: R): Resource[R] = Resource.make(r)(_ => ())
+
+ override def map[A, B](r: Resource[A])(mapping: A => B): Resource[B] =
+ new Resource[B] {
+ override def use[U](f: B => U): U = r.use(a => f(mapping(a)))
+ }
+
+ override def flatMap[A, B](r: Resource[A])(mapping: A => Resource[B]): Resource[B] =
+ new Resource[B] {
+ override def use[U](f: B => U): U =
+ r.use(res1 => mapping(res1).use(res2 => f(res2)))
+ }
+ }
+}
\ No newline at end of file