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

[NU-1800] Add template lazy param #7162

Open
wants to merge 8 commits into
base: staging
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -1816,7 +1816,8 @@ lazy val flinkBaseComponentsTests = (project in flink("components/base-tests"))
)
.dependsOn(
flinkComponentsTestkit % Test,
flinkTableApiComponents % Test
flinkTableApiComponents % Test,
scenarioCompiler % "test->test"
arkadius marked this conversation as resolved.
Show resolved Hide resolved
)

lazy val flinkKafkaComponents = (project in flink("components/kafka"))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.touk.nussknacker.engine.api

import pl.touk.nussknacker.engine.api.LazyParameter.TemplateLazyParameter.TemplateExpression
import pl.touk.nussknacker.engine.api.LazyParameter.{Evaluate, MappedLazyParameter, ProductLazyParameter}
import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypingResult}

Expand Down Expand Up @@ -68,6 +69,25 @@ object LazyParameter {

trait CustomLazyParameter[+T <: AnyRef] extends LazyParameter[T]

trait TemplateLazyParameter[T <: AnyRef] extends LazyParameter[T] {
def templateExpression: TemplateExpression
}

object TemplateLazyParameter {
case class TemplateExpression(parts: List[TemplateExpressionPart])
sealed trait TemplateExpressionPart

object TemplateExpressionPart {
case class Literal(value: String) extends TemplateExpressionPart

trait Placeholder extends TemplateExpressionPart {
val evaluate: Evaluate[String]
}

}

}

final class ProductLazyParameter[T <: AnyRef, Y <: AnyRef](
val arg1: LazyParameter[T],
val arg2: LazyParameter[Y]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package pl.touk.nussknacker.engine.flink

import com.typesafe.config.ConfigFactory
import org.apache.flink.api.connector.source.Boundedness
import org.apache.flink.streaming.api.datastream.{DataStream, DataStreamSink}
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment
import org.apache.flink.streaming.api.functions.sink
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.functions.sink.v2.DiscardingSink
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
import pl.touk.nussknacker.engine.api.LazyParameter.TemplateLazyParameter
import pl.touk.nussknacker.engine.api.LazyParameter.TemplateLazyParameter.TemplateExpressionPart.{Literal, Placeholder}
import pl.touk.nussknacker.engine.api.{Context, LazyParameter, MethodToInvoke, NodeId, Params, ValueWithContext}
import pl.touk.nussknacker.engine.api.component.{BoundedStreamComponent, Component, ComponentDefinition}
import pl.touk.nussknacker.engine.api.context.{OutputVar, ValidationContext}
import pl.touk.nussknacker.engine.api.context.transformation.{
DefinedLazyParameter,
NodeDependencyValue,
SingleInputDynamicComponent
}
import pl.touk.nussknacker.engine.api.definition.{
NodeDependency,
OutputVariableNameDependency,
ParameterDeclaration,
SpelTemplateParameterEditor
}
import pl.touk.nussknacker.engine.api.parameter.ParameterName
import pl.touk.nussknacker.engine.api.process.{Sink, SinkFactory, Source, SourceFactory}
import pl.touk.nussknacker.engine.api.typed.typing
import pl.touk.nussknacker.engine.build.ScenarioBuilder
import pl.touk.nussknacker.engine.flink.api.process.{FlinkCustomNodeContext, FlinkSink, StandardFlinkSource}
import pl.touk.nussknacker.engine.flink.test.FlinkSpec
import pl.touk.nussknacker.engine.flink.util.test.FlinkTestScenarioRunner._
import pl.touk.nussknacker.engine.graph.expression.Expression
import pl.touk.nussknacker.engine.process.FlinkJobConfig.ExecutionMode
import pl.touk.nussknacker.engine.spel.SpelExtension._
import pl.touk.nussknacker.engine.util.test.TestScenarioRunner
import pl.touk.nussknacker.test.ValidatedValuesDetailedMessage

class TemplateLazyParameterTest extends AnyFunSuite with FlinkSpec with Matchers with ValidatedValuesDetailedMessage {

private lazy val runner = TestScenarioRunner
.flinkBased(ConfigFactory.empty(), flinkMiniCluster)
.withExecutionMode(ExecutionMode.Batch)
.withExtraComponents(
List(ComponentDefinition("templateAstOperationSink", SpelTemplateAstOperationSink))
)
.build()

test("should use spel template ast operation parameter") {
val scenario = ScenarioBuilder
.streaming("test")
.source("source", TestScenarioRunner.testDataSource)
.emptySink(
"end",
"templateAstOperationSink",
"templateOutput" -> Expression.spelTemplate(s"Hello#{#input}")
)

val result = runner.runWithData(scenario, List(1, 2, 3), Boundedness.BOUNDED)
println(result)
result.validValue.successes shouldBe List(

Check failure on line 63 in engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala

View workflow job for this annotation

GitHub Actions / REPORT-BackendTests-2.13

pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest ► should use spel template ast operation parameter

Failed test found in: engine/flink/components/base-tests/target/test-reports/TEST-pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest.xml Error: org.scalatest.exceptions.TestFailedException: List() was not equal to List("[Hello]-literal[1]-templated", "[Hello]-literal[2]-templated", "[Hello]-literal[3]-templated")
Raw output
org.scalatest.exceptions.TestFailedException: List() was not equal to List("[Hello]-literal[1]-templated", "[Hello]-literal[2]-templated", "[Hello]-literal[3]-templated")
	at org.scalatest.matchers.MatchersHelper$.indicateFailure(MatchersHelper.scala:392)
	at org.scalatest.matchers.should.Matchers$AnyShouldWrapper.shouldBe(Matchers.scala:7539)
	at pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest.$anonfun$new$1(TemplateLazyParameterTest.scala:63)
	at org.scalatest.OutcomeOf.outcomeOf(OutcomeOf.scala:85)
	at org.scalatest.OutcomeOf.outcomeOf$(OutcomeOf.scala:83)
	at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
	at org.scalatest.Transformer.apply(Transformer.scala:22)
	at org.scalatest.Transformer.apply(Transformer.scala:20)
	at org.scalatest.funsuite.AnyFunSuiteLike$$anon$1.apply(AnyFunSuiteLike.scala:226)
	at org.scalatest.TestSuite.withFixture(TestSuite.scala:196)
	at org.scalatest.TestSuite.withFixture$(TestSuite.scala:195)
	at org.scalatest.funsuite.AnyFunSuite.withFixture(AnyFunSuite.scala:1564)
	at org.scalatest.funsuite.AnyFunSuiteLike.invokeWithFixture$1(AnyFunSuiteLike.scala:224)
	at org.scalatest.funsuite.AnyFunSuiteLike.$anonfun$runTest$1(AnyFunSuiteLike.scala:236)
	at org.scalatest.SuperEngine.runTestImpl(Engine.scala:306)
	at org.scalatest.funsuite.AnyFunSuiteLike.runTest(AnyFunSuiteLike.scala:236)
	at org.scalatest.funsuite.AnyFunSuiteLike.runTest$(AnyFunSuiteLike.scala:218)
	at pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest.org$scalatest$BeforeAndAfter$$super$runTest(TemplateLazyParameterTest.scala:41)
	at org.scalatest.BeforeAndAfter.runTest(BeforeAndAfter.scala:213)
	at org.scalatest.BeforeAndAfter.runTest$(BeforeAndAfter.scala:203)
	at pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest.runTest(TemplateLazyParameterTest.scala:41)
	at org.scalatest.funsuite.AnyFunSuiteLike.$anonfun$runTests$1(AnyFunSuiteLike.scala:269)
	at org.scalatest.SuperEngine.$anonfun$runTestsInBranch$1(Engine.scala:413)
	at scala.collection.immutable.List.foreach(List.scala:334)
	at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:401)
	at org.scalatest.SuperEngine.runTestsInBranch(Engine.scala:396)
	at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:475)
	at org.scalatest.funsuite.AnyFunSuiteLike.runTests(AnyFunSuiteLike.scala:269)
	at org.scalatest.funsuite.AnyFunSuiteLike.runTests$(AnyFunSuiteLike.scala:268)
	at org.scalatest.funsuite.AnyFunSuite.runTests(AnyFunSuite.scala:1564)
	at org.scalatest.Suite.run(Suite.scala:1114)
	at org.scalatest.Suite.run$(Suite.scala:1096)
	at org.scalatest.funsuite.AnyFunSuite.org$scalatest$funsuite$AnyFunSuiteLike$$super$run(AnyFunSuite.scala:1564)
	at org.scalatest.funsuite.AnyFunSuiteLike.$anonfun$run$1(AnyFunSuiteLike.scala:273)
	at org.scalatest.SuperEngine.runImpl(Engine.scala:535)
	at org.scalatest.funsuite.AnyFunSuiteLike.run(AnyFunSuiteLike.scala:273)
	at org.scalatest.funsuite.AnyFunSuiteLike.run$(AnyFunSuiteLike.scala:272)
	at pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest.org$scalatest$BeforeAndAfterAll$$super$run(TemplateLazyParameterTest.scala:41)
	at org.scalatest.BeforeAndAfterAll.liftedTree1$1(BeforeAndAfterAll.scala:213)
	at org.scalatest.BeforeAndAfterAll.run(BeforeAndAfterAll.scala:210)
	at org.scalatest.BeforeAndAfterAll.run$(BeforeAndAfterAll.scala:208)
	at pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest.org$scalatest$BeforeAndAfter$$super$run(TemplateLazyParameterTest.scala:41)
	at org.scalatest.BeforeAndAfter.run(BeforeAndAfter.scala:273)
	at org.scalatest.BeforeAndAfter.run$(BeforeAndAfter.scala:271)
	at pl.touk.nussknacker.engine.flink.TemplateLazyParameterTest.run(TemplateLazyParameterTest.scala:41)
	at org.scalatest.tools.Framework.org$scalatest$tools$Framework$$runSuite(Framework.scala:321)
	at org.scalatest.tools.Framework$ScalaTestTask.execute(Framework.scala:517)
	at sbt.TestRunner.runTest$1(TestFramework.scala:153)
	at sbt.TestRunner.run(TestFramework.scala:168)
	at sbt.TestFramework$$anon$3$$anonfun$$lessinit$greater$1.$anonfun$apply$1(TestFramework.scala:336)
	at sbt.TestFramework$.sbt$TestFramework$$withContextLoader(TestFramework.scala:296)
	at sbt.TestFramework$$anon$3$$anonfun$$lessinit$greater$1.apply(TestFramework.scala:336)
	at sbt.TestFramework$$anon$3$$anonfun$$lessinit$greater$1.apply(TestFramework.scala:336)
	at sbt.TestFunction.apply(TestFramework.scala:348)
	at sbt.Tests$.processRunnable$1(Tests.scala:475)
	at sbt.Tests$.$anonfun$makeSerial$1(Tests.scala:481)
	at sbt.std.Transform$$anon$3.$anonfun$apply$2(Transform.scala:47)
	at sbt.std.Transform$$anon$4.work(Transform.scala:69)
	at sbt.Execute.$anonfun$submit$2(Execute.scala:283)
	at sbt.internal.util.ErrorHandling$.wideConvert(ErrorHandling.scala:24)
	at sbt.Execute.work(Execute.scala:292)
	at sbt.Execute.$anonfun$submit$1(Execute.scala:283)
	at sbt.ConcurrentRestrictions$$anon$4.$anonfun$submitValid$1(ConcurrentRestrictions.scala:265)
	at sbt.CompletionService$$anon$2.call(CompletionService.scala:65)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:829)
"[Hello]-literal[1]-templated",
"[Hello]-literal[2]-templated",
"[Hello]-literal[3]-templated"
)
}

}

object SpelTemplateAstOperationSink
extends SingleInputDynamicComponent[Sink]
with SinkFactory
with BoundedStreamComponent {

private val spelTemplateParameter = ParameterDeclaration
.lazyOptional[String](ParameterName("templateOutput"))
.withCreator(modify =
_.copy(
editor = Some(SpelTemplateParameterEditor)
)
)

override type State = Unit

override def contextTransformation(context: ValidationContext, dependencies: List[NodeDependencyValue])(
implicit nodeId: NodeId
): SpelTemplateAstOperationSink.ContextTransformationDefinition = {
case TransformationStep(Nil, _) => NextParameters(List(spelTemplateParameter.createParameter()))
case TransformationStep((ParameterName("templateOutput"), DefinedLazyParameter(_)) :: Nil, _) =>
FinalResults(context, List.empty)
}

override def nodeDependencies: List[NodeDependency] = List.empty

override def implementation(params: Params, dependencies: List[NodeDependencyValue], finalState: Option[Unit]): Sink =
new FlinkSink {
override type Value = String

val valueFromParam: LazyParameter[String] = spelTemplateParameter.extractValueUnsafe(params)

override def prepareValue(
dataStream: DataStream[Context],
flinkCustomNodeContext: FlinkCustomNodeContext
): DataStream[ValueWithContext[Value]] = {
dataStream.flatMap(
flinkCustomNodeContext.lazyParameterHelper.lazyMapFunction(valueFromParam),
flinkCustomNodeContext.valueWithContextInfo.forType(valueFromParam.returnType)
)
}

override def registerSink(
dataStream: DataStream[ValueWithContext[String]],
flinkNodeContext: FlinkCustomNodeContext
): DataStreamSink[_] = {
println(dataStream)
dataStream.addSink(new SinkFunction[ValueWithContext[String]] {
override def invoke(value: ValueWithContext[String], context: SinkFunction.Context): Unit = {
println(value)
println("debug")

Comment on lines +117 to +122
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove debug println statements from production code

Production code should not contain debug print statements. Consider using proper logging if debugging information is needed.

-        println(dataStream)
         dataStream.addSink(new SinkFunction[ValueWithContext[String]] {
           override def invoke(value: ValueWithContext[String], context: SinkFunction.Context): Unit = {
-            println(value)
-            println("debug")
+            // Add actual sink implementation here
           }

Committable suggestion skipped: line range outside the PR's diff.

}
})
}

}
Comment on lines +113 to +127
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement proper sink functionality

The current sink implementation only prints to console and effectively discards the data. Consider:

  1. Implementing actual data persistence
  2. Adding error handling
  3. Implementing proper cleanup in close method


}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package pl.touk.nussknacker.engine.compile.nodecompilation

import pl.touk.nussknacker.engine.api.LazyParameter.{CustomLazyParameter, Evaluate}
import pl.touk.nussknacker.engine.api.typed.typing.TypingResult
import pl.touk.nussknacker.engine.api.{Context, JobData, MetaData, NodeId}
import pl.touk.nussknacker.engine.compiledgraph.{BaseCompiledParameter, CompiledParameter}
import pl.touk.nussknacker.engine.api.LazyParameter.TemplateLazyParameter.{TemplateExpression, TemplateExpressionPart}
import pl.touk.nussknacker.engine.api.LazyParameter.{CustomLazyParameter, Evaluate, TemplateLazyParameter}
import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypingResult}
import pl.touk.nussknacker.engine.api.{Context, JobData, LazyParameter, NodeId}
import pl.touk.nussknacker.engine.compiledgraph.BaseCompiledParameter
import pl.touk.nussknacker.engine.expression.ExpressionEvaluator
import pl.touk.nussknacker.engine.graph.expression.Expression.Language
import pl.touk.nussknacker.engine.spel.SpelExpression
import pl.touk.nussknacker.engine.spel.SpelTemplateExpressionPart.{Literal, Placeholder}

class EvaluableLazyParameter[T <: AnyRef](
compiledParameter: BaseCompiledParameter,
Expand All @@ -14,19 +18,87 @@ class EvaluableLazyParameter[T <: AnyRef](
override val returnType: TypingResult
) extends CustomLazyParameter[T] {

def this(
compiledParameter: CompiledParameter,
override val evaluate: Evaluate[T] =
LazyParameterEvaluator.evaluate(compiledParameter, expressionEvaluator, nodeId, jobData)

}

class SpelTemplateEvaluableLazyParameter[T <: AnyRef](
compiledParameter: BaseCompiledParameter,
expressionEvaluator: ExpressionEvaluator,
nodeId: NodeId,
jobData: JobData
) extends TemplateLazyParameter[T] {

override val evaluate: Evaluate[T] =
LazyParameterEvaluator.evaluate(compiledParameter, expressionEvaluator, nodeId, jobData)

override def templateExpression: TemplateExpression = compiledParameter.expression match {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to evaluate it during each invocation

case expression: SpelExpression =>
expression.templateSubexpressions match {
case Some(subexpressions) =>
val templateParts = subexpressions.map {
case Placeholder(expression) => {
new TemplateExpressionPart.Placeholder {
override val evaluate: Evaluate[String] = context => {
expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData).value
}
Comment on lines +43 to +45
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add error handling for expression evaluation

The expression evaluation should handle potential errors to prevent runtime exceptions.

-                override val evaluate: Evaluate[String] = context => {
-                  expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData).value
-                }
+                override val evaluate: Evaluate[String] = context => {
+                  val result = expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData)
+                  if (result.isRight) result.value
+                  else throw new IllegalStateException(s"Failed to evaluate template expression: ${result.left.get}")
+                }

Committable suggestion skipped: line range outside the PR's diff.

}
}
case Literal(value) => TemplateExpressionPart.Literal(value)
}
TemplateExpression(templateParts)
case None =>
throw new IllegalStateException("Non SpEL-template expression received in SpelTemplateLazyParameter")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can get rid of this exceptions by moving this logic to factory method and check templateSubexpressions instead of expression langage

}
case _ => throw new IllegalStateException("Non SpEL expression received in SpelTemplateLazyParameter")
}

override def returnType: TypingResult = Typed[String]
}

private[this] object LazyParameterEvaluator {

def evaluate[T <: AnyRef](
compiledParameter: BaseCompiledParameter,
expressionEvaluator: ExpressionEvaluator,
nodeId: NodeId,
jobData: JobData
) =
this(compiledParameter, expressionEvaluator, nodeId, jobData, compiledParameter.typingInfo.typingResult)

override val evaluate: Evaluate[T] = { ctx: Context =>
): Evaluate[T] = { ctx: Context =>
expressionEvaluator
.evaluateParameter(compiledParameter, ctx)(nodeId, jobData)
.value
.asInstanceOf[T]
Comment on lines +62 to 71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for type casting

The unsafe cast to type T could fail at runtime. Consider adding proper error handling.

   def evaluate[T <: AnyRef](
       compiledParameter: BaseCompiledParameter,
       expressionEvaluator: ExpressionEvaluator,
       nodeId: NodeId,
       jobData: JobData
   ): Evaluate[T] = { ctx: Context =>
     expressionEvaluator
       .evaluateParameter(compiledParameter, ctx)(nodeId, jobData)
       .value
-      .asInstanceOf[T]
+      match {
+        case value: T => value
+        case other => throw new ClassCastException(
+          s"Expected type ${compiledParameter.returnType} but got: ${other.getClass.getSimpleName}"
+        )
+      }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def evaluate[T <: AnyRef](
compiledParameter: BaseCompiledParameter,
expressionEvaluator: ExpressionEvaluator,
nodeId: NodeId,
jobData: JobData
) =
this(compiledParameter, expressionEvaluator, nodeId, jobData, compiledParameter.typingInfo.typingResult)
override val evaluate: Evaluate[T] = { ctx: Context =>
): Evaluate[T] = { ctx: Context =>
expressionEvaluator
.evaluateParameter(compiledParameter, ctx)(nodeId, jobData)
.value
.asInstanceOf[T]
def evaluate[T <: AnyRef](
compiledParameter: BaseCompiledParameter,
expressionEvaluator: ExpressionEvaluator,
nodeId: NodeId,
jobData: JobData
): Evaluate[T] = { ctx: Context =>
expressionEvaluator
.evaluateParameter(compiledParameter, ctx)(nodeId, jobData)
.value
match {
case value: T => value
case other => throw new ClassCastException(
s"Expected type ${compiledParameter.returnType} but got: ${other.getClass.getSimpleName}"
)
}

}

}

object EvaluableLazyParameterFactory {

def build[T <: AnyRef](
compiledParameter: BaseCompiledParameter,
expressionEvaluator: ExpressionEvaluator,
nodeId: NodeId,
jobData: JobData,
typingResult: TypingResult
): LazyParameter[T] = {
compiledParameter.expression.language match {
case Language.SpelTemplate =>
new SpelTemplateEvaluableLazyParameter[T](
compiledParameter,
expressionEvaluator,
nodeId,
jobData
)
case _ =>
new EvaluableLazyParameter[T](
compiledParameter,
expressionEvaluator,
nodeId,
jobData,
typingResult
)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class EvaluableLazyParameterCreator[T <: AnyRef](
override val returnType: TypingResult
) extends CustomLazyParameter[T] {

def create(deps: EvaluableLazyParameterCreatorDeps): EvaluableLazyParameter[T] = {
def create(deps: EvaluableLazyParameterCreatorDeps): LazyParameter[T] = {
createEvaluableLazyParameter(deps)
}

Expand All @@ -42,13 +42,14 @@ final class EvaluableLazyParameterCreator[T <: AnyRef](
override val shouldBeWrappedWithScalaOption: Boolean = parameterDef.scalaOptionParameter
override val shouldBeWrappedWithJavaOptional: Boolean = parameterDef.javaOptionalParameter
}
new EvaluableLazyParameter[T](
compiledParameter,
deps.expressionEvaluator,
nodeId,
deps.jobData,
returnType
)
EvaluableLazyParameterFactory
.build[T](
compiledParameter = compiledParameter,
expressionEvaluator = deps.expressionEvaluator,
nodeId = nodeId,
jobData = deps.jobData,
typingResult = returnType
)
}

}
Expand Down Expand Up @@ -92,7 +93,8 @@ class DefaultToEvaluateFunctionConverter(deps: EvaluableLazyParameterCreatorDeps
p.fun,
p.transformTypingResult
)
case p: CustomLazyParameter[T] => p
case p: CustomLazyParameter[T] => p
case p: TemplateLazyParameter[T] => p
Comment on lines +96 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Review pattern matching order for potential issues.

The case p: CustomLazyParameter[T] might catch instances of EvaluableLazyParameterCreator before they reach the more specific case above, potentially preventing proper evaluation of nested creators. Consider:

  1. Moving these cases before the EvaluableLazyParameterCreator case, or
  2. Making the pattern matching more specific to exclude EvaluableLazyParameterCreator

Here's a suggested fix:

-      case p: CustomLazyParameter[T]   => p
-      case p: TemplateLazyParameter[T] => p
+      case p: TemplateLazyParameter[T] => p
+      case p: CustomLazyParameter[T] if !p.isInstanceOf[EvaluableLazyParameterCreator[_]] => p
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case p: CustomLazyParameter[T] => p
case p: TemplateLazyParameter[T] => p
case p: TemplateLazyParameter[T] => p
case p: CustomLazyParameter[T] if !p.isInstanceOf[EvaluableLazyParameterCreator[_]] => p

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,13 @@ class ParameterEvaluator(
): LazyParameter[Nothing] = {
lazyParameterCreationStrategy match {
case EvaluableLazyParameterStrategy =>
new EvaluableLazyParameter(
CompiledParameter(exprValue, definition),
runtimeExpressionEvaluator,
nodeId,
jobData
val compiledParameter = CompiledParameter(exprValue, definition)
EvaluableLazyParameterFactory.build(
compiledParameter = compiledParameter,
expressionEvaluator = runtimeExpressionEvaluator,
nodeId = nodeId,
jobData = jobData,
typingResult = compiledParameter.typingInfo.typingResult
)
case PostponedEvaluatorLazyParameterStrategy =>
new EvaluableLazyParameterCreator(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import pl.touk.nussknacker.engine.expression.parse.{CompiledExpression, Expressi
import pl.touk.nussknacker.engine.graph.expression.Expression.Language
import pl.touk.nussknacker.engine.graph.expression.{Expression => GraphExpression}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.ExpressionCompilationError
import pl.touk.nussknacker.engine.spel.SpelExpressionParser.Flavour
import pl.touk.nussknacker.engine.spel.SpelExpressionParser.{Flavour, Standard}
import pl.touk.nussknacker.engine.spel.SpelTemplateExpressionPart.{Literal, Placeholder}
import pl.touk.nussknacker.engine.spel.internal.EvaluationContextPreparer

import scala.util.control.NonFatal
Expand Down Expand Up @@ -80,6 +81,13 @@ class SpelExpressionEvaluationException(val expression: String, val ctxId: Strin
cause = cause
)

sealed trait SpelTemplateExpressionPart

object SpelTemplateExpressionPart {
final case class Literal(value: String) extends SpelTemplateExpressionPart
final case class Placeholder(expression: SpelExpression) extends SpelTemplateExpressionPart
}

class SpelExpression(
parsed: ParsedSpelExpression,
expectedReturnType: TypingResult,
Expand All @@ -92,6 +100,27 @@ class SpelExpression(

override val language: Language = flavour.languageId

def templateSubexpressions: Option[List[SpelTemplateExpressionPart]] = {
def parseTemplate(expression: Expression): List[SpelTemplateExpressionPart] = expression match {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it should be rather called either parseExpression or parseParts

case lit: LiteralExpression => List(Literal(lit.getExpressionString))
case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
val parsedTemplateExpr = ParsedSpelExpression(spelExpr.getExpressionString, parsed.parser, spelExpr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a template expression

val compiledExpr = new SpelExpression(
parsedTemplateExpr,
typing.Typed[String],
Standard,
evaluationContextPreparer
)
List(Placeholder(compiledExpr))
case compositeExpr: CompositeStringExpression => compositeExpr.getExpressions.toList.flatMap(parseTemplate)
case other => throw new IllegalArgumentException(s"Unsupported expression type: [${other.getClass.getName}]")
}
flavour.languageId match {
case Language.SpelTemplate => Some(parseTemplate(parsed.parsed))
case _ => None
}
}

private val expectedClass =
expectedReturnType match {
case r: SingleTypingResult =>
Expand Down
Loading
Loading