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

Conversation

mslabek
Copy link
Member

@mslabek mslabek commented Nov 18, 2024

Describe your changes

Checklist before merge

  • Related issue ID is placed at the beginning of PR title in [brackets] (can be GH issue or Nu Jira issue)
  • Code is cleaned from temporary changes and commented out lines
  • Parts of the code that are not easy to understand are documented in the code
  • Changes are covered by automated tests
  • Showcase in dev-application.conf added to demonstrate the feature
  • Documentation added or updated
  • Added entry in Changelog.md describing the change from the perspective of a public distribution user
  • Added MigrationGuide.md entry in the appropriate subcategory if introducing a breaking change
  • Verify that PR will be squashed during merge

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced TemplateLazyParameter for enhanced parameter expression handling, supporting both static and dynamic components.
    • Added SpelTemplateAstOperationService for processing SpEL templates, improving template evaluation capabilities.
    • New method for handling subexpressions in SpEL expressions, allowing for more nuanced template scenarios.
  • Bug Fixes

    • Improved evaluation logic for lazy parameters and streamlined instantiation processes.
  • Tests

    • Added comprehensive tests for SpEL template processing to ensure expected behavior across various scenarios.
    • Introduced new test cases to validate the behavior of the templateAstOperationService with various SpEL template expressions.

Copy link

coderabbitai bot commented Nov 18, 2024

Walkthrough

The changes in this pull request involve enhancements to dependency management and functionality within the project. A new dependency, scenarioCompiler, is added for testing purposes. Additionally, new traits and classes are introduced to support template expressions in the lazy parameter system, improving the handling of both templated and non-templated parameters. Test cases are added to validate these new features, and modifications are made to existing classes to streamline the evaluation process of lazy parameters.

Changes

File Change Summary
build.sbt Added dependency: scenarioCompiler for flinkBaseComponentsTests.
components-api/src/main/scala/pl/touk/nussknacker/engine/api/LazyParameter.scala Added TemplateLazyParameter trait and TemplateExpression case class for handling templates.
engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala Introduced TemplateLazyParameterTest class with tests for template expressions.
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala Added SpelTemplateEvaluableLazyParameter class and modified EvaluableLazyParameter for template handling.
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameterCreator.scala Updated create method to return LazyParameter instead of EvaluableLazyParameter.
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/ParameterEvaluator.scala Modified prepareLazyParameterExpression to use factory method for creating lazy parameters.
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala Introduced SpelTemplateExpressionPart trait and modified SpelExpression for subexpression handling.
scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/InterpreterSpec.scala Added multiple test cases for templateAstOperationService related to SpEL templates.
scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/testcomponents/SpelTemplateAstOperationService.scala Introduced SpelTemplateAstOperationService object with methods for processing SpEL templates.

Poem

In the garden where code does bloom,
New templates rise, dispelling gloom.
With lazy parameters, they dance and play,
SpEL whispers secrets, guiding the way.
Hops of joy for tests anew,
In the land of code, we bid adieu! 🐇✨


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@mslabek mslabek marked this pull request as ready for review November 18, 2024 10:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (10)
engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala (1)

35-53: Consider expanding test coverage for robustness.

While the happy path is tested, consider adding test cases for:

  1. Edge cases (empty input, null values)
  2. Error scenarios (invalid templates)
  3. More detailed assertions about the output structure

Example additional test cases:

test("should handle empty input") {
  // Test with empty string input
}

test("should handle null values") {
  // Test with null input
}

test("should fail gracefully with invalid template") {
  // Test with malformed template
}
scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/InterpreterSpec.scala (1)

1089-1103: Consider consolidating redundant test cases.

These two individual test cases are already covered by the table-driven tests above. Consider removing them to avoid test duplication and maintain a more concise test suite.

-  test("spel template ast operation parameter should work for single literal value") {
-    val process = ScenarioBuilder
-      .streaming("test")
-      .source("start", "transaction-source")
-      .enricher(
-        "ex",
-        "out",
-        "templateAstOperationService",
-        "template" -> Expression.spelTemplate("Hello")
-      )
-      .buildSimpleVariable("result-end", resultVariable, "#out".spel)
-      .emptySink("end-end", "dummySink")
-
-    interpretProcess(process, Transaction(msisdn = "foo")) should equal("[Hello]-literal")
-  }
-
-  test("spel template ast operation parameter should work for single templated function call expression") {
-    val process = ScenarioBuilder
-      .streaming("test")
-      .source("start", "transaction-source")
-      .enricher(
-        "ex",
-        "out",
-        "templateAstOperationService",
-        "template" -> Expression.spelTemplate("#{#input.msisdn.toString()}")
-      )
-      .buildSimpleVariable("result-end", resultVariable, "#out".spel)
-      .emptySink("end-end", "dummySink")
-
-    interpretProcess(process, Transaction(msisdn = "foo")) should equal("[foo]-templated")
-  }

Also applies to: 1105-1119

scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/testcomponents/SpelTemplateAstOperationService.scala (1)

59-61: Handle potential exceptions during template evaluation

The call to template.evaluate(context) may throw exceptions if the evaluation fails. It's advisable to handle any potential exceptions to prevent unexpected runtime errors.

Apply this diff to handle exceptions during template evaluation:

         case template: TemplatedPart =>
-          s"[${template.evaluate(context)}]-templated"
+          try {
+            s"[${template.evaluate(context)}]-templated"
+          } catch {
+            case e: Exception =>
+              s"[Error evaluating template: ${e.getMessage}]"
+          }
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (1)

64-69: Consider type safety in casting with asInstanceOf[T]

In the LazyParmeterEvaluator.evaluate method, the result is cast using .asInstanceOf[T]. While this may be necessary, consider verifying the safety of this cast or using type-safe methods to prevent potential ClassCastException at runtime.

components-api/src/main/scala/pl/touk/nussknacker/engine/api/LazyParameter.scala (3)

3-3: Review the necessity of the import statement

Ensure that the import of TemplateExpression is required in this file. If it's not directly used, consider removing it to keep the imports clean.


72-74: Consider adding parentheses to templateExpression method

For consistency and clarity, consider adding parentheses to the templateExpression method, especially if it performs computations.

Apply this diff to add parentheses:

-    def templateExpression: TemplateExpression
+    def templateExpression(): TemplateExpression

83-85: Change evaluate from val to def

In the TemplatedPart trait, changing evaluate from a val to a def may better represent that it performs an evaluation each time it's called.

Apply this diff:

-        val evaluate: Evaluate[String]
+        def evaluate: Evaluate[String]
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (3)

87-87: Remove redundant val in case class parameter

In Scala, the val keyword in case class parameters is redundant since they are vals by default.

Apply this diff to remove the redundant val:

-  final case class NonTemplatedValue(val value: String) extends SpelTemplateSubexpression
+  final case class NonTemplatedValue(value: String) extends SpelTemplateSubexpression

84-94: Add unit tests for new subexpression classes

To ensure the correctness and reliability of the new SpelTemplateSubexpression trait and its case classes, consider adding unit tests that cover their behavior and interactions.


107-134: Add unit tests for the subexpressions method

The subexpressions method introduces important functionality in parsing and handling expressions. Adding unit tests covering various scenarios—such as composite expressions, literal expressions, and SpEL expressions—will help verify its correctness.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 14306e2 and d312b1a.

📒 Files selected for processing (9)
  • build.sbt (1 hunks)
  • components-api/src/main/scala/pl/touk/nussknacker/engine/api/LazyParameter.scala (2 hunks)
  • engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala (1 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameterCreator.scala (3 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/ParameterEvaluator.scala (1 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (3 hunks)
  • scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/InterpreterSpec.scala (5 hunks)
  • scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/testcomponents/SpelTemplateAstOperationService.scala (1 hunks)
🔇 Additional comments (14)
engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala (3)

1-25: LGTM! Well-structured test class with appropriate testing traits.

The test class follows Scala testing best practices by extending essential testing traits and organizing imports properly.


27-33: Consider adding documentation for the test runner configuration.

While the setup is correct, it would be helpful to document:

  1. Why an empty configuration is used
  2. Why batch execution mode is specifically chosen

Let's verify the SpelTemplateAstOperationService registration:


1-55: Verify test file naming convention.

The test file name TemplateLazyParameterTest appears appropriate, but let's verify it follows the project's test naming conventions.

✅ Verification successful

Test file naming follows project conventions

Based on the analysis of test files in the same directory, the naming convention TemplateLazyParameterTest follows the established pattern where test class names:

  • End with "Test" suffix
  • Use PascalCase
  • Describe the functionality being tested

Looking at neighboring test files like TableSourceTest, TableJoinTest, etc., confirms this is consistent with the project's conventions.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other test files in the same directory to verify naming patterns
fd -e scala . | rg "Test.scala$"

# Search for any documentation about test naming conventions
fd -e md . | xargs rg -l "test.*naming|naming.*convention"

Length of output: 20579


Script:

#!/bin/bash
# Let's analyze test files in the same directory to understand local naming conventions
fd -e scala "engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/"

# Also check for any test base classes that might define conventions
ast-grep --pattern 'class $_ extends $$$Test$$$'

Length of output: 786


Script:

#!/bin/bash
# Search for test files in the same directory with corrected fd command
fd --full-path ".*engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/.*Test\.scala$"

# Look for base test classes in the project
ast-grep --pattern 'class $name extends $$$Test {
  $$$
}'

Length of output: 1572

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameterCreator.scala (3)

45-53: Well-structured factory method implementation

Good use of the Factory pattern to encapsulate the creation logic. The code is clean, well-organized, and all necessary parameters are properly passed to the factory method.


97-98: Verify template parameter pass-through behavior

The TemplateLazyParameter[T] is handled as a pass-through, similar to CustomLazyParameter[T]. Please verify if template parameters require any transformation or preparation before evaluation, similar to how ProductLazyParameter and SequenceLazyParameter are handled.

#!/bin/bash
# Search for TemplateLazyParameter implementations to understand their evaluation requirements
ast-grep --pattern 'trait TemplateLazyParameter[T] {
  $$$
}'

ast-grep --pattern 'class $_ extends TemplateLazyParameter[$_] {
  $$$
}'

23-23: Verify the impact of return type change

The return type generalization from EvaluableLazyParameter[T] to LazyParameter[T] could be a breaking change if any code explicitly depends on EvaluableLazyParameter[T].

✅ Verification successful

Return type change is safe and part of internal refactoring

The change from EvaluableLazyParameter[T] to LazyParameter[T] is safe because:

  1. EvaluableLazyParameter is handled internally within the same file through pattern matching
  2. The change is part of a strategy-based factory pattern where EvaluableLazyParameter is one of the concrete implementations
  3. All usages properly handle the more generic LazyParameter type through pattern matching
  4. No external code directly depends on the concrete EvaluableLazyParameter return type
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for direct usage of EvaluableLazyParameter type
rg -t scala "EvaluableLazyParameter\[.*?\]" -A 3

Length of output: 139


Script:

#!/bin/bash
# Let's try a more comprehensive search for EvaluableLazyParameter
rg "EvaluableLazyParameter" -l

# Also search for LazyParameter to understand the type hierarchy
rg "LazyParameter" -l

# Search for direct usage of create method
rg "create\(.*deps.*\).*:.*LazyParameter" -t scala

Length of output: 14129


Script:

#!/bin/bash
# Let's check for references to EvaluableLazyParameterCreator.create to see how it's used
rg -t scala "EvaluableLazyParameterCreator.*create" -B 2 -A 2

# Also check for type casts or pattern matches that might depend on EvaluableLazyParameter
rg -t scala "case.*EvaluableLazyParameter|asInstanceOf\[EvaluableLazyParameter" -B 2 -A 2

Length of output: 3040

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/ParameterEvaluator.scala (1)

98-105: LGTM! Good use of factory pattern for parameter creation.

The refactoring to use EvaluableLazyParameterFactory.build improves the code structure by:

  • Centralizing parameter creation logic
  • Making the system more extensible for new parameter types
  • Providing better separation of concerns

Let's verify consistent usage of the factory pattern:

✅ Verification successful

Factory pattern implementation is correctly used

The direct instantiations found in the codebase are for different purposes and don't conflict with the factory pattern implementation:

  • EvaluableLazyParameterFactory.build is used for creating parameter instances based on editor language
  • new EvaluableLazyParameterCreator is a different class that extends CustomLazyParameter
  • new EvaluableLazyParameterCreatorDeps is just a dependency container
  • The direct new EvaluableLazyParameter instantiation in EvaluableLazyParameter.scala is within the factory itself
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any direct instantiations of EvaluableLazyParameter that should use the factory

# Search for new EvaluableLazyParameter instances
rg "new EvaluableLazyParameter" --type scala

# Search for all usages of the factory
rg "EvaluableLazyParameterFactory.build" --type scala

Length of output: 770


Script:

#!/bin/bash
# Let's check the content of EvaluableLazyParameter.scala to understand the relationship between factory and direct instantiation
rg -A 10 "object EvaluableLazyParameterFactory" --type scala

# Also check the EvaluableLazyParameterCreator usage context
rg -A 5 "class EvaluableLazyParameterCreator" --type scala

Length of output: 3595

scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/InterpreterSpec.scala (2)

1027-1041: LGTM! Clear and focused test case.

The test case effectively validates the combination of literal and templated values in SpEL expressions.


1043-1087: Well-structured table-driven tests with comprehensive coverage.

Good use of ScalaTest's table-driven testing to cover multiple scenarios:

  • Templated value and literal value combination
  • Single literal value
  • Single templated function call
  • Empty value

The test cases are well-organized and provide clear descriptions of each scenario.

build.sbt (1)

1819-1820: LGTM! Dependencies added for template lazy parameter testing

The addition of test dependencies to flinkTableApiComponents and scenarioCompiler is appropriate for implementing and testing the template lazy parameter feature.

scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/testcomponents/SpelTemplateAstOperationService.scala (2)

17-79: Overall implementation looks good

The code is well-structured and follows the project's conventions. The implementation correctly handles the template parameter and processes the parts as expected.


56-58: 🛠️ Refactor suggestion

Handle potential extraction and casting failures

Using extractValueUnsafe and casting with asInstanceOf can lead to runtime exceptions if the parameter extraction fails or if the type casting is invalid. Consider using extractValue and handling the possible None case to make the code safer.

Apply this diff to safely extract and cast the parameter:

-       val lazyParam = spelTemplateParameter
-         .extractValueUnsafe(params)
-         .asInstanceOf[TemplateLazyParameter[String]]
+       val lazyParamOption = spelTemplateParameter
+         .extractValue(params)
+         .flatMap(_.as[TemplateLazyParameter[String]])
+       lazyParamOption match {
+         case Some(lazyParam) =>
+           val result = lazyParam.templateExpression.parts.map {
+             case NonTemplatedPart(value) => s"[$value]-literal"
+             case template: TemplatedPart  => s"[${template.evaluate(context)}]-templated"
+           }.mkString
+           Future.successful(result)
+         case None =>
+           Future.failed(new IllegalArgumentException("Template parameter is missing or invalid"))
+       }

Likely invalid or redundant comment.

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (1)

28-60: SpelTemplateEvaluableLazyParameter implementation is correct

The new class SpelTemplateEvaluableLazyParameter appropriately extends TemplateLazyParameter and correctly implements the evaluate and templateExpression methods. The handling of SpEL expressions and subexpressions is well-structured.

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (1)

31-32: Imports are appropriate for the new functionality

The added imports align with the new code and are necessary for the introduced features.

Comment on lines 23 to 24
override val evaluate: Evaluate[T] =
LazyParmeterEvaluator.evaluate(compiledParameter, expressionEvaluator, nodeId, jobData)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Typo in LazyParmeterEvaluator class name

The class name LazyParmeterEvaluator is misspelled throughout the code. It should be LazyParameterEvaluator to maintain consistency and avoid confusion.

Apply the following diff to correct the typo:

-class LazyParmeterEvaluator {
+class LazyParameterEvaluator {

...

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

Also applies to: 35-36, 62-64

Comment on lines 120 to 130
Some(parsed.parsed match {
case compositeExpr: CompositeStringExpression =>
compositeExpr.getExpressions.toList.map {
case lit: LiteralExpression => NonTemplatedValue(lit.getExpressionString)
case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
createTemplatedExpression(spelExpr)
}
case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
List(createTemplatedExpression(spelExpr))
case litExpr: LiteralExpression => List(NonTemplatedValue(litExpr.getExpressionString))
})
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Handle unexpected expression types in pattern matching

In the subexpressions method, pattern matching on parsed.parsed may throw a MatchError if an unexpected expression type is encountered. Consider adding a default case to handle unforeseen types gracefully and provide informative error messages.

Apply this diff to include a default case:

             case litExpr: LiteralExpression => List(NonTemplatedValue(litExpr.getExpressionString))
+            case other =>
+              throw new IllegalArgumentException(s"Unsupported expression type: ${other.getClass.getName}")
📝 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
Some(parsed.parsed match {
case compositeExpr: CompositeStringExpression =>
compositeExpr.getExpressions.toList.map {
case lit: LiteralExpression => NonTemplatedValue(lit.getExpressionString)
case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
createTemplatedExpression(spelExpr)
}
case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
List(createTemplatedExpression(spelExpr))
case litExpr: LiteralExpression => List(NonTemplatedValue(litExpr.getExpressionString))
})
Some(parsed.parsed match {
case compositeExpr: CompositeStringExpression =>
compositeExpr.getExpressions.toList.map {
case lit: LiteralExpression => NonTemplatedValue(lit.getExpressionString)
case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
createTemplatedExpression(spelExpr)
}
case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
List(createTemplatedExpression(spelExpr))
case litExpr: LiteralExpression => List(NonTemplatedValue(litExpr.getExpressionString))
case other =>
throw new IllegalArgumentException(s"Unsupported expression type: ${other.getClass.getName}")
})

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (5)
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2)

54-56: Improve error messages for better debugging

The error messages could be more descriptive to help with debugging:

  • Include the actual expression type received
  • Explain what types are expected
-          throw new IllegalStateException("Non SpEL-template expression received in SpelTemplateLazyParameter")
+          throw new IllegalStateException(s"Expected SpEL template expression but got: ${expression.getClass.getSimpleName}")
-    case _ => throw new IllegalStateException("Non SpEL expression received in SpelTemplateLazyParameter")
+    case other => throw new IllegalStateException(s"Expected SpelExpression but got: ${other.getClass.getSimpleName}")

42-52: Consider simplifying nested pattern matching

The nested pattern matching could be simplified using a more functional approach with map and pattern matching in a single expression.

-          val templateParts = subexpressions.map {
-            case TemplatedExpression(expression) => {
-              new TemplateExpressionPart.TemplatedPart {
-                override val evaluate: Evaluate[String] = context => {
-                  expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData).value
-                }
-              }
-            }
-            case NonTemplatedValue(value) => TemplateExpressionPart.NonTemplatedPart(value)
-          }
-          TemplateExpression(templateParts)
+          TemplateExpression(subexpressions.map {
+            case TemplatedExpression(expression) => 
+              TemplateExpressionPart.TemplatedPart(context => 
+                expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData).value)
+            case NonTemplatedValue(value) => 
+              TemplateExpressionPart.NonTemplatedPart(value)
+          })
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (3)

84-93: LGTM! Consider adding scaladoc.

The sealed trait hierarchy is well-designed for handling template expressions. Consider adding scaladoc to document:

  • The purpose of SpelTemplateSubexpression
  • When to use each case class
  • Expected behavior of the evaluate method
+/**
+ * Represents parts of a template expression, which can be either static text (NonTemplatedValue)
+ * or dynamic expressions that need evaluation (TemplatedExpression).
+ */
 sealed trait SpelTemplateSubexpression

 object SpelTemplateSubexpression {
+  /**
+   * Represents a static part of the template that doesn't require evaluation.
+   * @param value The static string value
+   */
   final case class NonTemplatedValue(val value: String) extends SpelTemplateSubexpression

+  /**
+   * Represents a dynamic part of the template that requires evaluation.
+   * @param expression The SpelExpression to be evaluated
+   */
   final case class TemplatedExpression(expression: SpelExpression) extends SpelTemplateSubexpression {
     def evaluate: (Context, Map[String, Any]) => String = expression.evaluate[String]
   }
 }

107-136: Consider refactoring for improved maintainability.

The method implementation is functionally correct but could benefit from some improvements:

  1. Move helper method to companion object:
 object SpelTemplateSubexpression {
+  private def createTemplatedExpression(
+    expression: org.springframework.expression.spel.standard.SpelExpression,
+    parser: () => ValidatedNel[ExpressionParseError, Expression],
+    evaluationContextPreparer: EvaluationContextPreparer
+  ): TemplatedExpression = {
+    val parsedTemplateExpr = ParsedSpelExpression(expression.getExpressionString, parser, expression)
+    val compiledExpr = new SpelExpression(
+      parsedTemplateExpr,
+      typing.Typed[String],
+      Standard,
+      evaluationContextPreparer
+    )
+    TemplatedExpression(compiledExpr)
+  }
 }
  1. Simplify pattern matching:
