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