Skip to content

Commit

Permalink
[NU-1836] Add list methods as extension methods to array (#7032)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasz-bigorajski authored Oct 21, 2024
1 parent ba4f89a commit 8502620
Show file tree
Hide file tree
Showing 22 changed files with 432 additions and 211 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,6 @@ object typing {
override def withoutValue: SingleTypingResult

def runtimeObjType: TypedClass

// This type should be used only for: type hints, suggester and validation
def typeHintsObjType: TypedClass =
if (runtimeObjType.klass.isArray) TypedClass(classOf[java.util.List[_]], runtimeObjType.params)
else runtimeObjType

}

object TypedObjectTypingResult {
Expand Down Expand Up @@ -215,7 +209,7 @@ object typing {
override def withoutValue: TypedClass = this

override def display: String = {
val className = ReflectUtils.simpleNameWithoutSuffix(typeHintsObjType.klass)
val className = if (klass.isArray) "List" else ReflectUtils.simpleNameWithoutSuffix(runtimeObjType.klass)
if (params.nonEmpty) s"$className[${params.map(_.display).mkString(",")}]"
else className
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ class ExpressionSuggesterSpec
}

test("should suggest parameters for casts methods") {
spelSuggestionsFor("#unknown.canCastTo('')", column = 20).toSet shouldBe Set(
spelSuggestionsFor("#unknown.canCastTo('')", column = 20) should contain theSameElementsAs List(
suggestion("String", Typed[String]),
suggestion("Duration", Typed[Duration]),
suggestion("LocalDateTime", Typed[LocalDateTime]),
Expand All @@ -794,6 +794,35 @@ class ExpressionSuggesterSpec
)
}

test("should suggest the same methods for list and array") {
val suggester = new ExpressionSuggester(
expressionConfig,
ClassDefinitionTestUtils.createDefinitionWithDefaultsAndExtensions,
dictServices,
getClass.getClassLoader,
Nil
)
val variables = Map(
"list" -> Typed[java.util.List[String]],
"array" -> Typed.genericTypeClass(classOf[Array[String]], List(Typed[String])),
)
val listSpelExpression = Expression.spel("#list.")
val arraySpelExpression = Expression.spel("#array.")

def suggestion(expression: Expression): List[ExpressionSuggestion] =
suggester
.expressionSuggestions(
expression,
CaretPosition2d(0, expression.expression.length),
variables
)(ExecutionContext.global)
.futureValue

val listMethodsSuggestion = suggestion(listSpelExpression)
val arrayMethodsSuggestion = suggestion(arraySpelExpression)
listMethodsSuggestion should contain theSameElementsAs arrayMethodsSuggestion
}

}

object ExpressionSuggesterTestData {
Expand Down
3 changes: 1 addition & 2 deletions docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
in table source and sink components in `Table` parameter
* [#6950](https://github.com/TouK/nussknacker/pull/6950) Fix for testing mechanism for table sources: using full, model classpath instead of only flinkTable.jar
* [#6716](https://github.com/TouK/nussknacker/pull/6716) Fix type hints for #COLLECTION.merge function.
* [#6695](https://github.com/TouK/nussknacker/pull/6695) From now on, arrays on UI are visible as lists but on a
background they are stored as it is and SpeL converts them to lists in a runtime.
* [#6695](https://github.com/TouK/nussknacker/pull/6695) [7032](https://github.com/TouK/nussknacker/pull/7032) From now on, arrays on UI are visible as lists but on a background they are stored as it is.
* [#6750](https://github.com/TouK/nussknacker/pull/6750) Add varargs to `#COLLECTION.concat` and `#COLLECTION.merge`.
* [#6778](https://github.com/TouK/nussknacker/pull/6778) SpeL: check for methods if a property for a given name does not exist.
* [#6769](https://github.com/TouK/nussknacker/pull/6769) Added possibility to choose presets and define lists for Long typed parameter inputs in fragments.
Expand Down
1 change: 0 additions & 1 deletion docs/MigrationGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ To see the biggest differences please consult the [changelog](Changelog.md).
* `scenarioActivityManager` can be used by any `DeploymentManager` to save scenario activities in the Nu database
* there is `NoOpScenarioActivityManager` implementation available (if needed for tests etc.)
* [#6695](https://github.com/TouK/nussknacker/pull/6695) `SingleTypingResult` API changes:
* Added `typeHintsObjType` which is used as a type for a type hints, suggester and validation.
* Renamed `objType` to `runtimeObjType` which indicates a current object in a runtime.
* [#6766](https://github.com/TouK/nussknacker/pull/6766)
* Process API changes:
Expand Down
26 changes: 13 additions & 13 deletions docs/scenarios_authoring/Spel.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,19 @@ Explicit conversions are available in utility classes and build-in java conversi
| `#DATE_FORMAT.parseOffsetDateTime('2018-10-23T12:12:13+00:00')` | 1540296720000 | OffsetDateTime |
| `#DATE_FORMAT.parseLocalDateTime('2018-10-23T12:12:13')` | 2018-10-23T12:12:13+00:00 | LocalDateTime |

### Casting

When a type cannot be determined by parser, the type is presented as `Unknown`. When we know what the type will be on
runtime, we can cast a given type, and then we can operate on the cast type.

E.g. having a variable `obj` of a type: `List[Unknown]` and we know the elements are strings then we can cast elements
to String: `#obj.![#this.castToOrNull('String')]`.

Available methods:
- `canCastTo` - checks if a type can be cast to a given class.
- `castTo` - casts a type to a given class or throws exception if type cannot be cast.
- `castToOrNull` - casts a type to a given class or return null if type cannot be cast.

## Built-in helpers

Nussknacker comes with the following helpers:
Expand Down Expand Up @@ -376,16 +389,3 @@ On the other hand, formatter created using `#DATE_FORMAT.formatter()` method wil
- `#DATE_FORMAT.lenientFormatter('yyyy-MM-dd EEEE', 'PL')` - creates lenient version `DateTimeFormatter` using given pattern and locale

For full list of available format options take a look at [DateTimeFormatter api docs](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/format/DateTimeFormatter.html).

## Casting.

When a type cannot be determined by parser, the type is presented as `Unknown`. When we know what the type will be on
runtime, we can cast a given type, and then we can operate on the cast type.

E.g. having a variable `obj` of a type: `List[Unknown]` and we know the elements are strings then we can cast elements
to String: `#obj.![#this.castToOrNull('String')]`.

Available methods:
- `canCastTo` - checks if a type can be cast to a given class.
- `castTo` - casts a type to a given class or throws exception if type cannot be cast.
- `castToOrNull` - casts a type to a given class or return null if type cannot be cast.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,103 @@
"refClazzName": "[Ljava.lang.Object;"
},
"methods": {
"contains": [
{
"name": "contains",
"signature": {
"noVarArgs": [
{"name": "arg0", "refClazz": {"type": "Unknown"}}
],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"containsAll": [
{
"name": "containsAll",
"signature": {
"noVarArgs": [
{
"name": "arg0",
"refClazz": {
"params": [
{
"display": "Unknown",
"params": [],
"refClazzName": "java.lang.Object",
"type": "Unknown"
}
],
"refClazzName": "java.util.Collection"
}
}
],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"empty": [
{
"name": "empty",
"signature": {
"noVarArgs": [],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"get": [
{
"name": "get",
"signatures": [
{
"noVarArgs": [
{"name": "arg0", "refClazz": {"refClazzName": "java.lang.Integer"}}
],
"result": {"type": "Unknown"}
}
]
}
],
"indexOf": [
{
"name": "indexOf",
"signature": {
"noVarArgs": [
{"name": "arg0", "refClazz": {"type": "Unknown"}}
],
"result": {"refClazzName": "java.lang.Integer"}
}
}
],
"isEmpty": [
{
"name": "isEmpty",
"signature": {
"noVarArgs": [],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"lastIndexOf": [
{
"name": "lastIndexOf",
"signature": {
"noVarArgs": [
{"name": "arg0", "refClazz": {"type": "Unknown"}}
],
"result": {"refClazzName": "java.lang.Integer"}
}
}
],
"size": [
{
"name": "size",
"signature": {
"noVarArgs": [],
"result": {"refClazzName": "java.lang.Integer"}
}
}
],
"toString": [
{
"name": "toString",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,103 @@
"refClazzName": "[Ljava.lang.Object;"
},
"methods": {
"contains": [
{
"name": "contains",
"signature": {
"noVarArgs": [
{"name": "arg0", "refClazz": {"type": "Unknown"}}
],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"containsAll": [
{
"name": "containsAll",
"signature": {
"noVarArgs": [
{
"name": "arg0",
"refClazz": {
"params": [
{
"display": "Unknown",
"params": [],
"refClazzName": "java.lang.Object",
"type": "Unknown"
}
],
"refClazzName": "java.util.Collection"
}
}
],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"empty": [
{
"name": "empty",
"signature": {
"noVarArgs": [],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"get": [
{
"name": "get",
"signatures": [
{
"noVarArgs": [
{"name": "arg0", "refClazz": {"refClazzName": "java.lang.Integer"}}
],
"result": {"type": "Unknown"}
}
]
}
],
"indexOf": [
{
"name": "indexOf",
"signature": {
"noVarArgs": [
{"name": "arg0", "refClazz": {"type": "Unknown"}}
],
"result": {"refClazzName": "java.lang.Integer"}
}
}
],
"isEmpty": [
{
"name": "isEmpty",
"signature": {
"noVarArgs": [],
"result": {"refClazzName": "java.lang.Boolean"}
}
}
],
"lastIndexOf": [
{
"name": "lastIndexOf",
"signature": {
"noVarArgs": [
{"name": "arg0", "refClazz": {"type": "Unknown"}}
],
"result": {"refClazzName": "java.lang.Integer"}
}
}
],
"size": [
{
"name": "size",
"signature": {
"noVarArgs": [],
"result": {"refClazzName": "java.lang.Integer"}
}
}
],
"toString": [
{
"name": "toString",
Expand Down Expand Up @@ -15611,4 +15708,4 @@
]
}
}
]
]
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object TypingFunctionForClassMember {
}

private def extractParameters(invocationTargetClass: SingleTypingResult): List[TypingResult] = {
invocationTargetClass.typeHintsObjType.params
invocationTargetClass.runtimeObjType.params
}

protected def resultTypeBasedOnGenericParam(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@
import org.springframework.expression.spel.support.ReflectionHelper;
import org.springframework.expression.spel.support.ReflectiveMethodExecutor;
import org.springframework.util.ReflectionUtils;
import pl.touk.nussknacker.engine.spel.internal.ConversionAndExtensionsAwareMethodInvoker;
import pl.touk.nussknacker.engine.extension.ExtensionsAwareMethodInvoker;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

//this basically changed org.springframework.expression.spel.support.ReflectiveMethodExecutor
//we want to create TypeDescriptor using SpelEspReflectionHelper.convertArguments
//which should work faster
// As an additional feature we allow to invoke list methods on arrays and
// in the point of the method invocation we convert an array to a list
// As an additional feature we allow to invoke defined extension methods
public class NuReflectiveMethodExecutor extends ReflectiveMethodExecutor {

private final Method method;
Expand All @@ -30,10 +29,10 @@ public class NuReflectiveMethodExecutor extends ReflectiveMethodExecutor {

private boolean argumentConversionOccurred = false;

private final ConversionAndExtensionsAwareMethodInvoker methodInvoker;
private final ExtensionsAwareMethodInvoker methodInvoker;

public NuReflectiveMethodExecutor(ReflectiveMethodExecutor original,
ConversionAndExtensionsAwareMethodInvoker methodInvoker) {
ExtensionsAwareMethodInvoker methodInvoker) {
super(original.getMethod());
this.method = original.getMethod();
if (method.isVarArgs()) {
Expand Down Expand Up @@ -99,7 +98,7 @@ public TypedValue execute(EvaluationContext context, Object target, Object... ar
arguments = ReflectionHelper.setupArgumentsForVarargsInvocation(this.method.getParameterTypes(), arguments);
}
ReflectionUtils.makeAccessible(this.method);
//Nussknacker: we use custom method invoker which is aware of array conversion and extension methods
//Nussknacker: we use custom method invoker which is aware of extension methods
Object value = methodInvoker.invoke(this.method, target, arguments);
return new TypedValue(value, new TypeDescriptor(new MethodParameter(this.method, -1)).narrow(value));
}
Expand Down
Loading

0 comments on commit 8502620

Please sign in to comment.