-    flavour.languageId match {
-      case Language.SpelTemplate =>
-        Some(parsed.parsed match {
+    Option.when(flavour.languageId == Language.SpelTemplate) {
+      parsed.parsed match {
  1. Add documentation:
+  /**
+   * Decomposes the expression into template subexpressions if this is a template expression.
+   * @return Some(List[SpelTemplateSubexpression]) for template expressions, None otherwise
+   */
   def templateSubexpressions: Option[List[SpelTemplateSubexpression]] = {

130-131: Enhance error message for unsupported expression types.

The error message could be more descriptive to help with debugging.

-            throw new IllegalArgumentException(s"Unsupported expression type: [${other.getClass.getName}]")
+            throw new IllegalArgumentException(
+              s"Unsupported expression type in template: [${other.getClass.getName}]. " +
+              "Expected CompositeStringExpression, SpelExpression, or LiteralExpression."
+            )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between d312b1a and 01f949b.

📒 Files selected for processing (2)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (3 hunks)
🔇 Additional comments (2)
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2)

Line range hint 15-26: LGTM! Clean implementation of EvaluableLazyParameter

The class properly implements CustomLazyParameter with type safety and clear delegation to LazyParameterEvaluator.


88-104: Consider decoupling parameter type determination from editor type

As noted in the previous review, basing the parameter type on the editor definition creates tight coupling. Consider:

  1. Adding an explicit isTemplateParameter flag to ParameterDef
  2. Validating editor compatibility separately

This would make the system more maintainable and the intent more explicit.

Let's check if this pattern is used elsewhere in the codebase:

Comment on lines +64 to 73
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]
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}"
)
}

interpretProcess(process, Transaction(msisdn = "foo")) should equal("[Hello]-literal")
}

test("spel template ast operation parameter should work for single templated function call expression") {
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to have a test for the case when PostponedEvaluatorLazyParameterStrategy is used. AFAIK it is used in source/sink/custom nodes combined with Flink engine.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this test is in TemplateLazyParameterTest. But weirdly, the test passes without changes in EvaluableLazyParameterCreator :/ still trying to figure it out

Also I parameterized all test cases into one test and forgot to delete these tests here

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, so the EvaluableLazyParameterCreator doesnt get used when its parameter of EagerService. I tested it on a source and its not working. Unfortunately I see no simple way to add support for it.

Right now I'm trying an approach with an EvaluableLazyTemplateParameterCreator - something like EvaluableLazyParameterCreator but with a custom dedicated function in FlinkLazyParameterFunctionHelper that doesnt evaluate the parameter like normal, but allows access to template subexpression. I'm hoping it works, but am having some trouble with it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Way more complex than I hoped :( need some advice. let's talk about it tomorrow if you'll have the time

build.sbt Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (9)
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2)

38-57: Enhance error handling and simplify pattern matching

The template expression handling could be improved in several ways:

  1. Error messages should be more descriptive and include actual expression types
  2. The nested pattern matching could be simplified

Consider this refactoring:

  override def templateExpression: TemplateExpression = compiledParameter.expression match {
    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
-               }
-             }
-           }
-           case Literal(value) => TemplateExpressionPart.Literal(value)
-         }
-         TemplateExpression(templateParts)
-       case None =>
-         throw new IllegalStateException("Non SpEL-template expression received in SpelTemplateLazyParameter")
-     }
-   case _ => throw new IllegalStateException("Non SpEL expression received in SpelTemplateLazyParameter")
+     expression.templateSubexpressions.map { subexpressions =>
+       val templateParts = subexpressions.map {
+         case Placeholder(expr) => new TemplateExpressionPart.Placeholder {
+           override val evaluate: Evaluate[String] = context =>
+             expressionEvaluator.evaluate[String](expr, "expressionId", nodeId.id, context)(jobData).value
+         }
+         case Literal(value) => TemplateExpressionPart.Literal(value)
+       }
+       TemplateExpression(templateParts)
+     }.getOrElse(throw new IllegalStateException(
+       s"Expression ${expression} is not a SpEL template expression"
+     ))
+   case other => throw new IllegalStateException(
+     s"Expected SpEL expression but got: ${other.getClass.getName}"
+   )

35-36: Consider extracting common evaluation logic

The evaluate implementation is duplicated from EvaluableLazyParameter. Consider extracting this to a trait or using composition to avoid duplication.

components-api/src/main/scala/pl/touk/nussknacker/engine/api/LazyParameter.scala (2)

72-74: Add documentation for the new trait

Consider adding scaladoc to explain:

  • The purpose and use cases of TemplateLazyParameter
  • The relationship between template expressions and lazy parameter evaluation
  • Example usage demonstrating template interpolation

Example documentation:

/**
  * Represents a lazy parameter that supports template expressions with interpolated values.
  * Template expressions consist of literal parts and placeholders that are evaluated lazily.
  *
  * Example usage:
  * {{{
  *   val template = new TemplateLazyParameter[String] {
  *     def templateExpression = TemplateExpression(List(
  *       Literal("Hello "),
  *       Placeholder(ctx => ctx.name)
  *     ))
  *     // ... other implementations
  *   }
  * }}}
  *
  * @tparam T the type of the evaluated expression
  */

83-85: Consider making Placeholder's evaluate type more flexible

The Placeholder trait's evaluate method is fixed to return String. Consider parameterizing it to support different types of interpolated values:

