Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Variables passed in Expression.apply sometimes not resolved in imported jslts #222

Open
orElse opened this issue Sep 10, 2021 · 1 comment
Labels
bug Something isn't working

Comments

@orElse
Copy link

orElse commented Sep 10, 2021

For maintainability I have some commons and utils extracted to separate files which can be reused in my actual transformation jslts - for tracking the context of data I am also adding some variables during the apply which I than add to the transformed data via the defs inside the commons/utils.

Additionally I have a small string map resolver for the additional files since I cannot put them inside the classpath resources.

I have the issue that the injected variables are not available in all imported files - at the moment I am just not sure if it is a general issue or due my resolver (wouldn't hope that) - I tried to reduce the code to bare minimum reproducer (sorry that it is in scala)

With the import order in the test I am getting a:
com.schibsted.spt.data.jslt.JsltException: No such variable 'test-request-id' at commons.jslt:4:19
if I switch the imports it is fine, since my actual jslts are more complex I can currently not solve this with import reordering ( I probably have to merge all my additional/common jslts to one file as a workaround for now)

class JsltPropertiesSpec extends AnyWordSpec
  with must.Matchers
  with TryValues
  with EitherValues {

  import java.util.{Collections => JCollections}

  private val mapper = new ObjectMapper()

  val contextVariables: util.Map[String, JsonNode] = {
    import scala.jdk.CollectionConverters._
    Map(
      "test-request-id" -> new TextNode("1234").asInstanceOf[JsonNode]
    ).asJava
  }

  case class StringMapResolver(commonJsltPatterns: Map[String, String]) extends ResourceResolver {
    override def resolve(importedJslt: String): Reader = {
      commonJsltPatterns.get(importedJslt) match {
        case Some(pattern) => new StringReader(pattern)
        case None => throw new JsltException(s"""Could not resolve imported JSLT pattern "$importedJslt"""")
      }
    }
  }

  def toExpression(transformPattern: String, commonPatterns: Map[String, String] = Map.empty): Expression = {
    new Parser(new StringReader(transformPattern))
      .withSource("<inline>")
      .withFunctions(JCollections.emptySet())
      .withResourceResolver(StringMapResolver(commonPatterns))
      .withObjectFilter(". != null or . != {}") // empty arrays are omitted by default
      .compile()
  }

  def transformJson(expression: Expression, rawJson: String, context: util.Map[String, JsonNode] = contextVariables): Try[String] = Try {
    val input: JsonNode = mapper.readTree(rawJson)
    val mapped = expression.apply(context, input)
    mapped.toString
  }


  "JsltProperties" should {
    val jsonString =
      """
        |{
        |  "name": "test"
        |}
        |""".stripMargin

    "be resolvable in 'let' inside 'def' loaded via import" in {
      val commons = Map(
        "utils.jslt" ->
          """
            |def required(fieldValue, errorMessageFieldName)
            |  if ($fieldValue)
            |    $fieldValue
            |  else
            |    error("JobId: "+ $test-request-id +" missing required field: " + $errorMessageFieldName)
            |
            |""".stripMargin,
        "commons.jslt" ->
          """
            |
            |def get-request-id-commons()
            |  let requestId = $test-request-id
            |  $requestId
            |""".stripMargin
      )

      val expression = toExpression(
        """
          |import "commons.jslt" as commons
          |import "utils.jslt" as utils
          |
          |def get-request-id()
          |  commons:get-request-id-commons()
          |
          |{
          |  "name" : utils:required(.name, "name"),
          |  "request" : {
          |    "id" : get-request-id()
          |  }
          |}
          |""".stripMargin,
        commons
      )

      val result = transformJson(expression, jsonString)
      result.success.value mustEqual """{"name":"test","request":{"id":"1234"}}"""
    }
  }
}
@larsga larsga added the bug Something isn't working label Sep 16, 2021
@larsga
Copy link
Collaborator

larsga commented Sep 16, 2021

It sounds like this is a bug. A variable defined in one module shouldn't be visible in another module at all. So I think you need to rewrite this so that you don't have these cross-module variable dependencies.

I'll fix the bug, but the fix is going to be making sure this never works. :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants