Skip to content

[Nu-7983] Improve FragmentParameterTypingParser #7985

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

Merged
merged 8 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ val jacksonV = "2.17.2"
val catsV = "2.12.0"
val catsEffectV = "3.5.4"
val everitSchemaV = "1.14.4"
val fastParseV = "3.1.1"
val slf4jV = "1.7.36"
val scalaLoggingV = "3.9.5"
val scalaCompatV = "1.0.2"
Expand Down Expand Up @@ -848,6 +849,7 @@ lazy val scenarioCompiler = (project in file("scenario-compiler"))
Seq(
"org.typelevel" %% "cats-effect" % catsEffectV,
"org.scala-lang.modules" %% "scala-java8-compat" % scalaCompatV,
"com.lihaoyi" %% "fastparse" % fastParseV,
"org.apache.avro" % "avro" % avroV % Test,
"org.scalacheck" %% "scalacheck" % scalaCheckV % Test,
"com.cronutils" % "cron-utils" % cronParserV % Test,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
package pl.touk.nussknacker.engine.definition.fragment

import fastparse._
import org.apache.commons.lang3.ClassUtils
import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypingResult}
import pl.touk.nussknacker.engine.definition.clazz.ClassDefinitionSet

import scala.util.Try

import SingleLineWhitespace._

class FragmentParameterTypingParser(classLoader: ClassLoader, classDefinitions: ClassDefinitionSet) {

private val mapPattern = "Map\\[(.+),\\s*(.+)\\]".r
private val listPattern = "List\\[(.+)\\]".r
private val setPattern = "Set\\[(.+)\\]".r
private def identifier[_: P]: P[String] =
P(CharIn("a-zA-Z_") ~ CharsWhileIn("a-zA-Z0-9_.", 0)).!

private def simpleType[_: P]: P[TypingResult] =
identifier.map(resolveInnerClass)

private def mapType[_: P]: P[TypingResult] =
P("Map[" ~/ typParser ~ "," ~ typParser ~ "]")
.map { case (tr1, tr2) => Typed.genericTypeClass[java.util.Map[_, _]](List(tr1, tr2)) }

private def listType[_: P]: P[TypingResult] =
P("List[" ~/ typParser ~ "]")
.map(tr => Typed.genericTypeClass[java.util.List[_]](List(tr)))

private def setType[_: P]: P[TypingResult] =
P("Set[" ~/ typParser ~ "]")
.map(tr => Typed.genericTypeClass[java.util.Set[_]](List(tr)))

private def typParser[_: P]: P[TypingResult] =
P(mapType | listType | setType | simpleType)

private val classDefinitionsByName = classDefinitions.byName

private def resolveInnerClass(simpleClassName: String): TypingResult =
classDefinitionsByName.get(simpleClassName) match {
case Some(resolvedClassDefinition) =>
resolvedClassDefinition.clazzName
case None =>
// This is fallback - it may be removed and `ClassNotFound` exception may be thrown here after cleaning up the mess with `FragmentClazzRef` class
Typed(ClassUtils.getClass(classLoader, simpleClassName))
}

def parseClassNameToTypingResult(className: String): Try[TypingResult] = {
/*
TODO: Write this parser in a way that handles arbitrary depth expressions
One should not use regexes for doing so and rather build AST
*/
def resolveInnerClass(simpleClassName: String): TypingResult =
classDefinitionsByName.get(simpleClassName) match {
case Some(resolvedClassDefinition) =>
resolvedClassDefinition.clazzName
case None =>
// This is fallback - it may be removed and `ClassNotFound` exception may be thrown here after cleaning up the mess with `FragmentClazzRef` class
Typed(ClassUtils.getClass(classLoader, simpleClassName))
}

Try(className match {
case mapPattern(x, y) =>
val resolvedFirstTypeParam = resolveInnerClass(x)
val resolvedSecondTypeParam = resolveInnerClass(y)
Typed.genericTypeClass[java.util.Map[_, _]](List(resolvedFirstTypeParam, resolvedSecondTypeParam))
case listPattern(x) =>
val resolvedTypeParam = resolveInnerClass(x)
Typed.genericTypeClass[java.util.List[_]](List(resolvedTypeParam))
case setPattern(x) =>
val resolvedTypeParam = resolveInnerClass(x)
Typed.genericTypeClass[java.util.Set[_]](List(resolvedTypeParam))
case simpleClassName =>
resolveInnerClass(simpleClassName)

Try(parse(className, implicit ctx => typParser ~ End) match {
case Parsed.Success(typingResult, _) => typingResult
case Parsed.Failure(label, index, extra) =>
throw new IllegalArgumentException(s"Parsing failed at $index: $label\n${extra.trace().longMsg}")
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,71 @@ class NodeDataValidatorSpec extends AnyFunSuite with Matchers with Inside with T
}
}

test("should allow usage of nested generic type in FragmentInputDefinition parameter") {
val nodeId: String = "in"
val paramName = "param1"

inside(
validate(
FragmentInputDefinition(
nodeId,
List(
FragmentParameter(
ParameterName(paramName),
FragmentClazzRef("Map[List[Integer], Map[String, Map[Double, List[Integer]]]]"),
required = false,
initialValue = None,
hintText = None,
valueEditor = None,
valueCompileTimeValidation = None
)
),
),
ValidationContext.empty,
Map.empty,
outgoingEdges = List(OutgoingEdge("any", Some(FragmentOutput("out1"))))
)
) { case ValidationPerformed(errors, None, None) =>
errors shouldBe empty
}
}

test("should not allow type definition with unbalanced brackets") {
val nodeId: String = "in"
val paramName = "param1"
val additionalBracket = "]"

inside(
validate(
FragmentInputDefinition(
nodeId,
List(
FragmentParameter(
ParameterName(paramName),
FragmentClazzRef(s"Map[List[Integer], Map[String, Map[Double, List[Integer]]]]$additionalBracket"),
required = false,
initialValue = None,
hintText = None,
valueEditor = None,
valueCompileTimeValidation = None
)
),
),
ValidationContext.empty,
Map.empty,
outgoingEdges = List(OutgoingEdge("any", Some(FragmentOutput("out1"))))
)
) { case ValidationPerformed(errors, None, None) =>
errors shouldBe List(
FragmentParamClassLoadError(
ParameterName("param1"),
"Map[List[Integer], Map[String, Map[Double, List[Integer]]]]]",
"in"
)
)
}
}

test(
"should not allow usage of generic type in FragmentInputDefinition parameter when occurring type is not on classpath"
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package pl.touk.nussknacker.engine.definition.fragment

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import pl.touk.nussknacker.engine.api.typed.typing.TypedClass
import pl.touk.nussknacker.engine.definition.clazz.{ClassDefinitionSet, ClassDefinitionTestUtils}

import scala.util.{Failure, Success}

class FragmentParameterTypingParserSpec extends AnyFunSuite with Matchers {
private val classLoader = getClass.getClassLoader

private val classDefinitionExtractor = ClassDefinitionTestUtils.DefaultExtractor

private val clazzDefinitions: ClassDefinitionSet = ClassDefinitionSet(
Set(
classDefinitionExtractor.extract(classOf[String]),
classDefinitionExtractor.extract(classOf[Integer]),
)
)

private val fragmentParameterTypingParser = new FragmentParameterTypingParser(classLoader, clazzDefinitions)

test("should parse nested generic Map type into TypedClass") {
val triedTypingResult =
fragmentParameterTypingParser.parseClassNameToTypingResult("Map[List[Integer], Map[String, List[Integer]]]")

triedTypingResult match {
case Failure(ex) => fail(s"Unexpected failure: $ex")
case Success(tc: TypedClass) =>
tc.display shouldBe "Map[List[Integer],Map[String,List[Integer]]]"
case Success(tr) =>
fail(s"Expected a result type which is TypedClass but got instead: ${tr.getClass.getSimpleName}")
}
}

test("should fail to parse a class name when it contains a non-existing type") {
val triedTypingResult =
fragmentParameterTypingParser.parseClassNameToTypingResult("Map[String, NotExistingType]")

triedTypingResult match {
case Failure(ex) => ex shouldBe a[ClassNotFoundException]
case Success(tr) => fail(s"Expected failure due to non-existing type, but got success with: $tr")
}
}

}