-      trait Placeholder extends TemplateExpressionPart {
-        val evaluate: Evaluate[String]
-      }
+      trait Placeholder[T] extends TemplateExpressionPart {
+        val evaluate: Evaluate[T]
+        def asString: Evaluate[String] // For template rendering
+      }

This would allow for type-safe interpolation of non-string values while maintaining the ability to render them as strings in the final template.

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (4)

103-103: Add documentation for the templateSubexpressions method

Consider adding ScalaDoc to explain the purpose of this method, its return type semantics, and when it should be used.


104-113: Consider extracting placeholder creation logic

The createEvaluablePlaceholder helper function uses hardcoded values for typing and flavor. Consider:

  1. Making these parameters configurable
  2. Moving this logic to a separate factory method
  3. Adding error handling for parser failures
- def createEvaluablePlaceholder(expression: org.springframework.expression.spel.standard.SpelExpression) = {
+ private def createPlaceholder(
+     expression: org.springframework.expression.spel.standard.SpelExpression,
+     expectedType: TypingResult = typing.Typed[String],
+     exprFlavor: Flavour = Standard
+ ): Placeholder = {
    val parsedTemplateExpr = ParsedSpelExpression(expression.getExpressionString, parsed.parser, expression)
    val compiledExpr = new SpelExpression(
      parsedTemplateExpr,
-     typing.Typed[String],
-     Standard,
+     expectedType,
+     exprFlavor,
      evaluationContextPreparer
    )
    Placeholder(compiledExpr)
  }

114-131: Simplify nested pattern matching logic

The current implementation has deep nesting which could be simplified using early returns or pattern matching decomposition.

Consider restructuring like this:

def templateSubexpressions: Option[List[SpelTemplateExpressionPart]] = {
  if (flavour.languageId != Language.SpelTemplate) return None
  
  Some(parsed.parsed match {
    case compositeExpr: CompositeStringExpression =>
      compositeExpr.getExpressions.toList.map(expressionToPart)
    case expr => List(expressionToPart(expr))
  })
}

private def expressionToPart(expr: Expression): SpelTemplateExpressionPart = expr match {
  case lit: LiteralExpression => Literal(lit.getExpressionString)
  case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
    createPlaceholder(spelExpr)
  case other =>
    throw new IllegalArgumentException(
      s"Unsupported expression type: [${other.getClass.getName}]. Expected: LiteralExpression or SpelExpression"
    )
}

126-127: Enhance error message for unsupported expression types

The current error message could be more helpful by including:

  1. The expected types
  2. The actual expression content
  3. Guidance on how to fix the issue
- throw new IllegalArgumentException(s"Unsupported expression type: [${other.getClass.getName}]")
+ throw new IllegalArgumentException(
+   s"""Unsupported expression type: [${other.getClass.getName}] for expression: '${other.getExpressionString}'
+      |Expected: LiteralExpression or SpelExpression for template parsing.
+      |Please ensure the expression follows the template syntax.""".stripMargin)
scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/InterpreterSpec.scala (1)

1028-1053: Consider adding more test cases for comprehensive coverage.

The current test cases cover basic scenarios well. Consider adding these additional cases for more thorough testing:

  • Multiple templated expressions (e.g., Hello#{#input.msisdn}#{#input.accountId})
  • Nested function calls (e.g., #{#input.msisdn.toLowerCase().trim()})
  • Special characters in templates (e.g., Hello @#{#input.msisdn}!)
 val testCases = Seq(
   // ... existing test cases ...
+  (
+    "multiple templated expressions",
+    s"Hello#{#input.msisdn}#{#input.accountId}",
+    Transaction(msisdn = "foo", accountId = "123"),
+    "[Hello]-literal[foo]-templated[123]-templated"
+  ),
+  (
+    "nested function calls in template",
+    "#{#input.msisdn.toLowerCase().trim()}",
+    Transaction(msisdn = "FOO "),
+    "[foo]-templated"
+  ),
+  (
+    "special characters in template",
+    "Hello @#{#input.msisdn}!",
+    Transaction(msisdn = "foo"),
+    "[Hello @]-literal[foo]-templated[!]-literal"
+  ),
 )
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 01f949b and a7c4ee6.

📒 Files selected for processing (5)
  • components-api/src/main/scala/pl/touk/nussknacker/engine/api/LazyParameter.scala (2 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (3 hunks)
  • scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/InterpreterSpec.scala (5 hunks)
  • scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/testcomponents/SpelTemplateAstOperationService.scala (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/testcomponents/SpelTemplateAstOperationService.scala
🔇 Additional comments (7)
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (3)

Line range hint 15-26: LGTM: Clean implementation with good separation of concerns

The class implementation is clean and follows good practices with proper type parameterization and delegation of evaluation logic.


64-73: Reusing past review comment about type casting safety

The unsafe type casting issue mentioned in the previous review is still present and should be addressed.


80-105: Consider decoupling parameter type determination from editor implementation

While the current implementation works, it tightly couples parameter type determination to editor implementation. As mentioned in previous reviews, consider:

  1. Adding an explicit isTemplateParameter flag to Parameter
  2. Using expression type-based determination instead of editor-based determination

This would make the system more flexible and easier to extend in the future.

Let's verify the current usage of editor types in the codebase:

components-api/src/main/scala/pl/touk/nussknacker/engine/api/LazyParameter.scala (1)

3-3: LGTM: Import statement is correctly placed

The self-reference import for TemplateExpression follows Scala's idiomatic practices.

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (1)

84-89: Consider revising the naming scheme for template expression parts

As mentioned in the previous review, the current naming scheme (SpelTemplateExpressionPart, Literal, Placeholder) could be improved to be more intuitive in spoken language and documentation. Consider alternatives that better describe the role of each part in template expressions.

Some alternative naming suggestions:

  • SpelTemplateSegment with StaticSegment/DynamicSegment
  • TemplateElement with TextElement/ExpressionElement
scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/InterpreterSpec.scala (2)

9-10: LGTM: Import for table-driven tests.

The import for table-driven property tests is appropriate for the new parameterized test case.


1027-1071: LGTM: Well-structured table-driven test for template AST operations.

The test implementation is clean and covers essential scenarios using a table-driven approach with clear test case descriptions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (4)
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2)

52-54: Improve error messages for better debugging

The error messages in the exception cases could be more descriptive to help with debugging. Include more context about the actual expression type received.

-          throw new IllegalStateException("Non SpEL-template expression received in SpelTemplateLazyParameter")
+          throw new IllegalStateException(s"Expected SpEL-template expression but got: ${expression.templateSubexpressions}")
-    case _ => throw new IllegalStateException("Non SpEL expression received in SpelTemplateLazyParameter")
+    case other => throw new IllegalStateException(s"Expected SpelExpression but got: ${other.getClass.getSimpleName}")

41-49: Consider simplifying the pattern matching logic

The nested pattern matching with a single-expression block can be simplified for better readability.

-            case Placeholder(expression) => {
-              new TemplateExpressionPart.Placeholder {
-                override val evaluate: Evaluate[String] = context => {
-                  expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData).value
-                }
-              }
-            }
+            case Placeholder(expression) => new TemplateExpressionPart.Placeholder {
+              override val evaluate: Evaluate[String] = context =>
+                expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData).value
+            }
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (2)

84-89: Consider revising the naming based on previous feedback

The structure using a sealed trait with final case classes is well-designed. However, as mentioned in the previous review, the current naming might be confusing in spoken language. Consider alternatives that better describe the parts of a template:

-sealed trait SpelTemplateExpressionPart
-object SpelTemplateExpressionPart {
-  final case class Literal(value: String) extends SpelTemplateExpressionPart
-  final case class Placeholder(expression: SpelExpression) extends SpelTemplateExpressionPart
+sealed trait TemplateSegment
+object TemplateSegment {
+  final case class StaticText(value: String) extends TemplateSegment
+  final case class EvaluableExpression(expression: SpelExpression) extends TemplateSegment

This makes it clearer that a template consists of static text segments and evaluable expression segments.


103-122: Consider optimizing template parsing performance

The implementation is functionally correct but could benefit from performance optimizations:

  1. The creation of new SpelExpression instances inside parseTemplate could be expensive, especially for complex templates. Consider caching or reusing instances where possible.

  2. The recursive parsing of composite expressions could be optimized using a more efficient collection transformation.

Here's a suggested optimization:

 def templateSubexpressions: Option[List[SpelTemplateExpressionPart]] = {
   def parseTemplate(expression: Expression): List[SpelTemplateExpressionPart] = expression match {
     case lit: LiteralExpression => List(Literal(lit.getExpressionString))
     case spelExpr: org.springframework.expression.spel.standard.SpelExpression =>
-      val parsedTemplateExpr = ParsedSpelExpression(spelExpr.getExpressionString, parsed.parser, spelExpr)
-      val compiledExpr = new SpelExpression(
-        parsedTemplateExpr,
-        typing.Typed[String],
-        Standard,
-        evaluationContextPreparer
-      )
-      List(Placeholder(compiledExpr))
+      // Cache the parsed expression using memoization or a cache map
+      List(Placeholder(getOrCreateSpelExpression(spelExpr)))
     case compositeExpr: CompositeStringExpression =>
-      compositeExpr.getExpressions.toList.flatMap(parseTemplate)
+      // Use more efficient collection transformation
+      compositeExpr.getExpressions.iterator.map(parseTemplate).flatten.toList
     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 def getOrCreateSpelExpression(spelExpr: org.springframework.expression.spel.standard.SpelExpression): SpelExpression = {
+  // Implement caching logic here
+  val parsedTemplateExpr = ParsedSpelExpression(spelExpr.getExpressionString, parsed.parser, spelExpr)
+  new SpelExpression(
+    parsedTemplateExpr,
+    typing.Typed[String],
+    Standard,
+    evaluationContextPreparer
+  )
+}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a7c4ee6 and 8bc5ecd.

📒 Files selected for processing (5)
  • engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala (1 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (2 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameterCreator.scala (3 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/ParameterEvaluator.scala (1 hunks)
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala
  • scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/ParameterEvaluator.scala
🔇 Additional comments (5)
scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameter.scala (1)

85-101: Consider alternative approaches to parameter type determination

The current implementation uses language-based pattern matching to determine the parameter type. As discussed in previous reviews, there might be more robust alternatives:

  1. Add an isTemplateParameter flag in Parameter definition
  2. Use pattern matching on the expression type itself

The current approach couples the parameter type to the language, which might be too restrictive.

Let's verify the expression types in use:

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/EvaluableLazyParameterCreator.scala (2)

45-52: LGTM! Good use of the factory pattern.

The refactoring to use EvaluableLazyParameterFactory.build improves encapsulation and maintainability by centralizing the creation logic in a dedicated factory.


23-23: Verify the impact of the API change.

The return type change from EvaluableLazyParameter[T] to LazyParameter[T] makes the API more flexible but could affect existing code that expects specifically EvaluableLazyParameter[T].

✅ Verification successful

Let me gather more context about the usage patterns to better understand the impact.


Let me search for one more pattern to understand how the return value is used.


Based on the evidence gathered from the shell scripts, I can now provide a final response:

Return type change from EvaluableLazyParameter[T] to LazyParameter[T] is safe

The change is well-contained and safe because:

  • The implementation still handles EvaluableLazyParameter through pattern matching as seen in the original file
  • No direct dependencies on EvaluableLazyParameter return type were found in the codebase
  • The default strategy in ParameterEvaluator already works with the generic LazyParameter type
  • This change provides better flexibility while maintaining backward compatibility
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for direct usage of EvaluableLazyParameter type to identify potential breaking changes
rg -t scala "EvaluableLazyParameter\[.*?\]" --no-heading

Length of output: 1197


Script:

#!/bin/bash
# Search for pattern matching and type casting involving LazyParameter and EvaluableLazyParameter
rg -t scala "LazyParameter.*?[=:].*?Evaluable" -A 2 --no-heading

# Search for create method invocations
ast-grep --pattern 'create($$$)'

Length of output: 3752


Script:

#!/bin/bash
# Look for usages of the create method in EvaluableLazyParameterCreator and how its return value is handled
rg -t scala "EvaluableLazyParameterCreator.*?create" -B 2 -A 4 --no-heading

# Also check for any pattern matching on LazyParameter
ast-grep --pattern 'case $_: LazyParameter[$_] => $$$'

Length of output: 134

scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpression.scala (2)

31-32: LGTM: Import statements are well-organized

The new imports are correctly scoped and necessary for the template expression functionality.


103-122: Verify template parsing edge cases

The implementation should be tested with various template patterns to ensure robustness.

✅ Verification successful

Template parsing implementation appears sufficiently tested

The implementation has adequate test coverage and error handling:

  • Template expressions are tested in TemplateLazyParameterTest with real-world usage patterns (e.g., Hello#{#input})
  • Error handling for unsupported expressions is implemented
  • The implementation handles all standard SpEL template cases: literals, SpEL expressions, and composite expressions
  • The code is used in production scenarios as evidenced by the test cases
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for template expression test cases to verify coverage
rg -l "SpelTemplate|templateSubexpressions" --type scala --glob "*Test*.scala"

# Look for actual template usage in the codebase
ast-grep --pattern 'Language.SpelTemplate'

Length of output: 6742


Script:

#!/bin/bash
# Let's examine the test files to see how template expressions are tested
rg -A 10 "SpelTemplate|templateSubexpressions" scenario-api/src/test/scala/pl/touk/nussknacker/engine/ScenarioApiShowcasesTest.scala

# Check template lazy parameter tests
rg -A 10 "SpelTemplate|templateSubexpressions" engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala

# Look for actual template expression implementations
ast-grep --pattern 'class $_ {
  $$$
  templateSubexpressions$$$
  $$$
}'

Length of output: 1490


Script:

#!/bin/bash
# Let's look for test implementations of SpelTemplateAstOperationService
rg -A 15 "class SpelTemplateAstOperationService" 

# Search for any test cases involving template expressions
rg -A 10 "test.*template" --type scala --glob "*Test*.scala"

# Look for template-related error handling tests
rg "throw.*IllegalArgumentException.*Unsupported expression" --type scala

Length of output: 2094

Comment on lines +43 to +45
override val evaluate: Evaluate[String] = context => {
expressionEvaluator.evaluate[String](expression, "expressionId", nodeId.id, context)(jobData).value
}
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.

Comment on lines +96 to +97
case p: CustomLazyParameter[T] => p
case p: TemplateLazyParameter[T] => p
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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (3)
engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala (3)

62-62: Remove debug println statement

Debug print statements should be removed before merging.

-    println(result)

51-68: Consider adding more test cases

While the current test case validates basic functionality, consider adding tests for:

  • Empty input values
  • Special characters in template
  • Multiple placeholders
  • Error cases (invalid templates)

72-76: Add documentation for the sink implementation

Please add ScalaDoc comments explaining:

  • The purpose of this sink
  • Expected input format
  • Template processing behavior
  • Any limitations or requirements
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 8bc5ecd and 9e917bc.

📒 Files selected for processing (1)
  • engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala (1 hunks)
🔇 Additional comments (3)
engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/TemplateLazyParameterTest.scala (3)

1-42: LGTM! Well-structured test class setup

The test class follows best practices by extending appropriate testing traits and importing necessary dependencies.


43-50: LGTM! Appropriate test runner configuration

The runner is well-configured with lazy initialization and batch mode execution, properly registering the template operation sink component.


95-95: Verify empty nodeDependencies list

The nodeDependencies method returns an empty list. Please verify if this is intentional and add a comment explaining why no dependencies are needed.

Comment on lines +113 to +127
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")

}
})
}

}
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

Comment on lines +117 to +122
println(dataStream)
dataStream.addSink(new SinkFunction[ValueWithContext[String]] {
override def invoke(value: ValueWithContext[String], context: SinkFunction.Context): Unit = {
println(value)
println("debug")

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.

}
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

@@ -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

def parseTemplate(expression: Expression): List[SpelTemplateExpressionPart] = expression match {
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

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants