From 510774d2b533789ae18dce3cbfd6de7f8f3855ba Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Mon, 28 Oct 2024 18:24:09 +0100 Subject: [PATCH 01/26] remove unnecessary nesting in DictApiEndopoints --- designer/client/src/http/HttpService.ts | 8 +------- .../nussknacker/ui/api/DictApiHttpService.scala | 2 +- .../ui/api/description/DictApiEndpoints.scala | 13 ++++++------- .../nussknacker/ui/api/DictApiHttpServiceSpec.scala | 6 ------ 4 files changed, 8 insertions(+), 21 deletions(-) diff --git a/designer/client/src/http/HttpService.ts b/designer/client/src/http/HttpService.ts index 977af76a486..1728d43dbc8 100644 --- a/designer/client/src/http/HttpService.ts +++ b/designer/client/src/http/HttpService.ts @@ -859,13 +859,7 @@ class HttpService { fetchAllProcessDefinitionDataDicts(processingType: ProcessingType, refClazzName: string, type = "TypedClass") { return api .post(`/processDefinitionData/${processingType}/dicts`, { - expectedType: { - value: { - type: type, - refClazzName, - params: [], - }, - }, + expectedType: { type: type, refClazzName, params: [] }, }) .catch((error) => Promise.reject( diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala index 456eebccc9f..3b89e39be58 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala @@ -56,7 +56,7 @@ class DictApiHttpService( case Some((_, dictionaries, classLoader)) => val decoder = new TypingResultDecoder(ClassUtils.forName(_, classLoader)).decodeTypingResults - decoder.decodeJson(dictListRequestDto.expectedType.value) match { + decoder.decodeJson(dictListRequestDto.expectedType) match { case Left(failure) => Future.successful(businessError(MalformedTypingResult(failure.getMessage()))) case Right(expectedType) => Future { diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/DictApiEndpoints.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/DictApiEndpoints.scala index cfeed8df730..1a6fd43ae98 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/DictApiEndpoints.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/description/DictApiEndpoints.scala @@ -16,12 +16,14 @@ import pl.touk.nussknacker.ui.api.description.DictApiEndpoints.DictError.{ import pl.touk.nussknacker.ui.api.description.DictApiEndpoints.Dtos._ import sttp.model.StatusCode.{BadRequest, NotFound, Ok} import sttp.tapir._ -import sttp.tapir.json.circe.jsonBody +import sttp.tapir.json.circe._ +import sttp.tapir.Schema import scala.language.implicitConversions class DictApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpointDefinitions { + lazy val dictionaryEntryQueryEndpoint: SecuredEndpoint[(String, String, String), DictError, List[DictEntry], Any] = baseNuApiEndpoint .summary("Get list of dictionary entries matching the label pattern") @@ -81,13 +83,11 @@ class DictApiEndpoints(auth: EndpointInput[AuthCredentials]) extends BaseEndpoin object DictApiEndpoints { - object Dtos { - @JsonCodec - case class TypingResultInJson(value: Json) + object Dtos { @JsonCodec - case class DictListRequestDto(expectedType: TypingResultInJson) + case class DictListRequestDto(expectedType: Json) @JsonCodec case class DictDto( @@ -95,9 +95,8 @@ object DictApiEndpoints { label: String // TODO: introduce separate labels for dictionaries, currently this is just equal to id ) - implicit lazy val typingResultInJsonSchema: Schema[TypingResultInJson] = TypingDtoSchemas.typingResult.as + implicit lazy val dictListRequestDtoSchema: Schema[DictListRequestDto] = Schema.derived[DictListRequestDto] implicit lazy val dictEntrySchema: Schema[DictEntry] = Schema.derived - implicit lazy val dictListRequestDtoSchema: Schema[DictListRequestDto] = Schema.derived implicit lazy val dictDtoSchema: Schema[DictDto] = Schema.derived } diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala index 457743945f4..7e8dde413bc 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala @@ -22,11 +22,9 @@ class DictApiHttpServiceSpec .basicAuthAllPermUser() .jsonBody("""{ | "expectedType" : { - | "value" : { | "type" : "TypedClass", | "refClazzName" : "java.lang.String", | "params" : [] - | } | } |}""".stripMargin) .post(s"$nuDesignerHttpAddress/api/processDefinitionData/${Streaming.stringify}/dicts") @@ -56,11 +54,9 @@ class DictApiHttpServiceSpec .basicAuthAllPermUser() .jsonBody("""{ | "expectedType" : { - | "value" : { | "type" : "TypedClass", | "refClazzName" : "java.lang.Long", | "params" : [] - | } | } |}""".stripMargin) .post(s"$nuDesignerHttpAddress/api/processDefinitionData/${Streaming.stringify}/dicts") @@ -95,11 +91,9 @@ class DictApiHttpServiceSpec .basicAuthAllPermUser() .jsonBody("""{ | "expectedType" : { - | "value" : { | "type" : "TypedClass", | "refClazzName" : "java.lang.Long", | "params" : [] - | } | } |}""".stripMargin) .post(s"$nuDesignerHttpAddress/api/processDefinitionData/thisProcessingTypeDoesNotExist/dicts") From e8dee9e751559acfa5a09c431ea752d9e34e3176 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Wed, 30 Oct 2024 16:17:53 +0100 Subject: [PATCH 02/26] Fixed improper subtyping in CanBeSubclassDeterminer --- .../api/typed/CanBeSubclassDeterminer.scala | 14 +++++- .../api/typed/TypeConversionHandler.scala | 28 +++++++---- .../ui/api/DictApiHttpServiceSpec.scala | 46 +++++++++++++++++++ 3 files changed, 77 insertions(+), 11 deletions(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala index 9545ab30dd6..4d447a75bce 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala @@ -4,6 +4,7 @@ import cats.data.Validated._ import cats.data.{NonEmptyList, ValidatedNel} import cats.implicits.{catsSyntaxValidatedId, _} import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers import pl.touk.nussknacker.engine.api.typed.typing._ /** @@ -207,8 +208,17 @@ trait CanBeSubclassDeterminer { } // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... - private def isAssignable(from: Class[_], to: Class[_]): ValidatedNel[String, Unit] = - condNel(ClassUtils.isAssignable(from, to, true), (), s"$to is not assignable from $from") + private def isAssignable(from: Class[_], to: Class[_]): ValidatedNel[String, Unit] = { + val isAssignable = (from, to) match { + case (f, t) if ClassUtils.isAssignable(f, t, true) => true + // Number double check by hand because lang3 can incorrectly throw false when dealing with java types + case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => + AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) + case _ => false + } + condNel(isAssignable, (), s"$to is not assignable from $from") + } + } object CanBeSubclassDeterminer extends CanBeSubclassDeterminer diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index cc91f255239..9b9aa162b68 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -20,8 +20,8 @@ object TypeConversionHandler { /** * java.math.BigDecimal is quite often returned as a wrapper for all kind of numbers (floating and without floating point). - * Given to this we cannot to be sure if conversion is safe or not based on type (without scale knowledge). - * So we have two options: enforce user to convert to some type without floating point (e.g. BigInteger) or be loose in this point. + * Given to this we cannot be sure if conversion is safe or not based on type (without scale knowledge). + * So we have two options: force user to convert to some type without floating point (e.g. BigInteger) or be loose in this point. * Be default we will be loose. */ // TODO: Add feature flag: strictBigDecimalChecking (default false?) @@ -74,14 +74,24 @@ object TypeConversionHandler { val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) // We can't check precision here so we need to be loose here // TODO: Add feature flag: strictNumberPrecisionChecking (default false?) - if (NumberTypesPromotionStrategy - .isFloatingNumber(boxedSuperclassCandidate) || boxedSuperclassCandidate == classOf[java.math.BigDecimal]) { - ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) - } else if (NumberTypesPromotionStrategy.isDecimalNumber(boxedSuperclassCandidate)) { - ConversionFromClassesForDecimals.exists(ClassUtils.isAssignable(boxedGivenClass, _, true)) - } else { - false + + def isFloating(candidate: Class[_]): Boolean = { + NumberTypesPromotionStrategy.isFloatingNumber(candidate) || candidate == classOf[java.math.BigDecimal] + } + def isDecimalNumber(candidate: Class[_]): Boolean = { + NumberTypesPromotionStrategy.isDecimalNumber(candidate) + } + + boxedSuperclassCandidate match { + case candidate if isFloating(candidate) => + ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) + + case candidate if isDecimalNumber(candidate) => + ConversionFromClassesForDecimals.filter(ClassUtils.isAssignable(boxedGivenClass, _, true)).contains(candidate) + + case _ => false } + } private def handleStringToValueClassConversions( diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala index 7e8dde413bc..3decd5d7d82 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala @@ -16,6 +16,28 @@ class DictApiHttpServiceSpec with RestAssuredVerboseLoggingIfValidationFails { "The endpoint for listing available dictionaries of expected type should" - { + + "return proper empty list for expected type Integer - check subclassing" in { + given() + .when() + .basicAuthAllPermUser() + .jsonBody("""{ + | "expectedType" : { + | "type" : "TypedClass", + | "refClazzName" : "java.lang.Integer", + | "params":[] + | } + |}""".stripMargin) + .post(s"$nuDesignerHttpAddress/api/processDefinitionData/${Streaming.stringify}/dicts") + .Then() + .statusCode(200) + .equalsJsonBody( + s"""[ + |]""".stripMargin + ) + + } + "return proper list for expected type String" in { given() .when() @@ -72,6 +94,30 @@ class DictApiHttpServiceSpec ) } + "return proper list for expected type BigDecimal" in { + given() + .when() + .basicAuthAllPermUser() + .jsonBody("""{ + | "expectedType" : { + | "type" : "TypedClass", + | "refClazzName" : "java.math.BigInteger", + | "params" : [] + | } + |}""".stripMargin) + .post(s"$nuDesignerHttpAddress/api/processDefinitionData/${Streaming.stringify}/dicts") + .Then() + .statusCode(200) + .equalsJsonBody( + s"""[ + | { + | "id" : "long_dict", + | "label" : "long_dict" + | } + |]""".stripMargin + ) + } + "fail for bad request" in { given() .when() From 1f867b10e0ec279e25659ab2d0a3cbcc58b3bd74 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Thu, 31 Oct 2024 14:50:39 +0100 Subject: [PATCH 03/26] regenerated openapi --- docs-internal/api/nu-designer-openapi.yaml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/docs-internal/api/nu-designer-openapi.yaml b/docs-internal/api/nu-designer-openapi.yaml index 215692d8583..b3c4e8221f6 100644 --- a/docs-internal/api/nu-designer-openapi.yaml +++ b/docs-internal/api/nu-designer-openapi.yaml @@ -5080,16 +5080,7 @@ components: required: - expectedType properties: - expectedType: - oneOf: - - $ref: '#/components/schemas/TypedClass' - - $ref: '#/components/schemas/TypedDict' - - $ref: '#/components/schemas/TypedNull' - - $ref: '#/components/schemas/TypedObjectTypingResult' - - $ref: '#/components/schemas/TypedObjectWithValue' - - $ref: '#/components/schemas/TypedTaggedValue' - - $ref: '#/components/schemas/TypedUnion' - - $ref: '#/components/schemas/Unknown' + expectedType: {} DictParameterEditor: title: DictParameterEditor type: object From 8216a17d6275475e054be37ed7e8c4983233b4ae Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Thu, 31 Oct 2024 16:07:51 +0100 Subject: [PATCH 04/26] Changed type in UIProcess --- .../touk/nussknacker/ui/validation/UIProcessValidatorSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/validation/UIProcessValidatorSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/validation/UIProcessValidatorSpec.scala index 396f81c5fe6..3775754badd 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/validation/UIProcessValidatorSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/validation/UIProcessValidatorSpec.scala @@ -1950,7 +1950,7 @@ class UIProcessValidatorSpec extends AnyFunSuite with Matchers with TableDrivenP ) val fragmentDefinition: CanonicalProcess = - createFragmentDefinition(fragmentId, List(FragmentParameter(ParameterName("P1"), FragmentClazzRef[Short]))) + createFragmentDefinition(fragmentId, List(FragmentParameter(ParameterName("P1"), FragmentClazzRef[Integer]))) val processWithFragment = createScenarioGraphWithFragmentParams(fragmentId, List(NodeParameter(ParameterName("P1"), "123".spel))) From 139dabc696c8e242db4681cdfeb7556b756d6983 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Thu, 31 Oct 2024 16:08:20 +0100 Subject: [PATCH 05/26] extract isAssignable --- .../api/typed/CanBeSubclassDeterminer.scala | 18 ++++++++++++------ .../api/typed/TypeConversionHandler.scala | 3 ++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala index 4d447a75bce..4eedb8b7e4d 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala @@ -4,6 +4,7 @@ import cats.data.Validated._ import cats.data.{NonEmptyList, ValidatedNel} import cats.implicits.{catsSyntaxValidatedId, _} import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.CanBeSubclassDeterminer.isAssignable import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers import pl.touk.nussknacker.engine.api.typed.typing._ @@ -136,7 +137,11 @@ trait CanBeSubclassDeterminer { (), f"${givenClass.display} and ${superclassCandidate.display} are not the same" ) orElse - isAssignable(givenClass.klass, superclassCandidate.klass) + condNel( + isAssignable(givenClass.klass, superclassCandidate.klass), + (), + s"${givenClass.klass} is not assignable from ${superclassCandidate.klass}" + ) val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, superclassCandidate)) canBeSubclass orElse canBeConvertedTo(givenType, superclassCandidate) @@ -207,18 +212,19 @@ trait CanBeSubclassDeterminer { condNel(TypeConversionHandler.canBeConvertedTo(givenType, superclassCandidate), (), errMsgPrefix) } +} + +object CanBeSubclassDeterminer extends CanBeSubclassDeterminer { + // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... - private def isAssignable(from: Class[_], to: Class[_]): ValidatedNel[String, Unit] = { - val isAssignable = (from, to) match { + def isAssignable(from: Class[_], to: Class[_]): Boolean = { + (from, to) match { case (f, t) if ClassUtils.isAssignable(f, t, true) => true // Number double check by hand because lang3 can incorrectly throw false when dealing with java types case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) case _ => false } - condNel(isAssignable, (), s"$to is not assignable from $from") } } - -object CanBeSubclassDeterminer extends CanBeSubclassDeterminer diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index 9b9aa162b68..89942a5942d 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -87,7 +87,8 @@ object TypeConversionHandler { ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) case candidate if isDecimalNumber(candidate) => - ConversionFromClassesForDecimals.filter(ClassUtils.isAssignable(boxedGivenClass, _, true)).contains(candidate) + import CanBeSubclassDeterminer.isAssignable + isAssignable(boxedGivenClass, candidate) case _ => false } From 599124a549b3cd7bc93a09881e0a4a51f99717c4 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Wed, 6 Nov 2024 11:30:37 +0100 Subject: [PATCH 06/26] Added CanBeSubclassDeterminerSpec --- .../api/typed/CanBeSubclassDeterminer.scala | 3 +- .../typed/CanBeSubclassDeterminerSpec.scala | 33 +++++++++++++++++++ .../engine/api/typed/TypingResultSpec.scala | 2 +- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala index 4eedb8b7e4d..efed705fe3f 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala @@ -4,7 +4,6 @@ import cats.data.Validated._ import cats.data.{NonEmptyList, ValidatedNel} import cats.implicits.{catsSyntaxValidatedId, _} import org.apache.commons.lang3.ClassUtils -import pl.touk.nussknacker.engine.api.typed.CanBeSubclassDeterminer.isAssignable import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers import pl.touk.nussknacker.engine.api.typed.typing._ @@ -138,7 +137,7 @@ trait CanBeSubclassDeterminer { f"${givenClass.display} and ${superclassCandidate.display} are not the same" ) orElse condNel( - isAssignable(givenClass.klass, superclassCandidate.klass), + CanBeSubclassDeterminer.isAssignable(givenClass.klass, superclassCandidate.klass), (), s"${givenClass.klass} is not assignable from ${superclassCandidate.klass}" ) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala new file mode 100644 index 00000000000..b75b8ad2ea1 --- /dev/null +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala @@ -0,0 +1,33 @@ +package pl.touk.nussknacker.engine.api.typed + +import org.apache.commons.lang3.ClassUtils +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class CanBeSubclassDeterminerSpec extends AnyFunSuite with Matchers { + + test("Should validate assignability for decimal types") { + CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false + CanBeSubclassDeterminer.isAssignable(classOf[Number], classOf[Int]) shouldBe false + CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Short]) shouldBe false + + CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Long]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Number]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[Short], classOf[Int]) shouldBe true + } + + test("Should validate assignability for numerical types") { + CanBeSubclassDeterminer.isAssignable(classOf[Long], classOf[Double]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[Float], classOf[Double]) shouldBe true + + CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Float]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[Long], classOf[Double]) shouldBe true + } + + // to check if autoboxing lang3 is failing - we can remove our fallback if the lib works properly + test("Should check if lang3 fails for certain isAssignable cases") { + ClassUtils.isAssignable(classOf[Integer], classOf[Long], true) shouldBe true + ClassUtils.isAssignable(classOf[Short], classOf[Integer], true) shouldBe true + } + +} diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala index ec38ad9cf2a..cc07edee4eb 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala @@ -137,7 +137,7 @@ class TypingResultSpec test("determine if numbers can be converted") { Typed[Int].canBeSubclassOf(Typed[Long]) shouldBe true - Typed[Long].canBeSubclassOf(Typed[Int]) shouldBe true + Typed[Long].canBeSubclassOf(Typed[Int]) shouldBe false Typed[Long].canBeSubclassOf(Typed[Double]) shouldBe true Typed[Double].canBeSubclassOf(Typed[Long]) shouldBe false Typed[java.math.BigDecimal].canBeSubclassOf(Typed[Long]) shouldBe true From 5230e0ebd32d8c73d0cef77b044d1419669f770a Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Wed, 6 Nov 2024 11:32:45 +0100 Subject: [PATCH 07/26] comment fixup --- .../engine/api/typed/CanBeSubclassDeterminerSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala index b75b8ad2ea1..d45ce98fc9b 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala @@ -24,7 +24,7 @@ class CanBeSubclassDeterminerSpec extends AnyFunSuite with Matchers { CanBeSubclassDeterminer.isAssignable(classOf[Long], classOf[Double]) shouldBe true } - // to check if autoboxing lang3 is failing - we can remove our fallback if the lib works properly + // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly test("Should check if lang3 fails for certain isAssignable cases") { ClassUtils.isAssignable(classOf[Integer], classOf[Long], true) shouldBe true ClassUtils.isAssignable(classOf[Short], classOf[Integer], true) shouldBe true From ef04b92c11cc25c519639e489354692fac0d6ad7 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Wed, 6 Nov 2024 11:36:34 +0100 Subject: [PATCH 08/26] Fix decimal typing in SubclassDeterminerSpec --- .../engine/api/typed/CanBeSubclassDeterminerSpec.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala index d45ce98fc9b..d3ece8ee5a6 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala @@ -8,12 +8,12 @@ class CanBeSubclassDeterminerSpec extends AnyFunSuite with Matchers { test("Should validate assignability for decimal types") { CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false - CanBeSubclassDeterminer.isAssignable(classOf[Number], classOf[Int]) shouldBe false - CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Short]) shouldBe false + CanBeSubclassDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false + CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false - CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Long]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Number]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[Short], classOf[Int]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true } test("Should validate assignability for numerical types") { From e477ef803cb6afd4abd1f2e894b6cd90cbdc0edc Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Wed, 6 Nov 2024 11:38:28 +0100 Subject: [PATCH 09/26] Fix numerical typing in SubclassDeterminerSpec --- .../engine/api/typed/CanBeSubclassDeterminerSpec.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala index d3ece8ee5a6..91edd085195 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala @@ -17,11 +17,11 @@ class CanBeSubclassDeterminerSpec extends AnyFunSuite with Matchers { } test("Should validate assignability for numerical types") { - CanBeSubclassDeterminer.isAssignable(classOf[Long], classOf[Double]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[Float], classOf[Double]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[Int], classOf[Float]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[Long], classOf[Double]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true + CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true } // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly From 224408ac5edee171951176862283402b12b4aba3 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Wed, 6 Nov 2024 20:51:34 +0100 Subject: [PATCH 10/26] CanBeSubclassDeterminerSpec fixes --- .../api/typed/CanBeSubclassDeterminerSpec.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala index 91edd085195..259087731f1 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala @@ -26,8 +26,16 @@ class CanBeSubclassDeterminerSpec extends AnyFunSuite with Matchers { // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly test("Should check if lang3 fails for certain isAssignable cases") { - ClassUtils.isAssignable(classOf[Integer], classOf[Long], true) shouldBe true - ClassUtils.isAssignable(classOf[Short], classOf[Integer], true) shouldBe true + ClassUtils.isAssignable( + classOf[Integer], + classOf[java.lang.Long], + true + ) shouldBe false // should be true in reality, but currently the lib is bugged + ClassUtils.isAssignable( + classOf[java.lang.Short], + classOf[Integer], + true + ) shouldBe false // should be true in reality, but currently the lib is bugged } } From d4f85e8eb7b4473ac436ec4dec1a6a828e827f3a Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Thu, 7 Nov 2024 20:57:27 +0100 Subject: [PATCH 11/26] Extracted ConversionDeterminer and SubclassDeterminer --- .../api/typed/ConversionDeterminer.scala | 102 ++++++++++++++ ...ala => ImplicitConversionDeterminer.scala} | 113 +++------------ .../engine/api/typed/NumberTypeUtils.scala | 4 +- .../engine/api/typed/SubclassDeterminer.scala | 22 +++ .../api/typed/TypeConversionHandler.scala | 2 +- .../nussknacker/engine/api/typed/typing.scala | 18 ++- .../typed/CanBeSubclassDeterminerSpec.scala | 20 +-- .../api/typed/TypedFromInstanceTest.scala | 10 +- .../typed/TypingResultErrorMessagesSpec.scala | 18 +-- .../engine/api/typed/TypingResultSpec.scala | 132 +++++++++--------- .../ui/api/DictApiHttpService.scala | 4 +- .../ui/process/test/ScenarioTestService.scala | 4 +- .../aggregate/AggregatesSpec.scala | 4 +- .../transformer/aggregate/aggregates.scala | 10 +- .../util/transformer/ForEachTransformer.scala | 4 +- .../table/sink/TableTypeOutputValidator.scala | 2 +- .../utils/ToTableTypeSchemaBasedEncoder.scala | 10 +- .../global/DocumentationFunctions.scala | 4 +- .../sample/global/ExampleFunctions.scala | 2 +- .../lite/components/ForEachTransformer.scala | 4 +- .../FragmentParameterValidator.scala | 2 +- .../clazz/ClassDefinitionExtractor.scala | 4 +- .../definition/clazz/MethodDefinition.scala | 10 +- .../clazz/MethodTypeInfoSubclassChecker.scala | 6 +- .../DictKeyWithLabelExpressionParser.scala | 6 +- .../engine/spel/SpelExpressionSuggester.scala | 4 +- .../touk/nussknacker/engine/spel/Typer.scala | 36 ++--- .../spel/typer/MethodReferenceTyper.scala | 2 +- .../engine/util/functions/collection.scala | 4 +- .../engine/util/functions/numeric.scala | 2 +- .../encode/JsonSchemaOutputValidator.scala | 2 +- .../encode/AvroSchemaOutputValidator.scala | 8 +- 32 files changed, 322 insertions(+), 253 deletions(-) create mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala rename components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/{CanBeSubclassDeterminer.scala => ImplicitConversionDeterminer.scala} (55%) create mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala new file mode 100644 index 00000000000..5617a300d34 --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala @@ -0,0 +1,102 @@ +package pl.touk.nussknacker.engine.api.typed + +import cats.data.Validated.condNel +import cats.data.{NonEmptyList, Validated, ValidatedNel} +import cats.implicits.catsSyntaxValidatedId +import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers +import pl.touk.nussknacker.engine.api.typed.typing.{ + SingleTypingResult, + TypedClass, + TypedNull, + TypedObjectWithValue, + TypedUnion, + TypingResult, + Unknown +} + +trait ConversionDeterminer { + + def singleCanBeConvertedTo( + result: typing.SingleTypingResult, + result1: typing.SingleTypingResult + ): ValidatedNel[String, Unit] + + /** + * This method checks if `givenType` can by subclass of `superclassCandidate` + * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` + */ + def canBeConvertedTo(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { + (givenType, superclassCandidate) match { + case (_, Unknown) => ().validNel + case (Unknown, _) => ().validNel + case (TypedNull, other) => canNullBeConvertedTo(other) + case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel + case (given: SingleTypingResult, superclass: TypedUnion) => + canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) + case (given: TypedUnion, superclass: SingleTypingResult) => + canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) + case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeConvertedTo(given, superclass) + case (given: TypedUnion, superclass: TypedUnion) => + canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) + } + } + + def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { + // TODO: Null should not be subclass of typed map that has all values assigned. + case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel + case _ => ().validNel + } + + def canBeConvertedTo( + givenTypes: NonEmptyList[SingleTypingResult], + superclassCandidates: NonEmptyList[SingleTypingResult] + ): ValidatedNel[String, Unit] = { + // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against + // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. + // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here + // "double exists" looks like a good tradeoff + condNel( + givenTypes.exists(given => superclassCandidates.exists(singleCanBeConvertedTo(given, _).isValid)), + (), + s"""None of the following types: + |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} + |can be a subclass of any of: + |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin + ) + } + + // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) + def canBeConvertedTo( + givenType: SingleTypingResult, + superclassCandidate: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = s"${givenType.runtimeObjType.display} cannot be converted to ${superclassCandidate.display}" + condNel(TypeConversionHandler.canBeConvertedTo(givenType, superclassCandidate), (), errMsgPrefix) + } + + def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { + condNel( + givenClass == givenSuperclass, + (), + f"${givenClass.display} and ${givenSuperclass.display} are not the same" + ) orElse + condNel( + isAssignable(givenClass.klass, givenSuperclass.klass), + (), + s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" + ) + } + + // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... + def isAssignable(from: Class[_], to: Class[_]): Boolean = { + (from, to) match { + case (f, t) if ClassUtils.isAssignable(f, t, true) => true + // Number double check by hand because lang3 can incorrectly throw false when dealing with java types + case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => + AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) + case _ => false + } + } + +} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala similarity index 55% rename from components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala rename to components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala index efed705fe3f..530aa163b19 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala @@ -1,10 +1,8 @@ package pl.touk.nussknacker.engine.api.typed import cats.data.Validated._ -import cats.data.{NonEmptyList, ValidatedNel} +import cats.data.ValidatedNel import cats.implicits.{catsSyntaxValidatedId, _} -import org.apache.commons.lang3.ClassUtils -import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers import pl.touk.nussknacker.engine.api.typed.typing._ /** @@ -15,42 +13,17 @@ import pl.touk.nussknacker.engine.api.typed.typing._ * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". * WARNING: Evaluation of SpEL expressions fit into this spirit, for other language evaluation engines you need to provide such a compatibility. */ -trait CanBeSubclassDeterminer { +object ImplicitConversionDeterminer extends ConversionDeterminer { private val javaMapClass = classOf[java.util.Map[_, _]] private val javaListClass = classOf[java.util.List[_]] private val arrayOfAnyRefClass = classOf[Array[AnyRef]] - /** - * This method checks if `givenType` can by subclass of `superclassCandidate` - * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` - */ - def canBeSubclassOf(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - (givenType, superclassCandidate) match { - case (_, Unknown) => ().validNel - case (Unknown, _) => ().validNel - case (TypedNull, other) => canNullBeSubclassOf(other) - case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel - case (given: SingleTypingResult, superclass: TypedUnion) => - canBeSubclassOf(NonEmptyList.one(given), superclass.possibleTypes) - case (given: TypedUnion, superclass: SingleTypingResult) => - canBeSubclassOf(given.possibleTypes, NonEmptyList.one(superclass)) - case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeSubclassOf(given, superclass) - case (given: TypedUnion, superclass: TypedUnion) => canBeSubclassOf(given.possibleTypes, superclass.possibleTypes) - } - } - - private def canNullBeSubclassOf(result: TypingResult): ValidatedNel[String, Unit] = result match { - // TODO: Null should not be subclass of typed map that has all values assigned. - case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel - case _ => ().validNel - } - - protected def singleCanBeSubclassOf( + protected def singleCanBeConvertedTo( givenType: SingleTypingResult, superclassCandidate: SingleTypingResult ): ValidatedNel[String, Unit] = { - val objTypeRestriction = classCanBeSubclassOf(givenType, superclassCandidate.runtimeObjType) + val objTypeRestriction = classCanBeConvertedTo(givenType, superclassCandidate) val typedObjectRestrictions = (_: Unit) => superclassCandidate match { case superclass: TypedObjectTypingResult => @@ -66,7 +39,7 @@ trait CanBeSubclassDeterminer { s"Field '$name' is lacking".invalidNel case Some(givenFieldType) => condNel( - canBeSubclassOf(givenFieldType, typ).isValid, + canBeConvertedTo(givenFieldType, typ).isValid, (), s"Field '$name' is of the wrong type. Expected: ${givenFieldType.display}, actual: ${typ.display}" ) @@ -124,33 +97,23 @@ trait CanBeSubclassDeterminer { (typedObjectRestrictions combine dictRestriction combine taggedValueRestriction combine dataValueRestriction) } - protected def classCanBeSubclassOf( + private def classCanBeConvertedTo( givenType: SingleTypingResult, - superclassCandidate: TypedClass + superclassCandidate: SingleTypingResult ): ValidatedNel[String, Unit] = { - val givenClass = givenType.runtimeObjType + val givenClass = givenType.runtimeObjType + val givenSuperclass = superclassCandidate.runtimeObjType - val equalClassesOrCanAssign = - condNel( - givenClass == superclassCandidate, - (), - f"${givenClass.display} and ${superclassCandidate.display} are not the same" - ) orElse - condNel( - CanBeSubclassDeterminer.isAssignable(givenClass.klass, superclassCandidate.klass), - (), - s"${givenClass.klass} is not assignable from ${superclassCandidate.klass}" - ) - - val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, superclassCandidate)) + val equalClassesOrCanAssign = isStrictSubclass(givenClass, givenSuperclass) + val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, givenSuperclass)) canBeSubclass orElse canBeConvertedTo(givenType, superclassCandidate) } private def typeParametersMatches(givenClass: TypedClass, superclassCandidate: TypedClass) = { def canBeSubOrSuperclass(givenClassParam: TypingResult, superclassParam: TypingResult) = condNel( - canBeSubclassOf(givenClassParam, superclassParam).isValid || - canBeSubclassOf(superclassParam, givenClassParam).isValid, + canBeConvertedTo(givenClassParam, superclassParam).isValid || + canBeConvertedTo(superclassParam, givenClassParam).isValid, (), f"None of ${givenClassParam.display} and ${superclassParam.display} is a subclass of another" ) @@ -159,18 +122,18 @@ trait CanBeSubclassDeterminer { case (TypedClass(_, givenElementParam :: Nil), TypedClass(superclass, superclassParam :: Nil)) // Array are invariant but we have built-in conversion between array types - this check should be moved outside this class when we move away canBeConvertedTo as well if javaListClass.isAssignableFrom(superclass) || arrayOfAnyRefClass.isAssignableFrom(superclass) => - canBeSubclassOf(givenElementParam, superclassParam) + canBeConvertedTo(givenElementParam, superclassParam) case ( TypedClass(_, givenKeyParam :: givenValueParam :: Nil), TypedClass(superclass, superclassKeyParam :: superclassValueParam :: Nil) ) if javaMapClass.isAssignableFrom(superclass) => // Map's key generic param is invariant. We can't just check givenKeyParam == superclassKeyParam because of Unknown type which is a kind of wildcard condNel( - canBeSubclassOf(givenKeyParam, superclassKeyParam).isValid && - canBeSubclassOf(superclassKeyParam, givenKeyParam).isValid, + canBeConvertedTo(givenKeyParam, superclassKeyParam).isValid && + canBeConvertedTo(superclassKeyParam, givenKeyParam).isValid, (), s"Key types of Maps ${givenKeyParam.display} and ${superclassKeyParam.display} are not equals" - ) andThen (_ => canBeSubclassOf(givenValueParam, superclassValueParam)) + ) andThen (_ => canBeConvertedTo(givenValueParam, superclassValueParam)) case _ => // for unknown types we are lax - the generic type may be co- contra- or in-variant - and we don't want to // return validation errors in this case. It's better to accept to much than too little @@ -184,46 +147,4 @@ trait CanBeSubclassDeterminer { } } - private def canBeSubclassOf( - givenTypes: NonEmptyList[SingleTypingResult], - superclassCandidates: NonEmptyList[SingleTypingResult] - ): ValidatedNel[String, Unit] = { - // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against - // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. - // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here - // "double exists" looks like a good tradeoff - condNel( - givenTypes.exists(given => superclassCandidates.exists(singleCanBeSubclassOf(given, _).isValid)), - (), - s"""None of the following types: - |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} - |can be a subclass of any of: - |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin - ) - } - - // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) - private def canBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: TypedClass - ): ValidatedNel[String, Unit] = { - val errMsgPrefix = s"${givenType.runtimeObjType.display} cannot be converted to ${superclassCandidate.display}" - condNel(TypeConversionHandler.canBeConvertedTo(givenType, superclassCandidate), (), errMsgPrefix) - } - -} - -object CanBeSubclassDeterminer extends CanBeSubclassDeterminer { - - // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... - def isAssignable(from: Class[_], to: Class[_]): Boolean = { - (from, to) match { - case (f, t) if ClassUtils.isAssignable(f, t, true) => true - // Number double check by hand because lang3 can incorrectly throw false when dealing with java types - case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => - AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) - case _ => false - } - } - } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala index fa37bc54a40..a9cb2846918 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala @@ -15,9 +15,9 @@ object NumberTypeUtils { else if (typ == Typed[java.lang.Double]) java.lang.Double.valueOf(0) else if (typ == Typed[java.math.BigDecimal]) java.math.BigDecimal.ZERO // in case of some unions - else if (typ.canBeSubclassOf(Typed[java.lang.Integer])) java.lang.Integer.valueOf(0) + else if (typ.canBeImplicitlyConvertedTo(Typed[java.lang.Integer])) java.lang.Integer.valueOf(0) // double is quite safe - it can be converted to any Number - else if (typ.canBeSubclassOf(Typed[Number])) java.lang.Double.valueOf(0) + else if (typ.canBeImplicitlyConvertedTo(Typed[Number])) java.lang.Double.valueOf(0) else throw new IllegalArgumentException(s"Not expected type: ${typ.display}, should be Number") } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala new file mode 100644 index 00000000000..28de6dd53c2 --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala @@ -0,0 +1,22 @@ +package pl.touk.nussknacker.engine.api.typed + +import cats.data.ValidatedNel +import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypingResult} + +object SubclassDeterminer extends ConversionDeterminer { + + def singleCanBeConvertedTo( + givenType: SingleTypingResult, + superclassCandidate: SingleTypingResult + ): ValidatedNel[String, Unit] = { + val givenClass = givenType.runtimeObjType + val givenSuperclas = superclassCandidate.runtimeObjType + + isStrictSubclass(givenClass, givenSuperclas) + } + + def canBeStrictSubclassOf(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { + this.canBeConvertedTo(givenType, superclassCandidate) + } + +} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index 89942a5942d..f94a190890e 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -87,7 +87,7 @@ object TypeConversionHandler { ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) case candidate if isDecimalNumber(candidate) => - import CanBeSubclassDeterminer.isAssignable + import ImplicitConversionDeterminer.isAssignable isAssignable(boxedGivenClass, candidate) case _ => false diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala index ca5cbde9597..c2be6cdcf72 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala @@ -27,15 +27,11 @@ object typing { // TODO: Rename to Typed, maybe NuType? sealed trait TypingResult { - // TODO: We should split this method into two or three methods: - // - Simple, strictly checking subclassing similar to isAssignable, where we don't do heuristics like - // Any can be subclass of Int, or for Union of Int and String can be subclass of Int - // - The one with heuristics considering limitations of our tool like poor support for generics, lack - // of casting allowing things described above - // - The one that allow things above + SPeL conversions like any Number to any Number conversion, - // String to LocalDate etc. This one should be accessible only for context where SPeL is used - final def canBeSubclassOf(typingResult: TypingResult): Boolean = - CanBeSubclassDeterminer.canBeSubclassOf(this, typingResult).isValid + final def canBeImplicitlyConvertedTo(typingResult: TypingResult): Boolean = + ImplicitConversionDeterminer.canBeConvertedTo(this, typingResult).isValid + + def canBeStrictSubclassOf(typingResult: TypingResult): Boolean = + SubclassDeterminer.canBeStrictSubclassOf(this, typingResult).isValid def valueOpt: Option[Any] @@ -466,7 +462,9 @@ object typing { case class CastTypedValue[T: TypeTag]() { def unapply(typingResult: TypingResult): Option[TypingResultTypedValue[T]] = { - Option(typingResult).filter(_.canBeSubclassOf(Typed.fromDetailedType[T])).map(new TypingResultTypedValue(_)) + Option(typingResult) + .filter(_.canBeImplicitlyConvertedTo(Typed.fromDetailedType[T])) + .map(new TypingResultTypedValue(_)) } } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala index 259087731f1..8b51536aac2 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala @@ -7,21 +7,21 @@ import org.scalatest.matchers.should.Matchers class CanBeSubclassDeterminerSpec extends AnyFunSuite with Matchers { test("Should validate assignability for decimal types") { - CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false - CanBeSubclassDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false - CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false + ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false + ImplicitConversionDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false + ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false - CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true + ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true + ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true + ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true } test("Should validate assignability for numerical types") { - CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true + ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true + ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true - CanBeSubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true + ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true + ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true } // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala index 6e8e4ce6708..f3e50d76e35 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala @@ -60,20 +60,20 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w } test("should type empty list") { - Typed.fromInstance(Nil).canBeSubclassOf(Typed(classOf[List[_]])) shouldBe true - Typed.fromInstance(Nil.asJava).canBeSubclassOf(Typed(classOf[java.util.List[_]])) shouldBe true + Typed.fromInstance(Nil).canBeImplicitlyConvertedTo(Typed(classOf[List[_]])) shouldBe true + Typed.fromInstance(Nil.asJava).canBeImplicitlyConvertedTo(Typed(classOf[java.util.List[_]])) shouldBe true } test("should type lists and return union of types coming from all elements") { def checkTypingResult(obj: Any, klass: Class[_], paramTypingResult: TypingResult): Unit = { val typingResult = Typed.fromInstance(obj) - typingResult.canBeSubclassOf(Typed(klass)) shouldBe true + typingResult.canBeImplicitlyConvertedTo(Typed(klass)) shouldBe true typingResult.withoutValue .asInstanceOf[TypedClass] .params .loneElement - .canBeSubclassOf(paramTypingResult) shouldBe true + .canBeImplicitlyConvertedTo(paramTypingResult) shouldBe true } def checkNotASubclassOfOtherParamTypingResult(obj: Any, otherParamTypingResult: TypingResult): Unit = { @@ -82,7 +82,7 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w .asInstanceOf[TypedClass] .params .loneElement - .canBeSubclassOf(otherParamTypingResult) shouldBe false + .canBeImplicitlyConvertedTo(otherParamTypingResult) shouldBe false } val listOfSimpleObjects = List[Any](1.1, 2) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala index 5483df1a9a8..e55fda73496 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala @@ -13,11 +13,11 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio private def list(arg: TypingResult) = Typed.genericTypeClass[java.util.List[_]](List(arg)) - import CanBeSubclassDeterminer.canBeSubclassOf + import ImplicitConversionDeterminer.canBeConvertedTo test("determine if can be subclass for typed object") { - canBeSubclassOf( + canBeConvertedTo( typeMap( "field1" -> Typed[String], "field2" -> Typed[Int], @@ -38,7 +38,7 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio ) .invalid - canBeSubclassOf( + canBeConvertedTo( typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))), typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe NonEmptyList @@ -49,30 +49,30 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio } test("determine if can be subclass for class") { - canBeSubclassOf(Typed.fromDetailedType[Set[BigDecimal]], Typed.fromDetailedType[Set[String]]) shouldBe + canBeConvertedTo(Typed.fromDetailedType[Set[BigDecimal]], Typed.fromDetailedType[Set[String]]) shouldBe "Set[BigDecimal] cannot be converted to Set[String]".invalidNel } test("determine if can be subclass for tagged value") { - canBeSubclassOf( + canBeConvertedTo( Typed.tagged(Typed.typedClass[String], "tag1"), Typed.tagged(Typed.typedClass[String], "tag2") ) shouldBe "Tagged values have unequal tags: tag1 and tag2".invalidNel - canBeSubclassOf(Typed.typedClass[String], Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe + canBeConvertedTo(Typed.typedClass[String], Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe "The type is not a tagged value".invalidNel } test("determine if can be subclass for object with value") { - canBeSubclassOf(Typed.fromInstance(2), Typed.fromInstance(3)) shouldBe + canBeConvertedTo(Typed.fromInstance(2), Typed.fromInstance(3)) shouldBe "Types with value have different values: 2 and 3".invalidNel } test("determine if can be subclass for null") { - canBeSubclassOf(Typed[String], TypedNull) shouldBe + canBeConvertedTo(Typed[String], TypedNull) shouldBe "No type can be subclass of Null".invalidNel - canBeSubclassOf(TypedNull, Typed.fromInstance(1)) shouldBe + canBeConvertedTo(TypedNull, Typed.fromInstance(1)) shouldBe "Null cannot be subclass of type with value".invalidNel } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala index cc07edee4eb..9ab5919efad 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala @@ -32,33 +32,33 @@ class TypingResultSpec test("determine if can be subclass for typed object") { - typeMap("field1" -> Typed[String], "field2" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[String], "field2" -> Typed[Int]).canBeImplicitlyConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe true - typeMap("field1" -> Typed[String]).canBeSubclassOf( + typeMap("field1" -> Typed[String]).canBeImplicitlyConvertedTo( typeMap("field1" -> Typed[String], "field2" -> Typed[Int]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[Int]).canBeImplicitlyConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[Int]).canBeImplicitlyConvertedTo( typeMap("field1" -> Typed[Number]) ) shouldBe true - typeMap("field1" -> list(typeMap("field2" -> Typed[String], "field3" -> Typed[Int]))).canBeSubclassOf( + typeMap("field1" -> list(typeMap("field2" -> Typed[String], "field3" -> Typed[Int]))).canBeImplicitlyConvertedTo( typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe true - typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))).canBeSubclassOf( + typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))).canBeImplicitlyConvertedTo( typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe false - typeMap("field1" -> Typed[String]).canBeSubclassOf(Typed[java.util.Map[_, _]]) shouldBe true + typeMap("field1" -> Typed[String]).canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) shouldBe true - Typed[java.util.Map[_, _]].canBeSubclassOf(typeMap("field1" -> Typed[String])) shouldBe false + Typed[java.util.Map[_, _]].canBeImplicitlyConvertedTo(typeMap("field1" -> Typed[String])) shouldBe false } test("extract Unknown value type when no super matching supertype found among all fields of Record") { @@ -76,72 +76,78 @@ class TypingResultSpec } test("determine if can be subclass for typed unions") { - Typed(Typed[String], Typed[Int]).canBeSubclassOf(Typed[Int]) shouldBe true - Typed[Int].canBeSubclassOf(Typed(Typed[String], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeImplicitlyConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true - Typed(Typed[String], Typed[Int]).canBeSubclassOf(Typed(Typed[Long], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeImplicitlyConvertedTo(Typed(Typed[Long], Typed[Int])) shouldBe true } test("determine if can be subclass for unknown") { - Unknown.canBeSubclassOf(Typed[Int]) shouldBe true - Typed[Int].canBeSubclassOf(Unknown) shouldBe true + Unknown.canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeImplicitlyConvertedTo(Unknown) shouldBe true - Unknown.canBeSubclassOf(Typed(Typed[String], Typed[Int])) shouldBe true - Typed(Typed[String], Typed[Int]).canBeSubclassOf(Unknown) shouldBe true + Unknown.canBeImplicitlyConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeImplicitlyConvertedTo(Unknown) shouldBe true - Unknown.canBeSubclassOf(typeMap("field1" -> Typed[String])) shouldBe true - typeMap("field1" -> Typed[String]).canBeSubclassOf(Unknown) shouldBe true + Unknown.canBeImplicitlyConvertedTo(typeMap("field1" -> Typed[String])) shouldBe true + typeMap("field1" -> Typed[String]).canBeImplicitlyConvertedTo(Unknown) shouldBe true } test("determine if can be subclass for class") { Typed .fromDetailedType[java.util.List[BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.List[BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true Typed .fromDetailedType[java.util.List[Number]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe false + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, Number]]) shouldBe true + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, Number]]) shouldBe true Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[Number, Number]]) shouldBe false + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[Number, Number]]) shouldBe false Typed .fromDetailedType[java.util.Map[Number, Number]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[Number, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, Number]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[_, BigDecimal]]) shouldBe true + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[_, BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.Map[_, BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true // For arrays it might be tricky - Typed.fromDetailedType[Array[BigDecimal]].canBeSubclassOf(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe true - Typed.fromDetailedType[Array[BigDecimal]].canBeSubclassOf(Typed.fromDetailedType[Array[Number]]) shouldBe true - Typed.fromDetailedType[Array[Number]].canBeSubclassOf(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe false + Typed + .fromDetailedType[Array[BigDecimal]] + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe true + Typed + .fromDetailedType[Array[BigDecimal]] + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[Array[Number]]) shouldBe true + Typed + .fromDetailedType[Array[Number]] + .canBeImplicitlyConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe false } test("determine if numbers can be converted") { - Typed[Int].canBeSubclassOf(Typed[Long]) shouldBe true - Typed[Long].canBeSubclassOf(Typed[Int]) shouldBe false - Typed[Long].canBeSubclassOf(Typed[Double]) shouldBe true - Typed[Double].canBeSubclassOf(Typed[Long]) shouldBe false - Typed[java.math.BigDecimal].canBeSubclassOf(Typed[Long]) shouldBe true - Typed[Long].canBeSubclassOf(Typed[java.math.BigDecimal]) shouldBe true + Typed[Int].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe true + Typed[Long].canBeImplicitlyConvertedTo(Typed[Int]) shouldBe false + Typed[Long].canBeImplicitlyConvertedTo(Typed[Double]) shouldBe true + Typed[Double].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe false + Typed[java.math.BigDecimal].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe true + Typed[Long].canBeImplicitlyConvertedTo(Typed[java.math.BigDecimal]) shouldBe true } test("find common supertype for simple types") { @@ -297,22 +303,22 @@ class TypingResultSpec test("determine if can be subclass for tagged value") { Typed .tagged(Typed.typedClass[String], "tag1") - .canBeSubclassOf(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe true + .canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe true Typed .tagged(Typed.typedClass[String], "tag1") - .canBeSubclassOf(Typed.tagged(Typed.typedClass[String], "tag2")) shouldBe false + .canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[String], "tag2")) shouldBe false Typed .tagged(Typed.typedClass[String], "tag1") - .canBeSubclassOf(Typed.tagged(Typed.typedClass[Integer], "tag1")) shouldBe false - Typed.tagged(Typed.typedClass[String], "tag1").canBeSubclassOf(Typed.typedClass[String]) shouldBe true - Typed.typedClass[String].canBeSubclassOf(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe false + .canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[Integer], "tag1")) shouldBe false + Typed.tagged(Typed.typedClass[String], "tag1").canBeImplicitlyConvertedTo(Typed.typedClass[String]) shouldBe true + Typed.typedClass[String].canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe false } test("determine if can be subclass for null") { - TypedNull.canBeSubclassOf(Typed[Int]) shouldBe true - TypedNull.canBeSubclassOf(Typed.fromInstance(4)) shouldBe false - TypedNull.canBeSubclassOf(TypedNull) shouldBe true - Typed[String].canBeSubclassOf(TypedNull) shouldBe false + TypedNull.canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true + TypedNull.canBeImplicitlyConvertedTo(Typed.fromInstance(4)) shouldBe false + TypedNull.canBeImplicitlyConvertedTo(TypedNull) shouldBe true + Typed[String].canBeImplicitlyConvertedTo(TypedNull) shouldBe false } test("should deeply extract typ parameters") { @@ -331,20 +337,20 @@ class TypingResultSpec } test("determine if can be subclass for object with value") { - Typed.fromInstance(45).canBeSubclassOf(Typed.typedClass[Long]) shouldBe true - Typed.fromInstance(29).canBeSubclassOf(Typed.typedClass[String]) shouldBe false - Typed.fromInstance(78).canBeSubclassOf(Typed.fromInstance(78)) shouldBe true - Typed.fromInstance(12).canBeSubclassOf(Typed.fromInstance(15)) shouldBe false - Typed.fromInstance(41).canBeSubclassOf(Typed.fromInstance("t")) shouldBe false - Typed.typedClass[String].canBeSubclassOf(Typed.fromInstance("t")) shouldBe true + Typed.fromInstance(45).canBeImplicitlyConvertedTo(Typed.typedClass[Long]) shouldBe true + Typed.fromInstance(29).canBeImplicitlyConvertedTo(Typed.typedClass[String]) shouldBe false + Typed.fromInstance(78).canBeImplicitlyConvertedTo(Typed.fromInstance(78)) shouldBe true + Typed.fromInstance(12).canBeImplicitlyConvertedTo(Typed.fromInstance(15)) shouldBe false + Typed.fromInstance(41).canBeImplicitlyConvertedTo(Typed.fromInstance("t")) shouldBe false + Typed.typedClass[String].canBeImplicitlyConvertedTo(Typed.fromInstance("t")) shouldBe true } test("determine if can be subclass for object with value - use conversion") { - Typed.fromInstance("2007-12-03").canBeSubclassOf(Typed.typedClass[LocalDate]) shouldBe true - Typed.fromInstance("2007-12-03T10:15:30").canBeSubclassOf(Typed.typedClass[LocalDateTime]) shouldBe true + Typed.fromInstance("2007-12-03").canBeImplicitlyConvertedTo(Typed.typedClass[LocalDate]) shouldBe true + Typed.fromInstance("2007-12-03T10:15:30").canBeImplicitlyConvertedTo(Typed.typedClass[LocalDateTime]) shouldBe true - Typed.fromInstance("2007-12-03-qwerty").canBeSubclassOf(Typed.typedClass[LocalDate]) shouldBe false - Typed.fromInstance("2007-12-03").canBeSubclassOf(Typed.typedClass[Currency]) shouldBe false + Typed.fromInstance("2007-12-03-qwerty").canBeImplicitlyConvertedTo(Typed.typedClass[LocalDate]) shouldBe false + Typed.fromInstance("2007-12-03").canBeImplicitlyConvertedTo(Typed.typedClass[Currency]) shouldBe false } test("determinate if can be superclass for objects with value") { @@ -443,7 +449,7 @@ class TypingResultSpec logger.trace(s"Checking: ${input.display}") withClue(s"Input: ${input.display};") { - input.canBeSubclassOf(input) shouldBe true + input.canBeImplicitlyConvertedTo(input) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(input, input) withClue(s"Supertype: ${superType.display};") { superType shouldEqual input @@ -457,11 +463,11 @@ class TypingResultSpec logger.trace(s"Checking: ${input.display}") withClue(s"Input: ${input.display};") { - input.canBeSubclassOf(input) shouldBe true + input.canBeImplicitlyConvertedTo(input) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(input, input) withClue(s"Supertype: ${superType.display};") { // We generate combinations of types co we can only check if input type is a subclass of super type - input.canBeSubclassOf(superType) + input.canBeImplicitlyConvertedTo(superType) } } } @@ -477,12 +483,12 @@ class TypingResultSpec logger.trace(s"Checking supertype of: ${first.display} and ${second.display}") withClue(s"Input: ${first.display}; ${second.display};") { - first.canBeSubclassOf(first) shouldBe true - second.canBeSubclassOf(second) shouldBe true + first.canBeImplicitlyConvertedTo(first) shouldBe true + second.canBeImplicitlyConvertedTo(second) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(first, second) withClue(s"Supertype: ${superType.display};") { - first.canBeSubclassOf(superType) - second.canBeSubclassOf(superType) + first.canBeImplicitlyConvertedTo(superType) + second.canBeImplicitlyConvertedTo(superType) } } } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala index 3b89e39be58..16749e74938 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala @@ -62,7 +62,9 @@ class DictApiHttpService( Future { success( dictionaries - .filter { case (id, definition) => definition.valueType(id).canBeSubclassOf(expectedType) } + .filter { case (id, definition) => + definition.valueType(id).canBeImplicitlyConvertedTo(expectedType) + } .map { case (id, _) => DictDto(id, id) } .toList ) diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala index d5c1366fa17..c191fb9d1a4 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala @@ -7,7 +7,7 @@ import pl.touk.nussknacker.engine.api.definition.{DualParameterEditor, Parameter import pl.touk.nussknacker.engine.api.editor.DualEditorMode import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.api.test.ScenarioTestData -import pl.touk.nussknacker.engine.api.typed.CanBeSubclassDeterminer +import pl.touk.nussknacker.engine.api.typed.ImplicitConversionDeterminer import pl.touk.nussknacker.engine.api.typed.typing.Typed import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.definition.test.{TestInfoProvider, TestingCapabilities} @@ -134,7 +134,7 @@ class ScenarioTestService( val adaptedParameters = uiSourceParameter.parameters.map { uiParameter => uiParameter.editor match { case DualParameterEditor(StringParameterEditor, DualEditorMode.RAW) - if uiParameter.typ.canBeSubclassOf(Typed[String]) => + if uiParameter.typ.canBeImplicitlyConvertedTo(Typed[String]) => uiParameter.copy(editor = StringParameterEditor) case _ => uiParameter } diff --git a/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala b/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala index f4ad8ff12ee..12350236313 100644 --- a/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala +++ b/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala @@ -74,8 +74,8 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat private def shouldBeInstanceOf(obj: Any, typ: TypingResult): Unit = { val typeFromInstance = Typed.fromInstance(obj) - val canBeSubclassCase = typeFromInstance.canBeSubclassOf(typ) - val typedObjectCase = typ.isInstanceOf[TypedObjectTypingResult] && typeFromInstance.canBeSubclassOf( + val canBeSubclassCase = typeFromInstance.canBeImplicitlyConvertedTo(typ) + val typedObjectCase = typ.isInstanceOf[TypedObjectTypingResult] && typeFromInstance.canBeImplicitlyConvertedTo( typ.asInstanceOf[TypedObjectTypingResult].runtimeObjType ) (canBeSubclassCase || typedObjectCase) shouldBe true diff --git a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala index bdb37f86bee..a9f883d1e6e 100644 --- a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala +++ b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala @@ -190,7 +190,7 @@ object aggregates { override def result(finalAggregate: Aggregate): AnyRef = finalAggregate override def computeOutputType(input: typing.TypingResult): Validated[String, typing.TypingResult] = { - if (input.canBeSubclassOf(Typed[Boolean])) { + if (input.canBeImplicitlyConvertedTo(Typed[Boolean])) { Valid(Typed[Long]) } else { Invalid(s"Invalid aggregate type: ${input.display}, should be: ${Typed[Boolean].display}") @@ -239,7 +239,7 @@ object aggregates { override def computeOutputType(input: typing.TypingResult): Validated[String, typing.TypingResult] = { - if (!input.canBeSubclassOf(Typed[Number])) { + if (!input.canBeImplicitlyConvertedTo(Typed[Number])) { Invalid(s"Invalid aggregate type: ${input.display}, should be: ${Typed[Number].display}") } else { Valid(ForLargeFloatingNumbersOperation.promoteSingle(input)) @@ -353,7 +353,9 @@ object aggregates { ): Validated[String, TypedObjectTypingResult] = { input match { case TypedObjectTypingResult(inputFields, klass, _) - if inputFields.keySet == scalaFields.keySet && klass.canBeSubclassOf(Typed[java.util.Map[String, _]]) => + if inputFields.keySet == scalaFields.keySet && klass.canBeImplicitlyConvertedTo( + Typed[java.util.Map[String, _]] + ) => val validationRes = scalaFields .map { case (key, aggregator) => computeField(aggregator, inputFields(key)) @@ -437,7 +439,7 @@ object aggregates { trait MathAggregator { self: ReducingAggregator => override def computeOutputType(input: typing.TypingResult): Validated[String, typing.TypingResult] = { - if (input.canBeSubclassOf(Typed[Number])) { + if (input.canBeImplicitlyConvertedTo(Typed[Number])) { // In some cases type can be promoted to other class e.g. Byte is promoted to Int for sum Valid(promotionStrategy.promoteSingle(input)) } else { diff --git a/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala b/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala index f87e70e735d..951be76ce26 100644 --- a/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala +++ b/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala @@ -45,7 +45,9 @@ object ForEachTransformer extends CustomStreamTransformer with Serializable { private def returnType(elements: LazyParameter[util.Collection[AnyRef]]): typing.TypingResult = elements.returnType match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeSubclassOf(Typed[util.Collection[_]]) && tc.runtimeObjType.params.nonEmpty => + if tc.runtimeObjType.canBeImplicitlyConvertedTo( + Typed[util.Collection[_]] + ) && tc.runtimeObjType.params.nonEmpty => tc.runtimeObjType.params.head case _ => Unknown } diff --git a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala index 42c2f4d560c..05f6b8cda9e 100644 --- a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala +++ b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala @@ -14,7 +14,7 @@ object TableTypeOutputValidator { val aligned = ToTableTypeSchemaBasedEncoder.alignTypingResult(actualType, expectedType) val expectedTypingResult = expectedType.toTypingResult - if (aligned.canBeSubclassOf(expectedTypingResult)) { + if (aligned.canBeImplicitlyConvertedTo(expectedTypingResult)) { Valid(()) } else { invalidNel( diff --git a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala index d3e36185645..27ae00600c8 100644 --- a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala +++ b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala @@ -26,7 +26,8 @@ object ToTableTypeSchemaBasedEncoder { case (null, _) => null // We don't know what is the precise of decimal so we have to assume that it will fit the target type to not block the user - case (number: Number, _) if Typed.typedClass(number.getClass).canBeSubclassOf(targetType.toTypingResult) => + case (number: Number, _) + if Typed.typedClass(number.getClass).canBeImplicitlyConvertedTo(targetType.toTypingResult) => NumberUtils .convertNumberToTargetClass[Number](number, targetType.getDefaultConversion.asInstanceOf[Class[Number]]) case (_, rowType: RowType) => @@ -82,7 +83,8 @@ object ToTableTypeSchemaBasedEncoder { targetType.toTypingResult // We don't know what is the precision of decimal so we have to assume that it will fit the target type to not block the user case (typ: SingleTypingResult, _) - if typ.canBeSubclassOf(Typed[Number]) && typ.canBeSubclassOf(targetType.toTypingResult) => + if typ + .canBeImplicitlyConvertedTo(Typed[Number]) && typ.canBeImplicitlyConvertedTo(targetType.toTypingResult) => targetType.toTypingResult case (recordType: TypedObjectTypingResult, rowType: RowType) if Set[Class[_]](javaMapClass, rowClass).contains(recordType.runtimeObjType.klass) => @@ -104,10 +106,10 @@ object ToTableTypeSchemaBasedEncoder { case ( TypedObjectTypingResult(_, TypedClass(`javaMapClass`, keyType :: valueType :: Nil), _), multisetType: MultisetType - ) if valueType.canBeSubclassOf(Typed[Int]) => + ) if valueType.canBeImplicitlyConvertedTo(Typed[Int]) => alignMultisetType(keyType, multisetType) case (TypedClass(`javaMapClass`, keyType :: valueType :: Nil), multisetType: MultisetType) - if valueType.canBeSubclassOf(Typed[Int]) => + if valueType.canBeImplicitlyConvertedTo(Typed[Int]) => alignMultisetType(keyType, multisetType) case (TypedClass(`arrayClass`, elementType :: Nil), arrayType: ArrayType) => Typed.genericTypeClass(arrayClass, List(alignTypingResult(elementType, arrayType.getElementType))) diff --git a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala index d022901ffc8..bcbc358fc7c 100644 --- a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala +++ b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala @@ -62,7 +62,7 @@ object DocumentationFunctions { (left.withoutValue, right.withoutValue) match { case (`intType`, `intType`) => intType.validNel case (`doubleType`, `doubleType`) => doubleType.validNel - case (l, r) if List(l, r).forall(_.canBeSubclassOf(numberType)) => + case (l, r) if List(l, r).forall(_.canBeImplicitlyConvertedTo(numberType)) => OtherError(s"Addition of ${l.display} and ${r.display} is not supported").invalidNel case (`stringType`, `stringType`) => stringType.validNel case _ => ArgumentTypeError.invalidNel @@ -110,7 +110,7 @@ object DocumentationFunctions { case Some(v) => v.validNel case None => OtherError("No field with given name").invalidNel } - case TypedObjectTypingResult(_, _, _) :: x :: Nil if x.canBeSubclassOf(stringType) => + case TypedObjectTypingResult(_, _, _) :: x :: Nil if x.canBeImplicitlyConvertedTo(stringType) => OtherError("Expected string with known value").invalidNel case _ => ArgumentTypeError.invalidNel diff --git a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala index e17be319fa6..723c0d2257b 100644 --- a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala +++ b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala @@ -147,7 +147,7 @@ object ExampleFunctions { override def computeResultType( arguments: List[TypingResult] ): ValidatedNel[GenericFunctionTypingError, TypingResult] = { - if (arguments.exists(!_.canBeSubclassOf(Typed[Number]))) return ArgumentTypeError.invalidNel + if (arguments.exists(!_.canBeImplicitlyConvertedTo(Typed[Number]))) return ArgumentTypeError.invalidNel arguments match { case t :: Nil => t.validNel case l :: r :: Nil => Typed.record(Map("left" -> l, "right" -> r)).validNel diff --git a/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala b/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala index cdceb363052..a2f82110e22 100644 --- a/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala +++ b/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala @@ -42,7 +42,9 @@ class ForEachTransformerComponent(elements: LazyParameter[java.util.Collection[A override def returnType: typing.TypingResult = { elements.returnType match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Collection[_]]) && tc.runtimeObjType.params.nonEmpty => + if tc.runtimeObjType.canBeImplicitlyConvertedTo( + Typed[java.util.Collection[_]] + ) && tc.runtimeObjType.params.nonEmpty => tc.runtimeObjType.params.head case _ => Unknown } diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala index aae6d60de30..496b371e14a 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala @@ -169,7 +169,7 @@ case class FragmentParameterValidator(classDefinitions: Set[ClassDefinition] = S val dictValueType = dictDefinition.valueType(dictId) - if (dictValueType.canBeSubclassOf(fragmentParameterTypingResult)) { + if (dictValueType.canBeImplicitlyConvertedTo(fragmentParameterTypingResult)) { Valid(()) } else { invalidNel( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala index 263c406181a..d86ef54a602 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala @@ -141,7 +141,7 @@ class ClassDefinitionExtractor(settings: ClassExtractionSettings) extends LazyLo methodsForParams .find { case (_, method) => - methodsForParams.forall(mi => method.signature.result.canBeSubclassOf(mi._2.signature.result)) + methodsForParams.forall(mi => method.signature.result.canBeImplicitlyConvertedTo(mi._2.signature.result)) } .getOrElse(methodsForParams.minBy(_._2.signature.result.display)) } @@ -273,7 +273,7 @@ class ClassDefinitionExtractor(settings: ClassExtractionSettings) extends LazyLo ) reflectionBasedDefinition.result } - if (returnedResultType.canBeSubclassOf(returnedResultType)) { + if (returnedResultType.canBeImplicitlyConvertedTo(returnedResultType)) { returnedResultType } else { logger.warn( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala index bad04d2eca9..452d5b64258 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala @@ -35,13 +35,15 @@ sealed trait MethodDefinition { // Allow pass array as List argument because of array to list auto conversion: // pl.touk.nussknacker.engine.spel.internal.ArrayToListConverter case (tc @ TypedClass(klass, _), Parameter(_, y)) if klass.isArray => - tc.canBeSubclassOf(y) || Typed.genericTypeClass[java.util.List[_]](tc.params).canBeSubclassOf(y) - case (x, Parameter(_, y)) => x.canBeSubclassOf(y) + tc.canBeImplicitlyConvertedTo(y) || Typed + .genericTypeClass[java.util.List[_]](tc.params) + .canBeImplicitlyConvertedTo(y) + case (x, Parameter(_, y)) => x.canBeImplicitlyConvertedTo(y) } val checkVarArgs = methodTypeInfo.varArg match { case Some(Parameter(_, t)) => - arguments.drop(methodTypeInfo.noVarArgs.length).forall(_.canBeSubclassOf(t)) + arguments.drop(methodTypeInfo.noVarArgs.length).forall(_.canBeImplicitlyConvertedTo(t)) case None => arguments.length == methodTypeInfo.noVarArgs.length } @@ -94,7 +96,7 @@ case class FunctionalMethodDefinition( val typeCalculated = typeFunction(methodInvocationTarget, arguments).leftMap(_.map(errorConverter.convert)) typeCalculated.map { calculated => - if (!typesFromStaticMethodInfo.exists(calculated.canBeSubclassOf)) { + if (!typesFromStaticMethodInfo.exists(calculated.canBeImplicitlyConvertedTo)) { val expectedTypesString = typesFromStaticMethodInfo.map(_.display).mkString("(", ", ", ")") val argumentsString = arguments.map(_.display).mkString("(", ", ", ")") throw new AssertionError( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala index c6868bb4bb5..8a148911c10 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala @@ -12,7 +12,7 @@ object MethodTypeInfoSubclassChecker { val MethodTypeInfo(superclassNoVarArg, superclassVarArgOption, superclassResult) = superclassInfo val validatedVarArgs = (subclassVarArgOption, superclassVarArgOption) match { - case (Some(sub), Some(sup)) if sub.refClazz.canBeSubclassOf(sup.refClazz) => ().validNel + case (Some(sub), Some(sup)) if sub.refClazz.canBeImplicitlyConvertedTo(sup.refClazz) => ().validNel case (Some(sub), Some(sup)) => NotSubclassVarArgument(sub.refClazz, sup.refClazz).invalidNel case (Some(_), None) => BadVarArg.invalidNel case (None, Some(_)) => ().validNel @@ -38,7 +38,7 @@ object MethodTypeInfoSubclassChecker { ) val validatedNoVarArgs = zippedParameters.zipWithIndex .map { - case ((sub, sup), _) if sub.refClazz.canBeSubclassOf(sup.refClazz) => ().validNel + case ((sub, sup), _) if sub.refClazz.canBeImplicitlyConvertedTo(sup.refClazz) => ().validNel case ((sub, sup), i) => NotSubclassArgument(i + 1, sub.refClazz, sup.refClazz).invalidNel } .sequence @@ -46,7 +46,7 @@ object MethodTypeInfoSubclassChecker { val validatedResult = Validated.condNel( - subclassResult.canBeSubclassOf(superclassResult), + subclassResult.canBeImplicitlyConvertedTo(superclassResult), (), NotSubclassResult(subclassResult, superclassResult) ) diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala index 2a2eb13bf82..fffcc4d138e 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala @@ -85,11 +85,11 @@ object DictKeyWithLabelExpressionParser extends ExpressionParser { override def language: Language = languageId override def evaluate[T](ctx: Context, globals: Map[String, Any]): T = { - if (expectedType.canBeSubclassOf(Typed[Long])) { + if (expectedType.canBeImplicitlyConvertedTo(Typed[Long])) { key.toLong.asInstanceOf[T] - } else if (expectedType.canBeSubclassOf(Typed[Boolean])) { + } else if (expectedType.canBeImplicitlyConvertedTo(Typed[Boolean])) { key.toBoolean.asInstanceOf[T] - } else if (expectedType.canBeSubclassOf(Typed[String])) { + } else if (expectedType.canBeImplicitlyConvertedTo(Typed[String])) { key.asInstanceOf[T] } else { throw new IllegalStateException( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala index 333c1ff0a86..a63782e2b9f 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala @@ -411,9 +411,9 @@ class SpelExpressionSuggester( private def determineIterableElementTypingResult(parent: TypingResult): TypingResult = { parent match { - case tc: SingleTypingResult if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Collection[_]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Collection[_]]) => tc.runtimeObjType.params.headOption.getOrElse(Unknown) - case tc: SingleTypingResult if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Map[_, _]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) => Typed.record( Map( "key" -> tc.runtimeObjType.params.headOption.getOrElse(Unknown), diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 099e5c44183..1c7aa1abe60 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -146,8 +146,8 @@ private[spel] class Typer( def withChildrenOfType[Parts: universe.TypeTag](result: TypingResult) = { val w = valid(result) withTypedChildren { - case list if list.forall(_.canBeSubclassOf(Typed.fromDetailedType[Parts])) => w - case _ => w.tell(List(PartTypeError)) + case list if list.forall(_.canBeImplicitlyConvertedTo(Typed.fromDetailedType[Parts])) => w + case _ => w.tell(List(PartTypeError)) } } @@ -198,7 +198,7 @@ private[spel] class Typer( case (ref: PropertyOrFieldReference) :: Nil => typeFieldNameReferenceOnRecord(ref.getName, record) case _ => typeFieldNameReferenceOnRecord(indexString, record) } - case indexKey :: Nil if indexKey.canBeSubclassOf(Typed[String]) => + case indexKey :: Nil if indexKey.canBeImplicitlyConvertedTo(Typed[String]) => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) case _ :: Nil => indexer.children match { @@ -356,7 +356,8 @@ private[spel] class Typer( case e: OpMinus => withTypedChildren { - case left :: right :: Nil if left.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(Typed[Number]) => + case left :: right :: Nil + if left.canBeImplicitlyConvertedTo(Typed[Number]) && right.canBeImplicitlyConvertedTo(Typed[Number]) => val fallback = NumberTypesPromotionStrategy.ForMathOperation.promote(left, right) operationOnTypesValue[Number, Number, Number](left, right, fallback)((n1, n2) => Valid(MathUtils.minus(n1, n2)) @@ -365,7 +366,7 @@ private[spel] class Typer( invalid(OperatorNonNumericError(e.getOperatorName, left)) case left :: right :: Nil => invalid(OperatorMismatchTypeError(e.getOperatorName, left, right)) - case left :: Nil if left.canBeSubclassOf(Typed[Number]) => + case left :: Nil if left.canBeImplicitlyConvertedTo(Typed[Number]) => val resultType = left.withoutValue val result = operationOnTypesValue[Number, Number](left)(MathUtils.negate).getOrElse(resultType) valid(result) @@ -395,18 +396,20 @@ private[spel] class Typer( withTypedChildren { case left :: right :: Nil if left == Unknown || right == Unknown => valid(Unknown) - case left :: right :: Nil if left.canBeSubclassOf(Typed[String]) || right.canBeSubclassOf(Typed[String]) => + case left :: right :: Nil + if left.canBeImplicitlyConvertedTo(Typed[String]) || right.canBeImplicitlyConvertedTo(Typed[String]) => operationOnTypesValue[Any, Any, String](left, right, Typed[String])((l, r) => Valid(l.toString + r.toString) ) - case left :: right :: Nil if left.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(Typed[Number]) => + case left :: right :: Nil + if left.canBeImplicitlyConvertedTo(Typed[Number]) && right.canBeImplicitlyConvertedTo(Typed[Number]) => val fallback = NumberTypesPromotionStrategy.ForMathOperation.promote(left, right) operationOnTypesValue[Number, Number, Number](left, right, fallback)((n1, n2) => Valid(MathUtils.plus(n1, n2)) ) case left :: right :: Nil => invalid(OperatorMismatchTypeError(e.getOperatorName, left, right)) - case left :: Nil if left.canBeSubclassOf(Typed[Number]) => + case left :: Nil if left.canBeImplicitlyConvertedTo(Typed[Number]) => valid(left) case left :: Nil => invalid(OperatorNonNumericError(e.getOperatorName, left)) @@ -448,7 +451,7 @@ private[spel] class Typer( elementType <- extractIterativeType(iterateType) selectionType = resolveSelectionTypingResult(e, iterateType, elementType) result <- typeChildren(validationContext, node, current.pushOnStack(elementType)) { - case result :: Nil if result.canBeSubclassOf(Typed[Boolean]) => + case result :: Nil if result.canBeImplicitlyConvertedTo(Typed[Boolean]) => valid(selectionType) case other => invalid(IllegalSelectionTypeError(other), selectionType) @@ -459,7 +462,7 @@ private[spel] class Typer( case condition :: onTrue :: onFalse :: Nil => for { _ <- Option(condition) - .filter(_.canBeSubclassOf(Typed[Boolean])) + .filter(_.canBeImplicitlyConvertedTo(Typed[Boolean])) .map(valid) .getOrElse(invalid(TernaryOperatorNotBooleanError(condition))) } yield CommonSupertypeFinder.Default.commonSupertype(onTrue, onFalse) @@ -520,10 +523,10 @@ private[spel] class Typer( // as properly determining it would require evaluating the selection expression for each element (likely working on the AST) parentType match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Collection[_]]) || + if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Collection[_]]) || tc.runtimeObjType.klass.isArray => tc.withoutValue - case tc: SingleTypingResult if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Map[_, _]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) => Typed.record(Map.empty) case _ => parentType @@ -575,7 +578,8 @@ private[spel] class Typer( op: Option[(Number, Number) => Validated[ExpressionParseError, Any]] )(implicit numberPromotionStrategy: NumberTypesPromotionStrategy): TypingR[CollectedTypingResult] = { typeChildren(validationContext, node, current) { - case left :: right :: Nil if left.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(Typed[Number]) => + case left :: right :: Nil + if left.canBeImplicitlyConvertedTo(Typed[Number]) && right.canBeImplicitlyConvertedTo(Typed[Number]) => val fallback = numberPromotionStrategy.promote(left, right) op .map(operationOnTypesValue[Number, Number, Any](left, right, fallback)(_)) @@ -594,7 +598,7 @@ private[spel] class Typer( current: TypingContext )(op: Number => Any): TypingR[CollectedTypingResult] = { typeChildren(validationContext, node, current) { - case left :: Nil if left.canBeSubclassOf(Typed[Number]) => + case left :: Nil if left.canBeImplicitlyConvertedTo(Typed[Number]) => val result = operationOnTypesValue[Number, Any](left)(op).getOrElse(left.withoutValue) valid(result) case left :: Nil => @@ -690,10 +694,10 @@ private[spel] class Typer( private def extractIterativeType(parent: TypingResult): TypingR[TypingResult] = parent match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Collection[_]]) || + if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Collection[_]]) || tc.runtimeObjType.klass.isArray => valid(tc.runtimeObjType.params.headOption.getOrElse(Unknown)) - case tc: SingleTypingResult if tc.runtimeObjType.canBeSubclassOf(Typed[java.util.Map[_, _]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) => valid( Typed.record( Map( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala index ad0d4bf8ae8..9926b337e95 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala @@ -62,7 +62,7 @@ class MethodReferenceTyper(classDefinitionSet: ClassDefinitionSet, methodExecuti )(implicit reference: MethodReference): Either[Option[ExpressionParseError], NonEmptyList[MethodDefinition]] = { def displayableType = clazzDefinitions.map(k => k.clazzName).map(_.display).toList.mkString(", ") - def isClass = clazzDefinitions.map(k => k.clazzName).exists(_.canBeSubclassOf(Typed[Class[_]])) + def isClass = clazzDefinitions.map(k => k.clazzName).exists(_.canBeImplicitlyConvertedTo(Typed[Class[_]])) val clazzMethods = if (reference.isStatic) clazzDefinitions.toList.flatMap(_.staticMethods.get(reference.methodName).toList.flatten) diff --git a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala index 862381c03de..0858fe97465 100644 --- a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala +++ b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala @@ -356,7 +356,9 @@ object CollectionUtils { listType.copy(params = unknownMapType :: Nil) case _ if firstComponentType.withoutValue == secondComponentType.withoutValue => listType.copy(params = firstComponentType.withoutValue :: Nil) - case _ if firstComponentType.canBeSubclassOf(numberType) && secondComponentType.canBeSubclassOf(numberType) => + case _ + if firstComponentType.canBeImplicitlyConvertedTo(numberType) && secondComponentType + .canBeImplicitlyConvertedTo(numberType) => Typed.genericTypeClass(fClass, List(numberType)) case _ => listType.copy(params = Unknown :: Nil) } diff --git a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala index 2233d31fd93..bc6d1d2a1f5 100644 --- a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala +++ b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala @@ -129,7 +129,7 @@ object NumericUtils { override def computeResultType( arguments: List[typing.TypingResult] ): ValidatedNel[GenericFunctionTypingError, typing.TypingResult] = { - if (arguments.head.canBeSubclassOf(Typed[Number])) arguments.head.withoutValue.validNel + if (arguments.head.canBeImplicitlyConvertedTo(Typed[Number])) arguments.head.withoutValue.validNel else Typed[Number].validNel } diff --git a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala index 7d946008f7c..1219aacde4a 100644 --- a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala +++ b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala @@ -370,7 +370,7 @@ class JsonSchemaOutputValidator(validationMode: ValidationMode) extends LazyLogg case (TypedClass(_, Nil), TypedClass(_, Nil)) => invalid(typingResult, schema, rootSchema, path) case _ => condNel( - typingResult.canBeSubclassOf(schemaAsTypedResult), + typingResult.canBeImplicitlyConvertedTo(schemaAsTypedResult), (), OutputValidatorTypeError(path, typingResult, JsonSchemaExpected(schema, rootSchema)) ) diff --git a/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala b/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala index 6d5fc0b8f52..48292b5697e 100644 --- a/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala +++ b/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala @@ -152,9 +152,11 @@ class AvroSchemaOutputValidator(validationMode: ValidationMode) extends LazyLogg typingResult match { case _ @TypedClass(klass, key :: value :: Nil) if isMap(klass) => // Map keys are assumed to be strings: https://avro.apache.org/docs/current/spec.html#Maps - condNel(key.canBeSubclassOf(Typed.apply[java.lang.String]), (), typeError(typingResult, schema, path)).andThen( - _ => validateTypingResult(value, schema.getValueType, buildPath("*", path, useIndexer = true)) - ) + condNel( + key.canBeImplicitlyConvertedTo(Typed.apply[java.lang.String]), + (), + typeError(typingResult, schema, path) + ).andThen(_ => validateTypingResult(value, schema.getValueType, buildPath("*", path, useIndexer = true))) case map @ TypedClass(klass, _) if isMap(klass) => throw new IllegalArgumentException(s"Illegal typing Map: $map.") case _ @TypedObjectTypingResult(fields, TypedClass(klass, _), _) if isMap(klass) => From aeee5a89843547f926b8f7ae83c31d316c10f542 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Thu, 7 Nov 2024 22:02:48 +0100 Subject: [PATCH 12/26] Cleaned up tests and naming --- .../api/typed/ConversionDeterminer.scala | 23 +---------- .../typed/ImplicitConversionDeterminer.scala | 18 +++++++- .../engine/api/typed/SubclassDeterminer.scala | 15 ++++++- .../api/typed/TypeConversionHandler.scala | 24 ++++++++++- .../typed/CanBeSubclassDeterminerSpec.scala | 41 ------------------- .../api/typed/SubclassDeterminerSpec.scala | 41 +++++++++++++++++++ .../ui/api/DictApiHttpService.scala | 2 +- 7 files changed, 96 insertions(+), 68 deletions(-) delete mode 100644 components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala create mode 100644 components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala index 5617a300d34..b998edab8c3 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala @@ -42,7 +42,7 @@ trait ConversionDeterminer { } } - def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { + private def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { // TODO: Null should not be subclass of typed map that has all values assigned. case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel case _ => ().validNel @@ -66,15 +66,6 @@ trait ConversionDeterminer { ) } - // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) - def canBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: TypedClass - ): ValidatedNel[String, Unit] = { - val errMsgPrefix = s"${givenType.runtimeObjType.display} cannot be converted to ${superclassCandidate.display}" - condNel(TypeConversionHandler.canBeConvertedTo(givenType, superclassCandidate), (), errMsgPrefix) - } - def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { condNel( givenClass == givenSuperclass, @@ -88,15 +79,5 @@ trait ConversionDeterminer { ) } - // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... - def isAssignable(from: Class[_], to: Class[_]): Boolean = { - (from, to) match { - case (f, t) if ClassUtils.isAssignable(f, t, true) => true - // Number double check by hand because lang3 can incorrectly throw false when dealing with java types - case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => - AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) - case _ => false - } - } - + def isAssignable(from: Class[_], to: Class[_]): Boolean } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala index 530aa163b19..f980620ae77 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala @@ -3,6 +3,7 @@ package pl.touk.nussknacker.engine.api.typed import cats.data.Validated._ import cats.data.ValidatedNel import cats.implicits.{catsSyntaxValidatedId, _} +import org.apache.commons.lang3.ClassUtils import pl.touk.nussknacker.engine.api.typed.typing._ /** @@ -19,7 +20,7 @@ object ImplicitConversionDeterminer extends ConversionDeterminer { private val javaListClass = classOf[java.util.List[_]] private val arrayOfAnyRefClass = classOf[Array[AnyRef]] - protected def singleCanBeConvertedTo( + def singleCanBeConvertedTo( givenType: SingleTypingResult, superclassCandidate: SingleTypingResult ): ValidatedNel[String, Unit] = { @@ -106,7 +107,16 @@ object ImplicitConversionDeterminer extends ConversionDeterminer { val equalClassesOrCanAssign = isStrictSubclass(givenClass, givenSuperclass) val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, givenSuperclass)) - canBeSubclass orElse canBeConvertedTo(givenType, superclassCandidate) + canBeSubclass orElse canBeConvertedTo(givenType, givenSuperclass) + } + + // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) + def canBeConvertedTo( + givenType: SingleTypingResult, + superclassCandidate: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = s"${givenType.runtimeObjType.display} cannot be converted to ${superclassCandidate.display}" + condNel(TypeConversionHandler.canBeConvertedTo(givenType, superclassCandidate), (), errMsgPrefix) } private def typeParametersMatches(givenClass: TypedClass, superclassCandidate: TypedClass) = { @@ -147,4 +157,8 @@ object ImplicitConversionDeterminer extends ConversionDeterminer { } } + // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... + override def isAssignable(from: Class[_], to: Class[_]): Boolean = + ClassUtils.isAssignable(from, to, true) + } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala index 28de6dd53c2..4fcf9a0aa01 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala @@ -1,7 +1,10 @@ package pl.touk.nussknacker.engine.api.typed +import cats.data.Validated.condNel import cats.data.ValidatedNel -import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypingResult} +import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers +import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypedClass, TypingResult} object SubclassDeterminer extends ConversionDeterminer { @@ -19,4 +22,14 @@ object SubclassDeterminer extends ConversionDeterminer { this.canBeConvertedTo(givenType, superclassCandidate) } + override def isAssignable(from: Class[_], to: Class[_]): Boolean = { + (from, to) match { + case (f, t) if ClassUtils.isAssignable(f, t, true) => true + // Number double check by hand because lang3 can incorrectly throw false when dealing with java types + case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => + AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) + case _ => false + } + } + } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index f94a190890e..8312ff4ee15 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -68,12 +68,33 @@ object TypeConversionHandler { handleStringToValueClassConversions(givenType, superclassCandidate) } + def canBeStrictlyConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = { + handleStrictNumberConversions(givenType.runtimeObjType, superclassCandidate) || + handleStringToValueClassConversions(givenType, superclassCandidate) + } + // See org.springframework.core.convert.support.NumberToNumberConverterFactory private def handleNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) // We can't check precision here so we need to be loose here // TODO: Add feature flag: strictNumberPrecisionChecking (default false?) + if (NumberTypesPromotionStrategy + .isFloatingNumber(boxedSuperclassCandidate) || boxedSuperclassCandidate == classOf[java.math.BigDecimal]) { + ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) + } else if (NumberTypesPromotionStrategy.isDecimalNumber(boxedSuperclassCandidate)) { + ConversionFromClassesForDecimals.exists(ClassUtils.isAssignable(boxedGivenClass, _, true)) + } else { + false + } + } + + // See org.springframework.core.convert.support.NumberToNumberConverterFactory + private def handleStrictNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { + val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) + val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) + // We can't check precision here so we need to be loose here + // TODO: Add feature flag: strictNumberPrecisionChecking (default false?) def isFloating(candidate: Class[_]): Boolean = { NumberTypesPromotionStrategy.isFloatingNumber(candidate) || candidate == classOf[java.math.BigDecimal] @@ -87,8 +108,7 @@ object TypeConversionHandler { ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) case candidate if isDecimalNumber(candidate) => - import ImplicitConversionDeterminer.isAssignable - isAssignable(boxedGivenClass, candidate) + SubclassDeterminer.isAssignable(boxedGivenClass, candidate) case _ => false } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala deleted file mode 100644 index 8b51536aac2..00000000000 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminerSpec.scala +++ /dev/null @@ -1,41 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -import org.apache.commons.lang3.ClassUtils -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class CanBeSubclassDeterminerSpec extends AnyFunSuite with Matchers { - - test("Should validate assignability for decimal types") { - ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false - ImplicitConversionDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false - ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false - - ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true - ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true - ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true - } - - test("Should validate assignability for numerical types") { - ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true - ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true - - ImplicitConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true - ImplicitConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true - } - - // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly - test("Should check if lang3 fails for certain isAssignable cases") { - ClassUtils.isAssignable( - classOf[Integer], - classOf[java.lang.Long], - true - ) shouldBe false // should be true in reality, but currently the lib is bugged - ClassUtils.isAssignable( - classOf[java.lang.Short], - classOf[Integer], - true - ) shouldBe false // should be true in reality, but currently the lib is bugged - } - -} diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala new file mode 100644 index 00000000000..be7be9e72f7 --- /dev/null +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala @@ -0,0 +1,41 @@ +package pl.touk.nussknacker.engine.api.typed + +import org.apache.commons.lang3.ClassUtils +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +class SubclassDeterminerSpec extends AnyFunSuite with Matchers { + + test("Should validate assignability for decimal types") { + SubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false + SubclassDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false + SubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false + + SubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true + SubclassDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true + SubclassDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true + } + + test("Should validate assignability for numerical types") { + SubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true + SubclassDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true + + SubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true + SubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true + } + + // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly + test("Should check if lang3 fails for certain isAssignable cases") { + ClassUtils.isAssignable( + classOf[Integer], + classOf[java.lang.Long], + true + ) shouldBe false // should be true in reality, but currently the lib is bugged + ClassUtils.isAssignable( + classOf[java.lang.Short], + classOf[Integer], + true + ) shouldBe false // should be true in reality, but currently the lib is bugged + } + +} diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala index 16749e74938..d43ad4b89b9 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala @@ -63,7 +63,7 @@ class DictApiHttpService( success( dictionaries .filter { case (id, definition) => - definition.valueType(id).canBeImplicitlyConvertedTo(expectedType) + definition.valueType(id).canBeStrictSubclassOf(expectedType) } .map { case (id, _) => DictDto(id, id) } .toList From 3159b401ffaa0ef0f1e3183cd1bd06ce6821c1e9 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Thu, 7 Nov 2024 22:22:10 +0100 Subject: [PATCH 13/26] TypingResultSpec fix --- .../pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala index 9ab5919efad..189698008f7 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala @@ -143,7 +143,7 @@ class TypingResultSpec test("determine if numbers can be converted") { Typed[Int].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe true - Typed[Long].canBeImplicitlyConvertedTo(Typed[Int]) shouldBe false + Typed[Long].canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true Typed[Long].canBeImplicitlyConvertedTo(Typed[Double]) shouldBe true Typed[Double].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe false Typed[java.math.BigDecimal].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe true From 6ab58ca8d9d85b278b693b75cdb6b672fe1804d4 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Mon, 18 Nov 2024 16:57:39 +0100 Subject: [PATCH 14/26] feat: Add StrictConversionDeterminer for type conversion validation --- .../typed/StrictConversionDeterminer.scala | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala new file mode 100644 index 00000000000..ca310a7a792 --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala @@ -0,0 +1,39 @@ +package pl.touk.nussknacker.engine.api.typed + +import cats.data.Validated.condNel +import cats.data.ValidatedNel +import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers +import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypedClass, TypingResult} + +object StrictConversionDeterminer { + + def canBeConvertedTo(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { + ImplicitConversionDeterminer.canBeConvertedTo(givenType, superclassCandidate) + } + + def singleCanBeConvertedTo( + givenType: SingleTypingResult, + superclassCandidate: SingleTypingResult + ): ValidatedNel[String, Unit] = { + val givenClass = givenType.runtimeObjType + val givenSuperclas = superclassCandidate.runtimeObjType + + isStrictSubclass(givenClass, givenSuperclas) + } + + def canBeStrictSubclassOf(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { + this.canBeConvertedTo(givenType, superclassCandidate) + } + + override def isAssignable(from: Class[_], to: Class[_]): Boolean = { + (from, to) match { + case (f, t) if ClassUtils.isAssignable(f, t, true) => true + // Number double check by hand because lang3 can incorrectly throw false when dealing with java types + case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => + AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) + case _ => false + } + } + +} From 0c04b5de65f6cf6bf86f69f0e75b3ff8e9a76601 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Mon, 18 Nov 2024 20:24:36 +0100 Subject: [PATCH 15/26] Determiner remakes for PR --- .../api/typed/ConversionDeterminer.scala | 83 ------------------- .../typed/ImplicitConversionDeterminer.scala | 63 +++++++++++++- .../engine/api/typed/SubclassDeterminer.scala | 35 -------- 3 files changed, 60 insertions(+), 121 deletions(-) delete mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala delete mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala deleted file mode 100644 index b998edab8c3..00000000000 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ConversionDeterminer.scala +++ /dev/null @@ -1,83 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -import cats.data.Validated.condNel -import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.implicits.catsSyntaxValidatedId -import org.apache.commons.lang3.ClassUtils -import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers -import pl.touk.nussknacker.engine.api.typed.typing.{ - SingleTypingResult, - TypedClass, - TypedNull, - TypedObjectWithValue, - TypedUnion, - TypingResult, - Unknown -} - -trait ConversionDeterminer { - - def singleCanBeConvertedTo( - result: typing.SingleTypingResult, - result1: typing.SingleTypingResult - ): ValidatedNel[String, Unit] - - /** - * This method checks if `givenType` can by subclass of `superclassCandidate` - * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` - */ - def canBeConvertedTo(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - (givenType, superclassCandidate) match { - case (_, Unknown) => ().validNel - case (Unknown, _) => ().validNel - case (TypedNull, other) => canNullBeConvertedTo(other) - case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel - case (given: SingleTypingResult, superclass: TypedUnion) => - canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) - case (given: TypedUnion, superclass: SingleTypingResult) => - canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) - case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeConvertedTo(given, superclass) - case (given: TypedUnion, superclass: TypedUnion) => - canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) - } - } - - private def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { - // TODO: Null should not be subclass of typed map that has all values assigned. - case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel - case _ => ().validNel - } - - def canBeConvertedTo( - givenTypes: NonEmptyList[SingleTypingResult], - superclassCandidates: NonEmptyList[SingleTypingResult] - ): ValidatedNel[String, Unit] = { - // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against - // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. - // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here - // "double exists" looks like a good tradeoff - condNel( - givenTypes.exists(given => superclassCandidates.exists(singleCanBeConvertedTo(given, _).isValid)), - (), - s"""None of the following types: - |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} - |can be a subclass of any of: - |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin - ) - } - - def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { - condNel( - givenClass == givenSuperclass, - (), - f"${givenClass.display} and ${givenSuperclass.display} are not the same" - ) orElse - condNel( - isAssignable(givenClass.klass, givenSuperclass.klass), - (), - s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" - ) - } - - def isAssignable(from: Class[_], to: Class[_]): Boolean -} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala index f980620ae77..6d8b0ce7855 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala @@ -1,7 +1,7 @@ package pl.touk.nussknacker.engine.api.typed import cats.data.Validated._ -import cats.data.ValidatedNel +import cats.data.{NonEmptyList, Validated, ValidatedNel} import cats.implicits.{catsSyntaxValidatedId, _} import org.apache.commons.lang3.ClassUtils import pl.touk.nussknacker.engine.api.typed.typing._ @@ -14,12 +14,56 @@ import pl.touk.nussknacker.engine.api.typed.typing._ * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". * WARNING: Evaluation of SpEL expressions fit into this spirit, for other language evaluation engines you need to provide such a compatibility. */ -object ImplicitConversionDeterminer extends ConversionDeterminer { +object ImplicitConversionDeterminer { private val javaMapClass = classOf[java.util.Map[_, _]] private val javaListClass = classOf[java.util.List[_]] private val arrayOfAnyRefClass = classOf[Array[AnyRef]] + /** + * This method checks if `givenType` can by subclass of `superclassCandidate` + * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` + */ + def canBeConvertedTo(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { + (givenType, superclassCandidate) match { + case (_, Unknown) => ().validNel + case (Unknown, _) => ().validNel + case (TypedNull, other) => canNullBeConvertedTo(other) + case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel + case (given: SingleTypingResult, superclass: TypedUnion) => + canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) + case (given: TypedUnion, superclass: SingleTypingResult) => + canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) + case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeConvertedTo(given, superclass) + case (given: TypedUnion, superclass: TypedUnion) => + canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) + } + } + + def canBeConvertedTo( + givenTypes: NonEmptyList[SingleTypingResult], + superclassCandidates: NonEmptyList[SingleTypingResult] + ): ValidatedNel[String, Unit] = { + // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against + // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. + // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here + // "double exists" looks like a good tradeoff + condNel( + givenTypes.exists(given => superclassCandidates.exists(singleCanBeConvertedTo(given, _).isValid)), + (), + s"""None of the following types: + |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} + |can be a subclass of any of: + |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin + ) + } + + private def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { + // TODO: Null should not be subclass of typed map that has all values assigned. + case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel + case _ => ().validNel + } + def singleCanBeConvertedTo( givenType: SingleTypingResult, superclassCandidate: SingleTypingResult @@ -98,6 +142,19 @@ object ImplicitConversionDeterminer extends ConversionDeterminer { (typedObjectRestrictions combine dictRestriction combine taggedValueRestriction combine dataValueRestriction) } + def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { + condNel( + givenClass == givenSuperclass, + (), + f"${givenClass.display} and ${givenSuperclass.display} are not the same" + ) orElse + condNel( + isAssignable(givenClass.klass, givenSuperclass.klass), + (), + s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" + ) + } + private def classCanBeConvertedTo( givenType: SingleTypingResult, superclassCandidate: SingleTypingResult @@ -158,7 +215,7 @@ object ImplicitConversionDeterminer extends ConversionDeterminer { } // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... - override def isAssignable(from: Class[_], to: Class[_]): Boolean = + def isAssignable(from: Class[_], to: Class[_]): Boolean = ClassUtils.isAssignable(from, to, true) } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala deleted file mode 100644 index 4fcf9a0aa01..00000000000 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminer.scala +++ /dev/null @@ -1,35 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -import cats.data.Validated.condNel -import cats.data.ValidatedNel -import org.apache.commons.lang3.ClassUtils -import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers -import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypedClass, TypingResult} - -object SubclassDeterminer extends ConversionDeterminer { - - def singleCanBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: SingleTypingResult - ): ValidatedNel[String, Unit] = { - val givenClass = givenType.runtimeObjType - val givenSuperclas = superclassCandidate.runtimeObjType - - isStrictSubclass(givenClass, givenSuperclas) - } - - def canBeStrictSubclassOf(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - this.canBeConvertedTo(givenType, superclassCandidate) - } - - override def isAssignable(from: Class[_], to: Class[_]): Boolean = { - (from, to) match { - case (f, t) if ClassUtils.isAssignable(f, t, true) => true - // Number double check by hand because lang3 can incorrectly throw false when dealing with java types - case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => - AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) - case _ => false - } - } - -} From 28ccd519e7855115e00af764a97cf865ac838075 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Mon, 18 Nov 2024 20:25:10 +0100 Subject: [PATCH 16/26] rename for pr --- .../engine/api/typed/NumberTypeUtils.scala | 4 +- .../typed/StrictConversionDeterminer.scala | 60 ++++++++- .../api/typed/TypeConversionHandler.scala | 2 +- .../nussknacker/engine/api/typed/typing.scala | 15 ++- .../api/typed/SubclassDeterminerSpec.scala | 20 +-- .../api/typed/TypedFromInstanceTest.scala | 10 +- .../engine/api/typed/TypingResultSpec.scala | 126 +++++++++--------- .../ui/api/DictApiHttpService.scala | 2 +- .../aggregate/AggregatesSpec.scala | 4 +- .../transformer/aggregate/aggregates.scala | 8 +- .../util/transformer/ForEachTransformer.scala | 2 +- .../table/sink/TableTypeOutputValidator.scala | 2 +- .../utils/ToTableTypeSchemaBasedEncoder.scala | 9 +- .../global/DocumentationFunctions.scala | 4 +- .../sample/global/ExampleFunctions.scala | 2 +- .../lite/components/ForEachTransformer.scala | 2 +- .../FragmentParameterValidator.scala | 2 +- .../clazz/ClassDefinitionExtractor.scala | 4 +- .../definition/clazz/MethodDefinition.scala | 10 +- .../clazz/MethodTypeInfoSubclassChecker.scala | 6 +- .../DictKeyWithLabelExpressionParser.scala | 6 +- .../engine/spel/SpelExpressionSuggester.scala | 4 +- .../engine/spel/SpelExpressionValidator.scala | 2 +- .../touk/nussknacker/engine/spel/Typer.scala | 36 +++-- .../spel/typer/MethodReferenceTyper.scala | 2 +- .../engine/util/functions/collection.scala | 4 +- .../engine/util/functions/numeric.scala | 2 +- .../encode/JsonSchemaOutputValidator.scala | 2 +- .../encode/AvroSchemaOutputValidator.scala | 2 +- 29 files changed, 201 insertions(+), 153 deletions(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala index a9cb2846918..647f169c393 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/NumberTypeUtils.scala @@ -15,9 +15,9 @@ object NumberTypeUtils { else if (typ == Typed[java.lang.Double]) java.lang.Double.valueOf(0) else if (typ == Typed[java.math.BigDecimal]) java.math.BigDecimal.ZERO // in case of some unions - else if (typ.canBeImplicitlyConvertedTo(Typed[java.lang.Integer])) java.lang.Integer.valueOf(0) + else if (typ.canBeConvertedTo(Typed[java.lang.Integer])) java.lang.Integer.valueOf(0) // double is quite safe - it can be converted to any Number - else if (typ.canBeImplicitlyConvertedTo(Typed[Number])) java.lang.Double.valueOf(0) + else if (typ.canBeConvertedTo(Typed[Number])) java.lang.Double.valueOf(0) else throw new IllegalArgumentException(s"Not expected type: ${typ.display}, should be Number") } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala index ca310a7a792..63573cece27 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala @@ -1,18 +1,49 @@ package pl.touk.nussknacker.engine.api.typed import cats.data.Validated.condNel -import cats.data.ValidatedNel +import cats.data.{NonEmptyList, Validated, ValidatedNel} +import cats.implicits.catsSyntaxValidatedId import org.apache.commons.lang3.ClassUtils import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers -import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypedClass, TypingResult} +import pl.touk.nussknacker.engine.api.typed.typing._ object StrictConversionDeterminer { def canBeConvertedTo(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - ImplicitConversionDeterminer.canBeConvertedTo(givenType, superclassCandidate) + (givenType, superclassCandidate) match { + case (_, Unknown) => ().validNel + case (Unknown, _) => ().validNel + case (TypedNull, other) => canNullBeConvertedTo(other) + case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel + case (given: SingleTypingResult, superclass: TypedUnion) => + canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) + case (given: TypedUnion, superclass: SingleTypingResult) => + canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) + case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeConvertedTo(given, superclass) + case (given: TypedUnion, superclass: TypedUnion) => + canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) + } + } + + def canBeConvertedTo( + givenTypes: NonEmptyList[SingleTypingResult], + superclassCandidates: NonEmptyList[SingleTypingResult] + ): ValidatedNel[String, Unit] = { + // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against + // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. + // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here + // "double exists" looks like a good tradeoff + condNel( + givenTypes.exists(given => superclassCandidates.exists(singleCanBeConvertedTo(given, _).isValid)), + (), + s"""None of the following types: + |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} + |can be a subclass of any of: + |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin + ) } - def singleCanBeConvertedTo( + def singleCanBeConvertedTo( givenType: SingleTypingResult, superclassCandidate: SingleTypingResult ): ValidatedNel[String, Unit] = { @@ -22,11 +53,26 @@ object StrictConversionDeterminer { isStrictSubclass(givenClass, givenSuperclas) } - def canBeStrictSubclassOf(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - this.canBeConvertedTo(givenType, superclassCandidate) + private def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { + // TODO: Null should not be subclass of typed map that has all values assigned. + case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel + case _ => ().validNel + } + + def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { + condNel( + givenClass == givenSuperclass, + (), + f"${givenClass.display} and ${givenSuperclass.display} are not the same" + ) orElse + condNel( + isAssignable(givenClass.klass, givenSuperclass.klass), + (), + s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" + ) } - override def isAssignable(from: Class[_], to: Class[_]): Boolean = { + def isAssignable(from: Class[_], to: Class[_]): Boolean = { (from, to) match { case (f, t) if ClassUtils.isAssignable(f, t, true) => true // Number double check by hand because lang3 can incorrectly throw false when dealing with java types diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index 8312ff4ee15..4fbe1ce0724 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -108,7 +108,7 @@ object TypeConversionHandler { ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) case candidate if isDecimalNumber(candidate) => - SubclassDeterminer.isAssignable(boxedGivenClass, candidate) + StrictConversionDeterminer.isAssignable(boxedGivenClass, candidate) case _ => false } diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala index c2be6cdcf72..2ccf863a86a 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala @@ -27,11 +27,18 @@ object typing { // TODO: Rename to Typed, maybe NuType? sealed trait TypingResult { - final def canBeImplicitlyConvertedTo(typingResult: TypingResult): Boolean = + /** + * Checks if there exists a conversion to a given typingResult, with possible loss of precision, e.g. long to int. + * If you need to retain conversion precision, use canBeStrictlyConvertedTo + */ + final def canBeConvertedTo(typingResult: TypingResult): Boolean = ImplicitConversionDeterminer.canBeConvertedTo(this, typingResult).isValid - def canBeStrictSubclassOf(typingResult: TypingResult): Boolean = - SubclassDeterminer.canBeStrictSubclassOf(this, typingResult).isValid + /** + * Checks if the conversion to a given typingResult can be made without loss of precision + */ + final def canBeStrictlyConvertedTo(typingResult: TypingResult): Boolean = + StrictConversionDeterminer.canBeConvertedTo(this, typingResult).isValid def valueOpt: Option[Any] @@ -463,7 +470,7 @@ object typing { def unapply(typingResult: TypingResult): Option[TypingResultTypedValue[T]] = { Option(typingResult) - .filter(_.canBeImplicitlyConvertedTo(Typed.fromDetailedType[T])) + .filter(_.canBeConvertedTo(Typed.fromDetailedType[T])) .map(new TypingResultTypedValue(_)) } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala index be7be9e72f7..bcdc5a24bfe 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala @@ -7,21 +7,21 @@ import org.scalatest.matchers.should.Matchers class SubclassDeterminerSpec extends AnyFunSuite with Matchers { test("Should validate assignability for decimal types") { - SubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false - SubclassDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false - SubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false + StrictConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false + StrictConversionDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false + StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false - SubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true - SubclassDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true - SubclassDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true + StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true + StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true + StrictConversionDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true } test("Should validate assignability for numerical types") { - SubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true - SubclassDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true + StrictConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true + StrictConversionDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true - SubclassDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true - SubclassDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true + StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true + StrictConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true } // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala index f3e50d76e35..b1d98cd537d 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala @@ -60,20 +60,20 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w } test("should type empty list") { - Typed.fromInstance(Nil).canBeImplicitlyConvertedTo(Typed(classOf[List[_]])) shouldBe true - Typed.fromInstance(Nil.asJava).canBeImplicitlyConvertedTo(Typed(classOf[java.util.List[_]])) shouldBe true + Typed.fromInstance(Nil).canBeConvertedTo(Typed(classOf[List[_]])) shouldBe true + Typed.fromInstance(Nil.asJava).canBeConvertedTo(Typed(classOf[java.util.List[_]])) shouldBe true } test("should type lists and return union of types coming from all elements") { def checkTypingResult(obj: Any, klass: Class[_], paramTypingResult: TypingResult): Unit = { val typingResult = Typed.fromInstance(obj) - typingResult.canBeImplicitlyConvertedTo(Typed(klass)) shouldBe true + typingResult.canBeConvertedTo(Typed(klass)) shouldBe true typingResult.withoutValue .asInstanceOf[TypedClass] .params .loneElement - .canBeImplicitlyConvertedTo(paramTypingResult) shouldBe true + .canBeConvertedTo(paramTypingResult) shouldBe true } def checkNotASubclassOfOtherParamTypingResult(obj: Any, otherParamTypingResult: TypingResult): Unit = { @@ -82,7 +82,7 @@ class TypedFromInstanceTest extends AnyFunSuite with Matchers with LoneElement w .asInstanceOf[TypedClass] .params .loneElement - .canBeImplicitlyConvertedTo(otherParamTypingResult) shouldBe false + .canBeConvertedTo(otherParamTypingResult) shouldBe false } val listOfSimpleObjects = List[Any](1.1, 2) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala index 189698008f7..1581edf488c 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultSpec.scala @@ -32,33 +32,33 @@ class TypingResultSpec test("determine if can be subclass for typed object") { - typeMap("field1" -> Typed[String], "field2" -> Typed[Int]).canBeImplicitlyConvertedTo( + typeMap("field1" -> Typed[String], "field2" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe true - typeMap("field1" -> Typed[String]).canBeImplicitlyConvertedTo( + typeMap("field1" -> Typed[String]).canBeConvertedTo( typeMap("field1" -> Typed[String], "field2" -> Typed[Int]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeImplicitlyConvertedTo( + typeMap("field1" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeImplicitlyConvertedTo( + typeMap("field1" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[Number]) ) shouldBe true - typeMap("field1" -> list(typeMap("field2" -> Typed[String], "field3" -> Typed[Int]))).canBeImplicitlyConvertedTo( + typeMap("field1" -> list(typeMap("field2" -> Typed[String], "field3" -> Typed[Int]))).canBeConvertedTo( typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe true - typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))).canBeImplicitlyConvertedTo( + typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))).canBeConvertedTo( typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe false - typeMap("field1" -> Typed[String]).canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) shouldBe true + typeMap("field1" -> Typed[String]).canBeConvertedTo(Typed[java.util.Map[_, _]]) shouldBe true - Typed[java.util.Map[_, _]].canBeImplicitlyConvertedTo(typeMap("field1" -> Typed[String])) shouldBe false + Typed[java.util.Map[_, _]].canBeConvertedTo(typeMap("field1" -> Typed[String])) shouldBe false } test("extract Unknown value type when no super matching supertype found among all fields of Record") { @@ -76,78 +76,78 @@ class TypingResultSpec } test("determine if can be subclass for typed unions") { - Typed(Typed[String], Typed[Int]).canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true - Typed[Int].canBeImplicitlyConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true - Typed(Typed[String], Typed[Int]).canBeImplicitlyConvertedTo(Typed(Typed[Long], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeConvertedTo(Typed(Typed[Long], Typed[Int])) shouldBe true } test("determine if can be subclass for unknown") { - Unknown.canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true - Typed[Int].canBeImplicitlyConvertedTo(Unknown) shouldBe true + Unknown.canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeConvertedTo(Unknown) shouldBe true - Unknown.canBeImplicitlyConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true - Typed(Typed[String], Typed[Int]).canBeImplicitlyConvertedTo(Unknown) shouldBe true + Unknown.canBeConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeConvertedTo(Unknown) shouldBe true - Unknown.canBeImplicitlyConvertedTo(typeMap("field1" -> Typed[String])) shouldBe true - typeMap("field1" -> Typed[String]).canBeImplicitlyConvertedTo(Unknown) shouldBe true + Unknown.canBeConvertedTo(typeMap("field1" -> Typed[String])) shouldBe true + typeMap("field1" -> Typed[String]).canBeConvertedTo(Unknown) shouldBe true } test("determine if can be subclass for class") { Typed .fromDetailedType[java.util.List[BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.List[BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true Typed .fromDetailedType[java.util.List[Number]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, Number]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, Number]]) shouldBe true Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[Number, Number]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[Number, Number]]) shouldBe false Typed .fromDetailedType[java.util.Map[Number, Number]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[Number, BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, Number]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe false Typed .fromDetailedType[java.util.Map[BigDecimal, BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[_, BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[_, BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.Map[_, BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[BigDecimal, BigDecimal]]) shouldBe true // For arrays it might be tricky Typed .fromDetailedType[Array[BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe true Typed .fromDetailedType[Array[BigDecimal]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[Array[Number]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[Array[Number]]) shouldBe true Typed .fromDetailedType[Array[Number]] - .canBeImplicitlyConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe false + .canBeConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe false } test("determine if numbers can be converted") { - Typed[Int].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe true - Typed[Long].canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true - Typed[Long].canBeImplicitlyConvertedTo(Typed[Double]) shouldBe true - Typed[Double].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe false - Typed[java.math.BigDecimal].canBeImplicitlyConvertedTo(Typed[Long]) shouldBe true - Typed[Long].canBeImplicitlyConvertedTo(Typed[java.math.BigDecimal]) shouldBe true + Typed[Int].canBeConvertedTo(Typed[Long]) shouldBe true + Typed[Long].canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Long].canBeConvertedTo(Typed[Double]) shouldBe true + Typed[Double].canBeConvertedTo(Typed[Long]) shouldBe false + Typed[java.math.BigDecimal].canBeConvertedTo(Typed[Long]) shouldBe true + Typed[Long].canBeConvertedTo(Typed[java.math.BigDecimal]) shouldBe true } test("find common supertype for simple types") { @@ -303,22 +303,22 @@ class TypingResultSpec test("determine if can be subclass for tagged value") { Typed .tagged(Typed.typedClass[String], "tag1") - .canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe true + .canBeConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe true Typed .tagged(Typed.typedClass[String], "tag1") - .canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[String], "tag2")) shouldBe false + .canBeConvertedTo(Typed.tagged(Typed.typedClass[String], "tag2")) shouldBe false Typed .tagged(Typed.typedClass[String], "tag1") - .canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[Integer], "tag1")) shouldBe false - Typed.tagged(Typed.typedClass[String], "tag1").canBeImplicitlyConvertedTo(Typed.typedClass[String]) shouldBe true - Typed.typedClass[String].canBeImplicitlyConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe false + .canBeConvertedTo(Typed.tagged(Typed.typedClass[Integer], "tag1")) shouldBe false + Typed.tagged(Typed.typedClass[String], "tag1").canBeConvertedTo(Typed.typedClass[String]) shouldBe true + Typed.typedClass[String].canBeConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe false } test("determine if can be subclass for null") { - TypedNull.canBeImplicitlyConvertedTo(Typed[Int]) shouldBe true - TypedNull.canBeImplicitlyConvertedTo(Typed.fromInstance(4)) shouldBe false - TypedNull.canBeImplicitlyConvertedTo(TypedNull) shouldBe true - Typed[String].canBeImplicitlyConvertedTo(TypedNull) shouldBe false + TypedNull.canBeConvertedTo(Typed[Int]) shouldBe true + TypedNull.canBeConvertedTo(Typed.fromInstance(4)) shouldBe false + TypedNull.canBeConvertedTo(TypedNull) shouldBe true + Typed[String].canBeConvertedTo(TypedNull) shouldBe false } test("should deeply extract typ parameters") { @@ -337,20 +337,20 @@ class TypingResultSpec } test("determine if can be subclass for object with value") { - Typed.fromInstance(45).canBeImplicitlyConvertedTo(Typed.typedClass[Long]) shouldBe true - Typed.fromInstance(29).canBeImplicitlyConvertedTo(Typed.typedClass[String]) shouldBe false - Typed.fromInstance(78).canBeImplicitlyConvertedTo(Typed.fromInstance(78)) shouldBe true - Typed.fromInstance(12).canBeImplicitlyConvertedTo(Typed.fromInstance(15)) shouldBe false - Typed.fromInstance(41).canBeImplicitlyConvertedTo(Typed.fromInstance("t")) shouldBe false - Typed.typedClass[String].canBeImplicitlyConvertedTo(Typed.fromInstance("t")) shouldBe true + Typed.fromInstance(45).canBeConvertedTo(Typed.typedClass[Long]) shouldBe true + Typed.fromInstance(29).canBeConvertedTo(Typed.typedClass[String]) shouldBe false + Typed.fromInstance(78).canBeConvertedTo(Typed.fromInstance(78)) shouldBe true + Typed.fromInstance(12).canBeConvertedTo(Typed.fromInstance(15)) shouldBe false + Typed.fromInstance(41).canBeConvertedTo(Typed.fromInstance("t")) shouldBe false + Typed.typedClass[String].canBeConvertedTo(Typed.fromInstance("t")) shouldBe true } test("determine if can be subclass for object with value - use conversion") { - Typed.fromInstance("2007-12-03").canBeImplicitlyConvertedTo(Typed.typedClass[LocalDate]) shouldBe true - Typed.fromInstance("2007-12-03T10:15:30").canBeImplicitlyConvertedTo(Typed.typedClass[LocalDateTime]) shouldBe true + Typed.fromInstance("2007-12-03").canBeConvertedTo(Typed.typedClass[LocalDate]) shouldBe true + Typed.fromInstance("2007-12-03T10:15:30").canBeConvertedTo(Typed.typedClass[LocalDateTime]) shouldBe true - Typed.fromInstance("2007-12-03-qwerty").canBeImplicitlyConvertedTo(Typed.typedClass[LocalDate]) shouldBe false - Typed.fromInstance("2007-12-03").canBeImplicitlyConvertedTo(Typed.typedClass[Currency]) shouldBe false + Typed.fromInstance("2007-12-03-qwerty").canBeConvertedTo(Typed.typedClass[LocalDate]) shouldBe false + Typed.fromInstance("2007-12-03").canBeConvertedTo(Typed.typedClass[Currency]) shouldBe false } test("determinate if can be superclass for objects with value") { @@ -449,7 +449,7 @@ class TypingResultSpec logger.trace(s"Checking: ${input.display}") withClue(s"Input: ${input.display};") { - input.canBeImplicitlyConvertedTo(input) shouldBe true + input.canBeConvertedTo(input) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(input, input) withClue(s"Supertype: ${superType.display};") { superType shouldEqual input @@ -463,11 +463,11 @@ class TypingResultSpec logger.trace(s"Checking: ${input.display}") withClue(s"Input: ${input.display};") { - input.canBeImplicitlyConvertedTo(input) shouldBe true + input.canBeConvertedTo(input) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(input, input) withClue(s"Supertype: ${superType.display};") { // We generate combinations of types co we can only check if input type is a subclass of super type - input.canBeImplicitlyConvertedTo(superType) + input.canBeConvertedTo(superType) } } } @@ -483,12 +483,12 @@ class TypingResultSpec logger.trace(s"Checking supertype of: ${first.display} and ${second.display}") withClue(s"Input: ${first.display}; ${second.display};") { - first.canBeImplicitlyConvertedTo(first) shouldBe true - second.canBeImplicitlyConvertedTo(second) shouldBe true + first.canBeConvertedTo(first) shouldBe true + second.canBeConvertedTo(second) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(first, second) withClue(s"Supertype: ${superType.display};") { - first.canBeImplicitlyConvertedTo(superType) - second.canBeImplicitlyConvertedTo(superType) + first.canBeConvertedTo(superType) + second.canBeConvertedTo(superType) } } } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala index d43ad4b89b9..8a832701199 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/api/DictApiHttpService.scala @@ -63,7 +63,7 @@ class DictApiHttpService( success( dictionaries .filter { case (id, definition) => - definition.valueType(id).canBeStrictSubclassOf(expectedType) + definition.valueType(id).canBeStrictlyConvertedTo(expectedType) } .map { case (id, _) => DictDto(id, id) } .toList diff --git a/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala b/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala index 12350236313..1a8a5e13686 100644 --- a/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala +++ b/engine/flink/components/base-tests/src/test/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/AggregatesSpec.scala @@ -74,8 +74,8 @@ class AggregatesSpec extends AnyFunSuite with TableDrivenPropertyChecks with Mat private def shouldBeInstanceOf(obj: Any, typ: TypingResult): Unit = { val typeFromInstance = Typed.fromInstance(obj) - val canBeSubclassCase = typeFromInstance.canBeImplicitlyConvertedTo(typ) - val typedObjectCase = typ.isInstanceOf[TypedObjectTypingResult] && typeFromInstance.canBeImplicitlyConvertedTo( + val canBeSubclassCase = typeFromInstance.canBeConvertedTo(typ) + val typedObjectCase = typ.isInstanceOf[TypedObjectTypingResult] && typeFromInstance.canBeConvertedTo( typ.asInstanceOf[TypedObjectTypingResult].runtimeObjType ) (canBeSubclassCase || typedObjectCase) shouldBe true diff --git a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala index a9f883d1e6e..09d79139607 100644 --- a/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala +++ b/engine/flink/components/base-unbounded/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/aggregate/aggregates.scala @@ -190,7 +190,7 @@ object aggregates { override def result(finalAggregate: Aggregate): AnyRef = finalAggregate override def computeOutputType(input: typing.TypingResult): Validated[String, typing.TypingResult] = { - if (input.canBeImplicitlyConvertedTo(Typed[Boolean])) { + if (input.canBeConvertedTo(Typed[Boolean])) { Valid(Typed[Long]) } else { Invalid(s"Invalid aggregate type: ${input.display}, should be: ${Typed[Boolean].display}") @@ -239,7 +239,7 @@ object aggregates { override def computeOutputType(input: typing.TypingResult): Validated[String, typing.TypingResult] = { - if (!input.canBeImplicitlyConvertedTo(Typed[Number])) { + if (!input.canBeConvertedTo(Typed[Number])) { Invalid(s"Invalid aggregate type: ${input.display}, should be: ${Typed[Number].display}") } else { Valid(ForLargeFloatingNumbersOperation.promoteSingle(input)) @@ -353,7 +353,7 @@ object aggregates { ): Validated[String, TypedObjectTypingResult] = { input match { case TypedObjectTypingResult(inputFields, klass, _) - if inputFields.keySet == scalaFields.keySet && klass.canBeImplicitlyConvertedTo( + if inputFields.keySet == scalaFields.keySet && klass.canBeConvertedTo( Typed[java.util.Map[String, _]] ) => val validationRes = scalaFields @@ -439,7 +439,7 @@ object aggregates { trait MathAggregator { self: ReducingAggregator => override def computeOutputType(input: typing.TypingResult): Validated[String, typing.TypingResult] = { - if (input.canBeImplicitlyConvertedTo(Typed[Number])) { + if (input.canBeConvertedTo(Typed[Number])) { // In some cases type can be promoted to other class e.g. Byte is promoted to Int for sum Valid(promotionStrategy.promoteSingle(input)) } else { diff --git a/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala b/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala index 951be76ce26..e5f961a79df 100644 --- a/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala +++ b/engine/flink/components/base/src/main/scala/pl/touk/nussknacker/engine/flink/util/transformer/ForEachTransformer.scala @@ -45,7 +45,7 @@ object ForEachTransformer extends CustomStreamTransformer with Serializable { private def returnType(elements: LazyParameter[util.Collection[AnyRef]]): typing.TypingResult = elements.returnType match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeImplicitlyConvertedTo( + if tc.runtimeObjType.canBeConvertedTo( Typed[util.Collection[_]] ) && tc.runtimeObjType.params.nonEmpty => tc.runtimeObjType.params.head diff --git a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala index 05f6b8cda9e..5b22fca0343 100644 --- a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala +++ b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/sink/TableTypeOutputValidator.scala @@ -14,7 +14,7 @@ object TableTypeOutputValidator { val aligned = ToTableTypeSchemaBasedEncoder.alignTypingResult(actualType, expectedType) val expectedTypingResult = expectedType.toTypingResult - if (aligned.canBeImplicitlyConvertedTo(expectedTypingResult)) { + if (aligned.canBeConvertedTo(expectedTypingResult)) { Valid(()) } else { invalidNel( diff --git a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala index 27ae00600c8..d0a584b49f4 100644 --- a/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala +++ b/engine/flink/components/table/src/main/scala/pl/touk/nussknacker/engine/flink/table/utils/ToTableTypeSchemaBasedEncoder.scala @@ -26,8 +26,7 @@ object ToTableTypeSchemaBasedEncoder { case (null, _) => null // We don't know what is the precise of decimal so we have to assume that it will fit the target type to not block the user - case (number: Number, _) - if Typed.typedClass(number.getClass).canBeImplicitlyConvertedTo(targetType.toTypingResult) => + case (number: Number, _) if Typed.typedClass(number.getClass).canBeConvertedTo(targetType.toTypingResult) => NumberUtils .convertNumberToTargetClass[Number](number, targetType.getDefaultConversion.asInstanceOf[Class[Number]]) case (_, rowType: RowType) => @@ -84,7 +83,7 @@ object ToTableTypeSchemaBasedEncoder { // We don't know what is the precision of decimal so we have to assume that it will fit the target type to not block the user case (typ: SingleTypingResult, _) if typ - .canBeImplicitlyConvertedTo(Typed[Number]) && typ.canBeImplicitlyConvertedTo(targetType.toTypingResult) => + .canBeConvertedTo(Typed[Number]) && typ.canBeConvertedTo(targetType.toTypingResult) => targetType.toTypingResult case (recordType: TypedObjectTypingResult, rowType: RowType) if Set[Class[_]](javaMapClass, rowClass).contains(recordType.runtimeObjType.klass) => @@ -106,10 +105,10 @@ object ToTableTypeSchemaBasedEncoder { case ( TypedObjectTypingResult(_, TypedClass(`javaMapClass`, keyType :: valueType :: Nil), _), multisetType: MultisetType - ) if valueType.canBeImplicitlyConvertedTo(Typed[Int]) => + ) if valueType.canBeConvertedTo(Typed[Int]) => alignMultisetType(keyType, multisetType) case (TypedClass(`javaMapClass`, keyType :: valueType :: Nil), multisetType: MultisetType) - if valueType.canBeImplicitlyConvertedTo(Typed[Int]) => + if valueType.canBeConvertedTo(Typed[Int]) => alignMultisetType(keyType, multisetType) case (TypedClass(`arrayClass`, elementType :: Nil), arrayType: ArrayType) => Typed.genericTypeClass(arrayClass, List(alignTypingResult(elementType, arrayType.getElementType))) diff --git a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala index bcbc358fc7c..768f8f202a4 100644 --- a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala +++ b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/DocumentationFunctions.scala @@ -62,7 +62,7 @@ object DocumentationFunctions { (left.withoutValue, right.withoutValue) match { case (`intType`, `intType`) => intType.validNel case (`doubleType`, `doubleType`) => doubleType.validNel - case (l, r) if List(l, r).forall(_.canBeImplicitlyConvertedTo(numberType)) => + case (l, r) if List(l, r).forall(_.canBeConvertedTo(numberType)) => OtherError(s"Addition of ${l.display} and ${r.display} is not supported").invalidNel case (`stringType`, `stringType`) => stringType.validNel case _ => ArgumentTypeError.invalidNel @@ -110,7 +110,7 @@ object DocumentationFunctions { case Some(v) => v.validNel case None => OtherError("No field with given name").invalidNel } - case TypedObjectTypingResult(_, _, _) :: x :: Nil if x.canBeImplicitlyConvertedTo(stringType) => + case TypedObjectTypingResult(_, _, _) :: x :: Nil if x.canBeConvertedTo(stringType) => OtherError("Expected string with known value").invalidNel case _ => ArgumentTypeError.invalidNel diff --git a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala index 723c0d2257b..8c998464dd1 100644 --- a/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala +++ b/engine/flink/management/dev-model/src/main/scala/pl/touk/nussknacker/engine/management/sample/global/ExampleFunctions.scala @@ -147,7 +147,7 @@ object ExampleFunctions { override def computeResultType( arguments: List[TypingResult] ): ValidatedNel[GenericFunctionTypingError, TypingResult] = { - if (arguments.exists(!_.canBeImplicitlyConvertedTo(Typed[Number]))) return ArgumentTypeError.invalidNel + if (arguments.exists(!_.canBeConvertedTo(Typed[Number]))) return ArgumentTypeError.invalidNel arguments match { case t :: Nil => t.validNel case l :: r :: Nil => Typed.record(Map("left" -> l, "right" -> r)).validNel diff --git a/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala b/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala index a2f82110e22..3b8a47732dd 100644 --- a/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala +++ b/engine/lite/components/base/src/main/scala/pl/touk/nussknacker/engine/lite/components/ForEachTransformer.scala @@ -42,7 +42,7 @@ class ForEachTransformerComponent(elements: LazyParameter[java.util.Collection[A override def returnType: typing.TypingResult = { elements.returnType match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeImplicitlyConvertedTo( + if tc.runtimeObjType.canBeConvertedTo( Typed[java.util.Collection[_]] ) && tc.runtimeObjType.params.nonEmpty => tc.runtimeObjType.params.head diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala index 496b371e14a..f60b879a671 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/compile/nodecompilation/FragmentParameterValidator.scala @@ -169,7 +169,7 @@ case class FragmentParameterValidator(classDefinitions: Set[ClassDefinition] = S val dictValueType = dictDefinition.valueType(dictId) - if (dictValueType.canBeImplicitlyConvertedTo(fragmentParameterTypingResult)) { + if (dictValueType.canBeConvertedTo(fragmentParameterTypingResult)) { Valid(()) } else { invalidNel( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala index d86ef54a602..865134301ee 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/ClassDefinitionExtractor.scala @@ -141,7 +141,7 @@ class ClassDefinitionExtractor(settings: ClassExtractionSettings) extends LazyLo methodsForParams .find { case (_, method) => - methodsForParams.forall(mi => method.signature.result.canBeImplicitlyConvertedTo(mi._2.signature.result)) + methodsForParams.forall(mi => method.signature.result.canBeConvertedTo(mi._2.signature.result)) } .getOrElse(methodsForParams.minBy(_._2.signature.result.display)) } @@ -273,7 +273,7 @@ class ClassDefinitionExtractor(settings: ClassExtractionSettings) extends LazyLo ) reflectionBasedDefinition.result } - if (returnedResultType.canBeImplicitlyConvertedTo(returnedResultType)) { + if (returnedResultType.canBeConvertedTo(returnedResultType)) { returnedResultType } else { logger.warn( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala index 452d5b64258..beac88237fb 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodDefinition.scala @@ -35,15 +35,15 @@ sealed trait MethodDefinition { // Allow pass array as List argument because of array to list auto conversion: // pl.touk.nussknacker.engine.spel.internal.ArrayToListConverter case (tc @ TypedClass(klass, _), Parameter(_, y)) if klass.isArray => - tc.canBeImplicitlyConvertedTo(y) || Typed + tc.canBeConvertedTo(y) || Typed .genericTypeClass[java.util.List[_]](tc.params) - .canBeImplicitlyConvertedTo(y) - case (x, Parameter(_, y)) => x.canBeImplicitlyConvertedTo(y) + .canBeConvertedTo(y) + case (x, Parameter(_, y)) => x.canBeConvertedTo(y) } val checkVarArgs = methodTypeInfo.varArg match { case Some(Parameter(_, t)) => - arguments.drop(methodTypeInfo.noVarArgs.length).forall(_.canBeImplicitlyConvertedTo(t)) + arguments.drop(methodTypeInfo.noVarArgs.length).forall(_.canBeConvertedTo(t)) case None => arguments.length == methodTypeInfo.noVarArgs.length } @@ -96,7 +96,7 @@ case class FunctionalMethodDefinition( val typeCalculated = typeFunction(methodInvocationTarget, arguments).leftMap(_.map(errorConverter.convert)) typeCalculated.map { calculated => - if (!typesFromStaticMethodInfo.exists(calculated.canBeImplicitlyConvertedTo)) { + if (!typesFromStaticMethodInfo.exists(calculated.canBeConvertedTo)) { val expectedTypesString = typesFromStaticMethodInfo.map(_.display).mkString("(", ", ", ")") val argumentsString = arguments.map(_.display).mkString("(", ", ", ")") throw new AssertionError( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala index 8a148911c10..a2a6f417800 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/definition/clazz/MethodTypeInfoSubclassChecker.scala @@ -12,7 +12,7 @@ object MethodTypeInfoSubclassChecker { val MethodTypeInfo(superclassNoVarArg, superclassVarArgOption, superclassResult) = superclassInfo val validatedVarArgs = (subclassVarArgOption, superclassVarArgOption) match { - case (Some(sub), Some(sup)) if sub.refClazz.canBeImplicitlyConvertedTo(sup.refClazz) => ().validNel + case (Some(sub), Some(sup)) if sub.refClazz.canBeConvertedTo(sup.refClazz) => ().validNel case (Some(sub), Some(sup)) => NotSubclassVarArgument(sub.refClazz, sup.refClazz).invalidNel case (Some(_), None) => BadVarArg.invalidNel case (None, Some(_)) => ().validNel @@ -38,7 +38,7 @@ object MethodTypeInfoSubclassChecker { ) val validatedNoVarArgs = zippedParameters.zipWithIndex .map { - case ((sub, sup), _) if sub.refClazz.canBeImplicitlyConvertedTo(sup.refClazz) => ().validNel + case ((sub, sup), _) if sub.refClazz.canBeConvertedTo(sup.refClazz) => ().validNel case ((sub, sup), i) => NotSubclassArgument(i + 1, sub.refClazz, sup.refClazz).invalidNel } .sequence @@ -46,7 +46,7 @@ object MethodTypeInfoSubclassChecker { val validatedResult = Validated.condNel( - subclassResult.canBeImplicitlyConvertedTo(superclassResult), + subclassResult.canBeConvertedTo(superclassResult), (), NotSubclassResult(subclassResult, superclassResult) ) diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala index fffcc4d138e..ee07e37e8a1 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala @@ -85,11 +85,11 @@ object DictKeyWithLabelExpressionParser extends ExpressionParser { override def language: Language = languageId override def evaluate[T](ctx: Context, globals: Map[String, Any]): T = { - if (expectedType.canBeImplicitlyConvertedTo(Typed[Long])) { + if (expectedType.canBeConvertedTo(Typed[Long])) { key.toLong.asInstanceOf[T] - } else if (expectedType.canBeImplicitlyConvertedTo(Typed[Boolean])) { + } else if (expectedType.canBeConvertedTo(Typed[Boolean])) { key.toBoolean.asInstanceOf[T] - } else if (expectedType.canBeImplicitlyConvertedTo(Typed[String])) { + } else if (expectedType.canBeConvertedTo(Typed[String])) { key.asInstanceOf[T] } else { throw new IllegalStateException( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala index a63782e2b9f..546dbfebac6 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSuggester.scala @@ -411,9 +411,9 @@ class SpelExpressionSuggester( private def determineIterableElementTypingResult(parent: TypingResult): TypingResult = { parent match { - case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Collection[_]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeConvertedTo(Typed[java.util.Collection[_]]) => tc.runtimeObjType.params.headOption.getOrElse(Unknown) - case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeConvertedTo(Typed[java.util.Map[_, _]]) => Typed.record( Map( "key" -> tc.runtimeObjType.params.headOption.getOrElse(Unknown), diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionValidator.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionValidator.scala index 1e708e1a636..07367413d76 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionValidator.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/SpelExpressionValidator.scala @@ -23,7 +23,7 @@ class SpelExpressionValidator(typer: Typer) { Valid(collected) case a if a == Typed[String] && expectedType == Typed[TemplateEvaluationResult] => Valid(collected) - case a if a.canBeSubclassOf(expectedType) => + case a if a.canBeConvertedTo(expectedType) => Valid(collected) case a => Invalid(NonEmptyList.of(ExpressionTypeError(expectedType, a))) diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 1c7aa1abe60..94398a81cd6 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -146,8 +146,8 @@ private[spel] class Typer( def withChildrenOfType[Parts: universe.TypeTag](result: TypingResult) = { val w = valid(result) withTypedChildren { - case list if list.forall(_.canBeImplicitlyConvertedTo(Typed.fromDetailedType[Parts])) => w - case _ => w.tell(List(PartTypeError)) + case list if list.forall(_.canBeConvertedTo(Typed.fromDetailedType[Parts])) => w + case _ => w.tell(List(PartTypeError)) } } @@ -198,7 +198,7 @@ private[spel] class Typer( case (ref: PropertyOrFieldReference) :: Nil => typeFieldNameReferenceOnRecord(ref.getName, record) case _ => typeFieldNameReferenceOnRecord(indexString, record) } - case indexKey :: Nil if indexKey.canBeImplicitlyConvertedTo(Typed[String]) => + case indexKey :: Nil if indexKey.canBeConvertedTo(Typed[String]) => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) case _ :: Nil => indexer.children match { @@ -356,8 +356,7 @@ private[spel] class Typer( case e: OpMinus => withTypedChildren { - case left :: right :: Nil - if left.canBeImplicitlyConvertedTo(Typed[Number]) && right.canBeImplicitlyConvertedTo(Typed[Number]) => + case left :: right :: Nil if left.canBeConvertedTo(Typed[Number]) && right.canBeConvertedTo(Typed[Number]) => val fallback = NumberTypesPromotionStrategy.ForMathOperation.promote(left, right) operationOnTypesValue[Number, Number, Number](left, right, fallback)((n1, n2) => Valid(MathUtils.minus(n1, n2)) @@ -366,7 +365,7 @@ private[spel] class Typer( invalid(OperatorNonNumericError(e.getOperatorName, left)) case left :: right :: Nil => invalid(OperatorMismatchTypeError(e.getOperatorName, left, right)) - case left :: Nil if left.canBeImplicitlyConvertedTo(Typed[Number]) => + case left :: Nil if left.canBeConvertedTo(Typed[Number]) => val resultType = left.withoutValue val result = operationOnTypesValue[Number, Number](left)(MathUtils.negate).getOrElse(resultType) valid(result) @@ -396,20 +395,18 @@ private[spel] class Typer( withTypedChildren { case left :: right :: Nil if left == Unknown || right == Unknown => valid(Unknown) - case left :: right :: Nil - if left.canBeImplicitlyConvertedTo(Typed[String]) || right.canBeImplicitlyConvertedTo(Typed[String]) => + case left :: right :: Nil if left.canBeConvertedTo(Typed[String]) || right.canBeConvertedTo(Typed[String]) => operationOnTypesValue[Any, Any, String](left, right, Typed[String])((l, r) => Valid(l.toString + r.toString) ) - case left :: right :: Nil - if left.canBeImplicitlyConvertedTo(Typed[Number]) && right.canBeImplicitlyConvertedTo(Typed[Number]) => + case left :: right :: Nil if left.canBeConvertedTo(Typed[Number]) && right.canBeConvertedTo(Typed[Number]) => val fallback = NumberTypesPromotionStrategy.ForMathOperation.promote(left, right) operationOnTypesValue[Number, Number, Number](left, right, fallback)((n1, n2) => Valid(MathUtils.plus(n1, n2)) ) case left :: right :: Nil => invalid(OperatorMismatchTypeError(e.getOperatorName, left, right)) - case left :: Nil if left.canBeImplicitlyConvertedTo(Typed[Number]) => + case left :: Nil if left.canBeConvertedTo(Typed[Number]) => valid(left) case left :: Nil => invalid(OperatorNonNumericError(e.getOperatorName, left)) @@ -451,7 +448,7 @@ private[spel] class Typer( elementType <- extractIterativeType(iterateType) selectionType = resolveSelectionTypingResult(e, iterateType, elementType) result <- typeChildren(validationContext, node, current.pushOnStack(elementType)) { - case result :: Nil if result.canBeImplicitlyConvertedTo(Typed[Boolean]) => + case result :: Nil if result.canBeConvertedTo(Typed[Boolean]) => valid(selectionType) case other => invalid(IllegalSelectionTypeError(other), selectionType) @@ -462,7 +459,7 @@ private[spel] class Typer( case condition :: onTrue :: onFalse :: Nil => for { _ <- Option(condition) - .filter(_.canBeImplicitlyConvertedTo(Typed[Boolean])) + .filter(_.canBeConvertedTo(Typed[Boolean])) .map(valid) .getOrElse(invalid(TernaryOperatorNotBooleanError(condition))) } yield CommonSupertypeFinder.Default.commonSupertype(onTrue, onFalse) @@ -523,10 +520,10 @@ private[spel] class Typer( // as properly determining it would require evaluating the selection expression for each element (likely working on the AST) parentType match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Collection[_]]) || + if tc.runtimeObjType.canBeConvertedTo(Typed[java.util.Collection[_]]) || tc.runtimeObjType.klass.isArray => tc.withoutValue - case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeConvertedTo(Typed[java.util.Map[_, _]]) => Typed.record(Map.empty) case _ => parentType @@ -578,8 +575,7 @@ private[spel] class Typer( op: Option[(Number, Number) => Validated[ExpressionParseError, Any]] )(implicit numberPromotionStrategy: NumberTypesPromotionStrategy): TypingR[CollectedTypingResult] = { typeChildren(validationContext, node, current) { - case left :: right :: Nil - if left.canBeImplicitlyConvertedTo(Typed[Number]) && right.canBeImplicitlyConvertedTo(Typed[Number]) => + case left :: right :: Nil if left.canBeConvertedTo(Typed[Number]) && right.canBeConvertedTo(Typed[Number]) => val fallback = numberPromotionStrategy.promote(left, right) op .map(operationOnTypesValue[Number, Number, Any](left, right, fallback)(_)) @@ -598,7 +594,7 @@ private[spel] class Typer( current: TypingContext )(op: Number => Any): TypingR[CollectedTypingResult] = { typeChildren(validationContext, node, current) { - case left :: Nil if left.canBeImplicitlyConvertedTo(Typed[Number]) => + case left :: Nil if left.canBeConvertedTo(Typed[Number]) => val result = operationOnTypesValue[Number, Any](left)(op).getOrElse(left.withoutValue) valid(result) case left :: Nil => @@ -694,10 +690,10 @@ private[spel] class Typer( private def extractIterativeType(parent: TypingResult): TypingR[TypingResult] = parent match { case tc: SingleTypingResult - if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Collection[_]]) || + if tc.runtimeObjType.canBeConvertedTo(Typed[java.util.Collection[_]]) || tc.runtimeObjType.klass.isArray => valid(tc.runtimeObjType.params.headOption.getOrElse(Unknown)) - case tc: SingleTypingResult if tc.runtimeObjType.canBeImplicitlyConvertedTo(Typed[java.util.Map[_, _]]) => + case tc: SingleTypingResult if tc.runtimeObjType.canBeConvertedTo(Typed[java.util.Map[_, _]]) => valid( Typed.record( Map( diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala index 9926b337e95..08cb6e00379 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/typer/MethodReferenceTyper.scala @@ -62,7 +62,7 @@ class MethodReferenceTyper(classDefinitionSet: ClassDefinitionSet, methodExecuti )(implicit reference: MethodReference): Either[Option[ExpressionParseError], NonEmptyList[MethodDefinition]] = { def displayableType = clazzDefinitions.map(k => k.clazzName).map(_.display).toList.mkString(", ") - def isClass = clazzDefinitions.map(k => k.clazzName).exists(_.canBeImplicitlyConvertedTo(Typed[Class[_]])) + def isClass = clazzDefinitions.map(k => k.clazzName).exists(_.canBeConvertedTo(Typed[Class[_]])) val clazzMethods = if (reference.isStatic) clazzDefinitions.toList.flatMap(_.staticMethods.get(reference.methodName).toList.flatten) diff --git a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala index 0858fe97465..aaaab391009 100644 --- a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala +++ b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/collection.scala @@ -357,8 +357,8 @@ object CollectionUtils { case _ if firstComponentType.withoutValue == secondComponentType.withoutValue => listType.copy(params = firstComponentType.withoutValue :: Nil) case _ - if firstComponentType.canBeImplicitlyConvertedTo(numberType) && secondComponentType - .canBeImplicitlyConvertedTo(numberType) => + if firstComponentType.canBeConvertedTo(numberType) && secondComponentType + .canBeConvertedTo(numberType) => Typed.genericTypeClass(fClass, List(numberType)) case _ => listType.copy(params = Unknown :: Nil) } diff --git a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala index bc6d1d2a1f5..880a216ed77 100644 --- a/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala +++ b/utils/default-helpers/src/main/scala/pl/touk/nussknacker/engine/util/functions/numeric.scala @@ -129,7 +129,7 @@ object NumericUtils { override def computeResultType( arguments: List[typing.TypingResult] ): ValidatedNel[GenericFunctionTypingError, typing.TypingResult] = { - if (arguments.head.canBeImplicitlyConvertedTo(Typed[Number])) arguments.head.withoutValue.validNel + if (arguments.head.canBeConvertedTo(Typed[Number])) arguments.head.withoutValue.validNel else Typed[Number].validNel } diff --git a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala index 1219aacde4a..d15d678e89a 100644 --- a/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala +++ b/utils/json-utils/src/main/scala/pl/touk/nussknacker/engine/json/encode/JsonSchemaOutputValidator.scala @@ -370,7 +370,7 @@ class JsonSchemaOutputValidator(validationMode: ValidationMode) extends LazyLogg case (TypedClass(_, Nil), TypedClass(_, Nil)) => invalid(typingResult, schema, rootSchema, path) case _ => condNel( - typingResult.canBeImplicitlyConvertedTo(schemaAsTypedResult), + typingResult.canBeConvertedTo(schemaAsTypedResult), (), OutputValidatorTypeError(path, typingResult, JsonSchemaExpected(schema, rootSchema)) ) diff --git a/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala b/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala index 48292b5697e..38106336172 100644 --- a/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala +++ b/utils/schemed-kafka-components-utils/src/main/scala/pl/touk/nussknacker/engine/schemedkafka/encode/AvroSchemaOutputValidator.scala @@ -153,7 +153,7 @@ class AvroSchemaOutputValidator(validationMode: ValidationMode) extends LazyLogg case _ @TypedClass(klass, key :: value :: Nil) if isMap(klass) => // Map keys are assumed to be strings: https://avro.apache.org/docs/current/spec.html#Maps condNel( - key.canBeImplicitlyConvertedTo(Typed.apply[java.lang.String]), + key.canBeConvertedTo(Typed.apply[java.lang.String]), (), typeError(typingResult, schema, path) ).andThen(_ => validateTypingResult(value, schema.getValueType, buildPath("*", path, useIndexer = true))) From 0b8dbc7dbcff601f7e24fbf1a23410bf036ee6c3 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak <137457433+Diamekod0221@users.noreply.github.com> Date: Wed, 20 Nov 2024 18:11:50 +0100 Subject: [PATCH 17/26] Update components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../nussknacker/engine/api/typed/SubclassDeterminerSpec.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala index bcdc5a24bfe..70daa30de37 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala @@ -19,9 +19,7 @@ class SubclassDeterminerSpec extends AnyFunSuite with Matchers { test("Should validate assignability for numerical types") { StrictConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true StrictConversionDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true - StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true - StrictConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true } // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly From 6636dcf5c6d430899f7cb32698186813de2b9827 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Thu, 21 Nov 2024 18:32:50 +0100 Subject: [PATCH 18/26] pr changes - delte test cases --- .../typed/ImplicitConversionDeterminer.scala | 51 +++++++++++++++++++ .../typed/StrictConversionDeterminer.scala | 5 +- .../api/typed/SubclassDeterminerSpec.scala | 30 +---------- docs/MigrationGuide.md | 2 + 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala index 6d8b0ce7855..3c84bb7727d 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala @@ -4,6 +4,13 @@ import cats.data.Validated._ import cats.data.{NonEmptyList, Validated, ValidatedNel} import cats.implicits.{catsSyntaxValidatedId, _} import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.StrictConversionDeterminer.{ + canBeConvertedTo, + canNullBeConvertedTo, + isAssignable, + isStrictSubclass, + singleCanBeConvertedTo +} import pl.touk.nussknacker.engine.api.typed.typing._ /** @@ -40,6 +47,50 @@ object ImplicitConversionDeterminer { } } + def canBeStrictlyConvertedTo( + givenType: TypingResult, + superclassCandidate: TypingResult + ): ValidatedNel[String, Unit] = { + (givenType, superclassCandidate) match { + case (_, Unknown) => ().validNel + case (Unknown, _) => ().validNel + case (TypedNull, other) => canNullBeConvertedTo(other) + case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel + case (given: SingleTypingResult, superclass: TypedUnion) => + canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) + case (given: TypedUnion, superclass: SingleTypingResult) => + canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) + case (given: SingleTypingResult, superclass: SingleTypingResult) => + singleCanBeStrictlyConvertedTo(given, superclass) + case (given: TypedUnion, superclass: TypedUnion) => + canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) + } + + } + + def singleCanBeStrictlyConvertedTo( + givenType: SingleTypingResult, + superclassCandidate: SingleTypingResult + ): ValidatedNel[String, Unit] = { + val givenClass = givenType.runtimeObjType + val givenSuperclas = superclassCandidate.runtimeObjType + + isStrictSubclass(givenClass, givenSuperclas) + } + + def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { + condNel( + givenClass == givenSuperclass, + (), + f"${givenClass.display} and ${givenSuperclass.display} are not the same" + ) orElse + condNel( + isAssignable(givenClass.klass, givenSuperclass.klass), + (), + s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" + ) + } + def canBeConvertedTo( givenTypes: NonEmptyList[SingleTypingResult], superclassCandidates: NonEmptyList[SingleTypingResult] diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala index 63573cece27..a77e7f2227c 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala @@ -72,10 +72,11 @@ object StrictConversionDeterminer { ) } - def isAssignable(from: Class[_], to: Class[_]): Boolean = { + // We double check with a fallback because lang3 only checks strict assignability subtyping. We also want to check + // for possible subtyping, e.g. Int to Long. + private def isAssignable(from: Class[_], to: Class[_]): Boolean = { (from, to) match { case (f, t) if ClassUtils.isAssignable(f, t, true) => true - // Number double check by hand because lang3 can incorrectly throw false when dealing with java types case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) case _ => false diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala index 70daa30de37..44f7fe938da 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala @@ -6,34 +6,8 @@ import org.scalatest.matchers.should.Matchers class SubclassDeterminerSpec extends AnyFunSuite with Matchers { - test("Should validate assignability for decimal types") { - StrictConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Integer]) shouldBe false - StrictConversionDeterminer.isAssignable(classOf[Number], classOf[Integer]) shouldBe false - StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Short]) shouldBe false + test("Should validate assignability for decimal types") {} - StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Long]) shouldBe true - StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[Number]) shouldBe true - StrictConversionDeterminer.isAssignable(classOf[java.lang.Short], classOf[Integer]) shouldBe true - } - - test("Should validate assignability for numerical types") { - StrictConversionDeterminer.isAssignable(classOf[java.lang.Long], classOf[java.lang.Double]) shouldBe true - StrictConversionDeterminer.isAssignable(classOf[java.lang.Float], classOf[Double]) shouldBe true - StrictConversionDeterminer.isAssignable(classOf[Integer], classOf[java.lang.Float]) shouldBe true - } - - // to check if autoboxing lang3 is failing - we can remove our fallback from SubclassDeterminer.isAssignable if the lib works properly - test("Should check if lang3 fails for certain isAssignable cases") { - ClassUtils.isAssignable( - classOf[Integer], - classOf[java.lang.Long], - true - ) shouldBe false // should be true in reality, but currently the lib is bugged - ClassUtils.isAssignable( - classOf[java.lang.Short], - classOf[Integer], - true - ) shouldBe false // should be true in reality, but currently the lib is bugged - } + test("Should validate assignability for numerical types") {} } diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index ebda8ade20e..e9d2e4bf89c 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -625,6 +625,8 @@ To see the biggest differences please consult the [changelog](Changelog.md). * `api/parameters/*/validate` request * `scenarioName` is removed * `processProperties` is removed +* [#7115](https://github.com/TouK/nussknacker/pull/7115) Changes in DictApiEndpoints: + * `DictListRequestDto` `expectedType`: TypingResultInJson -> Json ### Configuration changes * [#4860](https://github.com/TouK/nussknacker/pull/4860) In file-based configuration, the field `scenarioTypes..additionalPropertiesConfig` is renamed to `scenarioTypes..scenarioPropertiesConfig` From 2be5f30c64f8d524245539a3c5d52f196a8026a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20S=C5=82abek?= Date: Mon, 25 Nov 2024 18:49:03 +0100 Subject: [PATCH 19/26] simplify conversion check (#7220) * simplify conversion check * changes to TypeConversionHandler * test fixes --------- Co-authored-by: Marcel Philipiak --- .../api/typed/AssignabilityDeterminer.scala | 249 ++++++++++++++++ .../typed/ImplicitConversionDeterminer.scala | 272 ------------------ .../typed/StrictConversionDeterminer.scala | 86 ------ .../api/typed/TypeConversionHandler.scala | 55 ++-- .../nussknacker/engine/api/typed/typing.scala | 4 +- .../typed/TypingResultErrorMessagesSpec.scala | 23 +- .../ui/process/test/ScenarioTestService.scala | 4 +- 7 files changed, 294 insertions(+), 399 deletions(-) create mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala delete mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala delete mode 100644 components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala new file mode 100644 index 00000000000..05add2e02a9 --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -0,0 +1,249 @@ +package pl.touk.nussknacker.engine.api.typed + +import cats.data.Validated._ +import cats.data.{NonEmptyList, Validated, ValidatedNel} +import cats.implicits.{catsSyntaxValidatedId, _} +import org.apache.commons.lang3.ClassUtils +import pl.touk.nussknacker.engine.api.typed.typing._ + +/** + * This class determine if type can be subclass of other type. It basically based on fact that TypingResults are + * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). + * + * This class, like CommonSupertypeFinder is in spirit of "Be type safety as much as possible, but also provide some helpful + * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". + * WARNING: Evaluation of SpEL expressions fit into this spirit, for other language evaluation engines you need to provide such a compatibility. + */ +object AssignabilityDeterminer { + + private val javaMapClass = classOf[java.util.Map[_, _]] + private val javaListClass = classOf[java.util.List[_]] + private val arrayOfAnyRefClass = classOf[Array[AnyRef]] + + /** + * This method checks if `givenType` can by subclass of `superclassCandidate` + * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` + */ + def isAssignableLoose(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = + isAssignable(from, to, LooseConversionChecker) + + def isAssignableStrict(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = + isAssignable(from, to, StrictConversionChecker) + + private def isAssignable(from: TypingResult, to: TypingResult, conversionChecker: ConversionChecker) = { + (from, to) match { + case (_, Unknown) => ().validNel + case (Unknown, _) => ().validNel + case (TypedNull, other) => isNullAsignableTo(other) + case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel + case (given: SingleTypingResult, superclass: TypedUnion) => + isAnyOfAssignableToAnyOf(NonEmptyList.one(given), superclass.possibleTypes, conversionChecker) + case (given: TypedUnion, superclass: SingleTypingResult) => + isAnyOfAssignableToAnyOf(given.possibleTypes, NonEmptyList.one(superclass), conversionChecker) + case (given: SingleTypingResult, superclass: SingleTypingResult) => + isSingleAssignableToSingle(given, superclass, conversionChecker) + case (given: TypedUnion, superclass: TypedUnion) => + isAnyOfAssignableToAnyOf(given.possibleTypes, superclass.possibleTypes, conversionChecker) + } + } + + private def isNullAsignableTo(to: TypingResult): ValidatedNel[String, Unit] = to match { + // TODO: Null should not be subclass of typed map that has all values assigned. + case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel + case _ => ().validNel + } + + private def isSingleAssignableToSingle( + from: SingleTypingResult, + to: SingleTypingResult, + conversionChecker: ConversionChecker + ): ValidatedNel[String, Unit] = { + val objTypeRestriction = isSingleAssignableToTypedClass(from, to.runtimeObjType, conversionChecker) + val typedObjectRestrictions = (_: Unit) => + to match { + case superclass: TypedObjectTypingResult => + val givenTypeFields = from match { + case given: TypedObjectTypingResult => given.fields + case _ => Map.empty[String, TypingResult] + } + + superclass.fields.toList + .map { case (name, typ) => + givenTypeFields.get(name) match { + case None => + s"Field '$name' is lacking".invalidNel + case Some(givenFieldType) => + condNel( + isAssignable(givenFieldType, typ, conversionChecker).isValid, + (), + s"Field '$name' is of the wrong type. Expected: ${givenFieldType.display}, actual: ${typ.display}" + ) + } + } + .foldLeft(().validNel[String])(_.combine(_)) + case _ => + ().validNel + } + val dictRestriction = (_: Unit) => { + (from, to) match { + case (given: TypedDict, superclass: TypedDict) => + condNel( + given.dictId == superclass.dictId, + (), + "The type and the superclass candidate are Dicts with unequal IDs" + ) + case (_: TypedDict, _) => + "The type is a Dict but the superclass candidate not".invalidNel + case (_, _: TypedDict) => + "The superclass candidate is a Dict but the type not".invalidNel + case _ => + ().validNel + } + } + val taggedValueRestriction = (_: Unit) => { + (from, to) match { + case (givenTaggedValue: TypedTaggedValue, superclassTaggedValue: TypedTaggedValue) => + condNel( + givenTaggedValue.tag == superclassTaggedValue.tag, + (), + s"Tagged values have unequal tags: ${givenTaggedValue.tag} and ${superclassTaggedValue.tag}" + ) + case (_: TypedTaggedValue, _) => ().validNel + case (_, _: TypedTaggedValue) => + s"The type is not a tagged value".invalidNel + case _ => ().validNel + } + } + // Type like Integer can be subclass of Integer{5}, because Integer could + // possibly have value of 5, that would make it subclass of Integer{5}. + // This allows us to supply unknown Integer to function that requires + // Integer{5}. + val dataValueRestriction = (_: Unit) => { + (from, to) match { + case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) + if givenValue == candidateValue => + ().validNel + case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) => + s"Types with value have different values: $givenValue and $candidateValue".invalidNel + case _ => ().validNel + } + } + objTypeRestriction andThen + (typedObjectRestrictions combine dictRestriction combine taggedValueRestriction combine dataValueRestriction) + } + + private def isSingleAssignableToTypedClass( + from: SingleTypingResult, + to: TypedClass, + conversionChecker: ConversionChecker + ): ValidatedNel[String, Unit] = { + def typeParametersMatches(givenClass: TypedClass, superclassCandidate: TypedClass) = { + def canBeSubOrSuperclass(givenClassParam: TypingResult, superclassParam: TypingResult) = + condNel( + isAssignable(givenClassParam, superclassParam, conversionChecker).isValid || + isAssignable(superclassParam, givenClassParam, conversionChecker).isValid, + (), + f"None of ${givenClassParam.display} and ${superclassParam.display} is a subclass of another" + ) + + (givenClass, superclassCandidate) match { + case (TypedClass(_, givenElementParam :: Nil), TypedClass(superclass, superclassParam :: Nil)) + // Array are invariant but we have built-in conversion between array types - this check should be moved outside this class when we move away canBeConvertedTo as well + if javaListClass.isAssignableFrom(superclass) || arrayOfAnyRefClass.isAssignableFrom(superclass) => + isAssignable(givenElementParam, superclassParam, conversionChecker) + case ( + TypedClass(_, givenKeyParam :: givenValueParam :: Nil), + TypedClass(superclass, superclassKeyParam :: superclassValueParam :: Nil) + ) if javaMapClass.isAssignableFrom(superclass) => + // Map's key generic param is invariant. We can't just check givenKeyParam == superclassKeyParam because of Unknown type which is a kind of wildcard + condNel( + isAssignable(givenKeyParam, superclassKeyParam, conversionChecker).isValid && + isAssignable(superclassKeyParam, givenKeyParam, conversionChecker).isValid, + (), + s"Key types of Maps ${givenKeyParam.display} and ${superclassKeyParam.display} are not equals" + ) andThen (_ => isAssignable(givenValueParam, superclassValueParam, conversionChecker)) + case _ => + // for unknown types we are lax - the generic type may be co- contra- or in-variant - and we don't want to + // return validation errors in this case. It's better to accept to much than too little + condNel( + superclassCandidate.params.zip(givenClass.params).forall { case (superclassParam, givenClassParam) => + canBeSubOrSuperclass(givenClassParam, superclassParam).isValid + }, + (), + s"Wrong type parameters" + ) + } + } + val givenClass = from.runtimeObjType + + val equalClassesOrCanAssign = + condNel( + givenClass == to, + (), + f"${givenClass.display} and ${to.display} are not the same" + ) orElse + isAssignable(givenClass.klass, to.klass) + + val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, to)) + canBeSubclass orElse conversionChecker.isConvertable(from, to) + } + + private def isAnyOfAssignableToAnyOf( + from: NonEmptyList[SingleTypingResult], + to: NonEmptyList[SingleTypingResult], + conversionChecker: ConversionChecker + ): ValidatedNel[String, Unit] = { + // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against + // e.g. (String | Int).isAnyOfAssignableToAnyOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. + // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here + // "double exists" looks like a good tradeoff + condNel( + from.exists(given => to.exists(isSingleAssignableToSingle(given, _, conversionChecker).isValid)), + (), + s"""None of the following types: + |${from.map(" - " + _.display).toList.mkString(",\n")} + |can be a subclass of any of: + |${to.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin + ) + } + + // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... + private def isAssignable(from: Class[_], to: Class[_]): ValidatedNel[String, Unit] = + condNel(ClassUtils.isAssignable(from, to, true), (), s"$to is not assignable from $from") + + // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) + private sealed trait ConversionChecker { + + def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] + + } + + private object StrictConversionChecker extends ConversionChecker { + + override def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = + s"${from.runtimeObjType.display} cannot be strictly converted to ${to.display}" + condNel(TypeConversionHandler.canBeStrictlyConvertedTo(from, to), (), errMsgPrefix) + } + + } + + private object LooseConversionChecker extends ConversionChecker { + + override def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = s"${from.runtimeObjType.display} cannot be converted to ${to.display}" + condNel(TypeConversionHandler.canBeLooselyConvertedTo(from, to), (), errMsgPrefix) + } + + } + +} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala deleted file mode 100644 index 3c84bb7727d..00000000000 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/ImplicitConversionDeterminer.scala +++ /dev/null @@ -1,272 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -import cats.data.Validated._ -import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.implicits.{catsSyntaxValidatedId, _} -import org.apache.commons.lang3.ClassUtils -import pl.touk.nussknacker.engine.api.typed.StrictConversionDeterminer.{ - canBeConvertedTo, - canNullBeConvertedTo, - isAssignable, - isStrictSubclass, - singleCanBeConvertedTo -} -import pl.touk.nussknacker.engine.api.typed.typing._ - -/** - * This class determine if type can be subclass of other type. It basically based on fact that TypingResults are - * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). - * - * This class, like CommonSupertypeFinder is in spirit of "Be type safety as much as possible, but also provide some helpful - * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". - * WARNING: Evaluation of SpEL expressions fit into this spirit, for other language evaluation engines you need to provide such a compatibility. - */ -object ImplicitConversionDeterminer { - - private val javaMapClass = classOf[java.util.Map[_, _]] - private val javaListClass = classOf[java.util.List[_]] - private val arrayOfAnyRefClass = classOf[Array[AnyRef]] - - /** - * This method checks if `givenType` can by subclass of `superclassCandidate` - * It will return true if `givenType` is equals to `superclassCandidate` or `givenType` "extends" `superclassCandidate` - */ - def canBeConvertedTo(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - (givenType, superclassCandidate) match { - case (_, Unknown) => ().validNel - case (Unknown, _) => ().validNel - case (TypedNull, other) => canNullBeConvertedTo(other) - case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel - case (given: SingleTypingResult, superclass: TypedUnion) => - canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) - case (given: TypedUnion, superclass: SingleTypingResult) => - canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) - case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeConvertedTo(given, superclass) - case (given: TypedUnion, superclass: TypedUnion) => - canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) - } - } - - def canBeStrictlyConvertedTo( - givenType: TypingResult, - superclassCandidate: TypingResult - ): ValidatedNel[String, Unit] = { - (givenType, superclassCandidate) match { - case (_, Unknown) => ().validNel - case (Unknown, _) => ().validNel - case (TypedNull, other) => canNullBeConvertedTo(other) - case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel - case (given: SingleTypingResult, superclass: TypedUnion) => - canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) - case (given: TypedUnion, superclass: SingleTypingResult) => - canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) - case (given: SingleTypingResult, superclass: SingleTypingResult) => - singleCanBeStrictlyConvertedTo(given, superclass) - case (given: TypedUnion, superclass: TypedUnion) => - canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) - } - - } - - def singleCanBeStrictlyConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: SingleTypingResult - ): ValidatedNel[String, Unit] = { - val givenClass = givenType.runtimeObjType - val givenSuperclas = superclassCandidate.runtimeObjType - - isStrictSubclass(givenClass, givenSuperclas) - } - - def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { - condNel( - givenClass == givenSuperclass, - (), - f"${givenClass.display} and ${givenSuperclass.display} are not the same" - ) orElse - condNel( - isAssignable(givenClass.klass, givenSuperclass.klass), - (), - s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" - ) - } - - def canBeConvertedTo( - givenTypes: NonEmptyList[SingleTypingResult], - superclassCandidates: NonEmptyList[SingleTypingResult] - ): ValidatedNel[String, Unit] = { - // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against - // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. - // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here - // "double exists" looks like a good tradeoff - condNel( - givenTypes.exists(given => superclassCandidates.exists(singleCanBeConvertedTo(given, _).isValid)), - (), - s"""None of the following types: - |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} - |can be a subclass of any of: - |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin - ) - } - - private def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { - // TODO: Null should not be subclass of typed map that has all values assigned. - case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel - case _ => ().validNel - } - - def singleCanBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: SingleTypingResult - ): ValidatedNel[String, Unit] = { - val objTypeRestriction = classCanBeConvertedTo(givenType, superclassCandidate) - val typedObjectRestrictions = (_: Unit) => - superclassCandidate match { - case superclass: TypedObjectTypingResult => - val givenTypeFields = givenType match { - case given: TypedObjectTypingResult => given.fields - case _ => Map.empty[String, TypingResult] - } - - superclass.fields.toList - .map { case (name, typ) => - givenTypeFields.get(name) match { - case None => - s"Field '$name' is lacking".invalidNel - case Some(givenFieldType) => - condNel( - canBeConvertedTo(givenFieldType, typ).isValid, - (), - s"Field '$name' is of the wrong type. Expected: ${givenFieldType.display}, actual: ${typ.display}" - ) - } - } - .foldLeft(().validNel[String])(_.combine(_)) - case _ => - ().validNel - } - val dictRestriction = (_: Unit) => { - (givenType, superclassCandidate) match { - case (given: TypedDict, superclass: TypedDict) => - condNel( - given.dictId == superclass.dictId, - (), - "The type and the superclass candidate are Dicts with unequal IDs" - ) - case (_: TypedDict, _) => - "The type is a Dict but the superclass candidate not".invalidNel - case (_, _: TypedDict) => - "The superclass candidate is a Dict but the type not".invalidNel - case _ => - ().validNel - } - } - val taggedValueRestriction = (_: Unit) => { - (givenType, superclassCandidate) match { - case (givenTaggedValue: TypedTaggedValue, superclassTaggedValue: TypedTaggedValue) => - condNel( - givenTaggedValue.tag == superclassTaggedValue.tag, - (), - s"Tagged values have unequal tags: ${givenTaggedValue.tag} and ${superclassTaggedValue.tag}" - ) - case (_: TypedTaggedValue, _) => ().validNel - case (_, _: TypedTaggedValue) => - s"The type is not a tagged value".invalidNel - case _ => ().validNel - } - } - // Type like Integer can be subclass of Integer{5}, because Integer could - // possibly have value of 5, that would make it subclass of Integer{5}. - // This allows us to supply unknown Integer to function that requires - // Integer{5}. - val dataValueRestriction = (_: Unit) => { - (givenType, superclassCandidate) match { - case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) - if givenValue == candidateValue => - ().validNel - case (TypedObjectWithValue(_, givenValue), TypedObjectWithValue(_, candidateValue)) => - s"Types with value have different values: $givenValue and $candidateValue".invalidNel - case _ => ().validNel - } - } - objTypeRestriction andThen - (typedObjectRestrictions combine dictRestriction combine taggedValueRestriction combine dataValueRestriction) - } - - def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { - condNel( - givenClass == givenSuperclass, - (), - f"${givenClass.display} and ${givenSuperclass.display} are not the same" - ) orElse - condNel( - isAssignable(givenClass.klass, givenSuperclass.klass), - (), - s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" - ) - } - - private def classCanBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: SingleTypingResult - ): ValidatedNel[String, Unit] = { - val givenClass = givenType.runtimeObjType - val givenSuperclass = superclassCandidate.runtimeObjType - - val equalClassesOrCanAssign = isStrictSubclass(givenClass, givenSuperclass) - val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, givenSuperclass)) - canBeSubclass orElse canBeConvertedTo(givenType, givenSuperclass) - } - - // TODO: Conversions should be checked during typing, not during generic usage of TypingResult.canBeSubclassOf(...) - def canBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: TypedClass - ): ValidatedNel[String, Unit] = { - val errMsgPrefix = s"${givenType.runtimeObjType.display} cannot be converted to ${superclassCandidate.display}" - condNel(TypeConversionHandler.canBeConvertedTo(givenType, superclassCandidate), (), errMsgPrefix) - } - - private def typeParametersMatches(givenClass: TypedClass, superclassCandidate: TypedClass) = { - def canBeSubOrSuperclass(givenClassParam: TypingResult, superclassParam: TypingResult) = - condNel( - canBeConvertedTo(givenClassParam, superclassParam).isValid || - canBeConvertedTo(superclassParam, givenClassParam).isValid, - (), - f"None of ${givenClassParam.display} and ${superclassParam.display} is a subclass of another" - ) - - (givenClass, superclassCandidate) match { - case (TypedClass(_, givenElementParam :: Nil), TypedClass(superclass, superclassParam :: Nil)) - // Array are invariant but we have built-in conversion between array types - this check should be moved outside this class when we move away canBeConvertedTo as well - if javaListClass.isAssignableFrom(superclass) || arrayOfAnyRefClass.isAssignableFrom(superclass) => - canBeConvertedTo(givenElementParam, superclassParam) - case ( - TypedClass(_, givenKeyParam :: givenValueParam :: Nil), - TypedClass(superclass, superclassKeyParam :: superclassValueParam :: Nil) - ) if javaMapClass.isAssignableFrom(superclass) => - // Map's key generic param is invariant. We can't just check givenKeyParam == superclassKeyParam because of Unknown type which is a kind of wildcard - condNel( - canBeConvertedTo(givenKeyParam, superclassKeyParam).isValid && - canBeConvertedTo(superclassKeyParam, givenKeyParam).isValid, - (), - s"Key types of Maps ${givenKeyParam.display} and ${superclassKeyParam.display} are not equals" - ) andThen (_ => canBeConvertedTo(givenValueParam, superclassValueParam)) - case _ => - // for unknown types we are lax - the generic type may be co- contra- or in-variant - and we don't want to - // return validation errors in this case. It's better to accept to much than too little - condNel( - superclassCandidate.params.zip(givenClass.params).forall { case (superclassParam, givenClassParam) => - canBeSubOrSuperclass(givenClassParam, superclassParam).isValid - }, - (), - s"Wrong type parameters" - ) - } - } - - // we use explicit autoboxing = true flag, as ClassUtils in commons-lang3:3.3 (used in Flink) cannot handle JDK 11... - def isAssignable(from: Class[_], to: Class[_]): Boolean = - ClassUtils.isAssignable(from, to, true) - -} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala deleted file mode 100644 index a77e7f2227c..00000000000 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/StrictConversionDeterminer.scala +++ /dev/null @@ -1,86 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -import cats.data.Validated.condNel -import cats.data.{NonEmptyList, Validated, ValidatedNel} -import cats.implicits.catsSyntaxValidatedId -import org.apache.commons.lang3.ClassUtils -import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers -import pl.touk.nussknacker.engine.api.typed.typing._ - -object StrictConversionDeterminer { - - def canBeConvertedTo(givenType: TypingResult, superclassCandidate: TypingResult): ValidatedNel[String, Unit] = { - (givenType, superclassCandidate) match { - case (_, Unknown) => ().validNel - case (Unknown, _) => ().validNel - case (TypedNull, other) => canNullBeConvertedTo(other) - case (_, TypedNull) => s"No type can be subclass of ${TypedNull.display}".invalidNel - case (given: SingleTypingResult, superclass: TypedUnion) => - canBeConvertedTo(NonEmptyList.one(given), superclass.possibleTypes) - case (given: TypedUnion, superclass: SingleTypingResult) => - canBeConvertedTo(given.possibleTypes, NonEmptyList.one(superclass)) - case (given: SingleTypingResult, superclass: SingleTypingResult) => singleCanBeConvertedTo(given, superclass) - case (given: TypedUnion, superclass: TypedUnion) => - canBeConvertedTo(given.possibleTypes, superclass.possibleTypes) - } - } - - def canBeConvertedTo( - givenTypes: NonEmptyList[SingleTypingResult], - superclassCandidates: NonEmptyList[SingleTypingResult] - ): ValidatedNel[String, Unit] = { - // Would be more safety to do givenTypes.forAll(... superclassCandidates.exists ...) - we wil protect against - // e.g. (String | Int).canBeSubclassOf(String) which can fail in runtime for Int, but on the other hand we can't block user's intended action. - // He/she could be sure that in this type, only String will appear. He/she also can't easily downcast (String | Int) to String so leaving here - // "double exists" looks like a good tradeoff - condNel( - givenTypes.exists(given => superclassCandidates.exists(singleCanBeConvertedTo(given, _).isValid)), - (), - s"""None of the following types: - |${givenTypes.map(" - " + _.display).toList.mkString(",\n")} - |can be a subclass of any of: - |${superclassCandidates.map(" - " + _.display).toList.mkString(",\n")}""".stripMargin - ) - } - - def singleCanBeConvertedTo( - givenType: SingleTypingResult, - superclassCandidate: SingleTypingResult - ): ValidatedNel[String, Unit] = { - val givenClass = givenType.runtimeObjType - val givenSuperclas = superclassCandidate.runtimeObjType - - isStrictSubclass(givenClass, givenSuperclas) - } - - private def canNullBeConvertedTo(result: TypingResult): ValidatedNel[String, Unit] = result match { - // TODO: Null should not be subclass of typed map that has all values assigned. - case TypedObjectWithValue(_, _) => s"${TypedNull.display} cannot be subclass of type with value".invalidNel - case _ => ().validNel - } - - def isStrictSubclass(givenClass: TypedClass, givenSuperclass: TypedClass): Validated[NonEmptyList[String], Unit] = { - condNel( - givenClass == givenSuperclass, - (), - f"${givenClass.display} and ${givenSuperclass.display} are not the same" - ) orElse - condNel( - isAssignable(givenClass.klass, givenSuperclass.klass), - (), - s"${givenClass.klass} is not assignable from ${givenSuperclass.klass}" - ) - } - - // We double check with a fallback because lang3 only checks strict assignability subtyping. We also want to check - // for possible subtyping, e.g. Int to Long. - private def isAssignable(from: Class[_], to: Class[_]): Boolean = { - (from, to) match { - case (f, t) if ClassUtils.isAssignable(f, t, true) => true - case (f, t) if AllNumbers.contains(f) && AllNumbers.contains(t) => - AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) - case _ => false - } - } - -} diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index 4fbe1ce0724..2b36a96c9ab 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -3,6 +3,7 @@ package pl.touk.nussknacker.engine.api.typed import org.apache.commons.lang3.{ClassUtils, LocaleUtils} import org.springframework.util.StringUtils import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy +import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypedClass, TypedObjectWithValue} import java.nio.charset.Charset @@ -63,18 +64,22 @@ object TypeConversionHandler { StringConversion[ChronoLocalDateTime[_]](LocalDateTime.parse) ) - def canBeConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = { - handleNumberConversions(givenType.runtimeObjType, superclassCandidate) || - handleStringToValueClassConversions(givenType, superclassCandidate) - } + def canBeLooselyConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = + handleLooseConversion(givenType, superclassCandidate) - def canBeStrictlyConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = { - handleStrictNumberConversions(givenType.runtimeObjType, superclassCandidate) || - handleStringToValueClassConversions(givenType, superclassCandidate) + private def handleLooseConversion( + givenType: SingleTypingResult, + superclassCandidate: TypedClass + ) = { + handleStringToValueClassConversions(givenType, superclassCandidate) || + handleLooseNumberConversions(givenType.runtimeObjType, superclassCandidate) } // See org.springframework.core.convert.support.NumberToNumberConverterFactory - private def handleNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { + private def handleLooseNumberConversions( + givenClass: TypedClass, + superclassCandidate: TypedClass + ): Boolean = { val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) // We can't check precision here so we need to be loose here @@ -89,30 +94,26 @@ object TypeConversionHandler { } } - // See org.springframework.core.convert.support.NumberToNumberConverterFactory - private def handleStrictNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { - val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) - val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) - // We can't check precision here so we need to be loose here - // TODO: Add feature flag: strictNumberPrecisionChecking (default false?) + def canBeStrictlyConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = + handleStrictConversion(givenType, superclassCandidate) - def isFloating(candidate: Class[_]): Boolean = { - NumberTypesPromotionStrategy.isFloatingNumber(candidate) || candidate == classOf[java.math.BigDecimal] - } - def isDecimalNumber(candidate: Class[_]): Boolean = { - NumberTypesPromotionStrategy.isDecimalNumber(candidate) - } - - boxedSuperclassCandidate match { - case candidate if isFloating(candidate) => - ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) + private def handleStrictConversion(givenType: SingleTypingResult, superclassCandidate: TypedClass) = { + handleStringToValueClassConversions(givenType, superclassCandidate) || + handleStrictNumberConversions(givenType.runtimeObjType, superclassCandidate) + } - case candidate if isDecimalNumber(candidate) => - StrictConversionDeterminer.isAssignable(boxedGivenClass, candidate) + private def handleStrictNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { + val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) + val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) + // TODO: This is probably wrong - relying on index of AllNumbers + (boxedGivenClass, boxedSuperclassCandidate) match { + case (f, t) if ClassUtils.isAssignable(f, t, true) => true + case (f, t) if (AllNumbers.contains(f) && AllNumbers.contains(t)) => + AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) case _ => false - } + } } private def handleStringToValueClassConversions( diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala index 2ccf863a86a..1aff3f9a890 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala @@ -32,13 +32,13 @@ object typing { * If you need to retain conversion precision, use canBeStrictlyConvertedTo */ final def canBeConvertedTo(typingResult: TypingResult): Boolean = - ImplicitConversionDeterminer.canBeConvertedTo(this, typingResult).isValid + AssignabilityDeterminer.isAssignableLoose(this, typingResult).isValid /** * Checks if the conversion to a given typingResult can be made without loss of precision */ final def canBeStrictlyConvertedTo(typingResult: TypingResult): Boolean = - StrictConversionDeterminer.canBeConvertedTo(this, typingResult).isValid + AssignabilityDeterminer.isAssignableStrict(this, typingResult).isValid def valueOpt: Option[Any] diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala index e55fda73496..22d0836fd1b 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala @@ -5,6 +5,7 @@ import cats.data.NonEmptyList import org.scalatest.{Inside, OptionValues} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import pl.touk.nussknacker.engine.api.typed.AssignabilityDeterminer.isAssignableLoose import pl.touk.nussknacker.engine.api.typed.typing._ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with OptionValues with Inside { @@ -13,11 +14,11 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio private def list(arg: TypingResult) = Typed.genericTypeClass[java.util.List[_]](List(arg)) - import ImplicitConversionDeterminer.canBeConvertedTo + import AssignabilityDeterminer.isAssignable - test("determine if can be subclass for typed object") { + test("determine if can be subclass for simple typed objects") { - canBeConvertedTo( + isAssignableLoose( typeMap( "field1" -> Typed[String], "field2" -> Typed[Int], @@ -37,8 +38,10 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio "Field 'field4' is lacking" ) .invalid + } - canBeConvertedTo( + test("determine if can be subclass for map of typed objects") { + isAssignableLoose( typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))), typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe NonEmptyList @@ -49,30 +52,30 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio } test("determine if can be subclass for class") { - canBeConvertedTo(Typed.fromDetailedType[Set[BigDecimal]], Typed.fromDetailedType[Set[String]]) shouldBe + isAssignableLoose(Typed.fromDetailedType[Set[BigDecimal]], Typed.fromDetailedType[Set[String]]) shouldBe "Set[BigDecimal] cannot be converted to Set[String]".invalidNel } test("determine if can be subclass for tagged value") { - canBeConvertedTo( + isAssignableLoose( Typed.tagged(Typed.typedClass[String], "tag1"), Typed.tagged(Typed.typedClass[String], "tag2") ) shouldBe "Tagged values have unequal tags: tag1 and tag2".invalidNel - canBeConvertedTo(Typed.typedClass[String], Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe + isAssignableLoose(Typed.typedClass[String], Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe "The type is not a tagged value".invalidNel } test("determine if can be subclass for object with value") { - canBeConvertedTo(Typed.fromInstance(2), Typed.fromInstance(3)) shouldBe + isAssignableLoose(Typed.fromInstance(2), Typed.fromInstance(3)) shouldBe "Types with value have different values: 2 and 3".invalidNel } test("determine if can be subclass for null") { - canBeConvertedTo(Typed[String], TypedNull) shouldBe + isAssignableLoose(Typed[String], TypedNull) shouldBe "No type can be subclass of Null".invalidNel - canBeConvertedTo(TypedNull, Typed.fromInstance(1)) shouldBe + isAssignableLoose(TypedNull, Typed.fromInstance(1)) shouldBe "Null cannot be subclass of type with value".invalidNel } diff --git a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala index c191fb9d1a4..e905110d881 100644 --- a/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala +++ b/designer/server/src/main/scala/pl/touk/nussknacker/ui/process/test/ScenarioTestService.scala @@ -7,7 +7,7 @@ import pl.touk.nussknacker.engine.api.definition.{DualParameterEditor, Parameter import pl.touk.nussknacker.engine.api.editor.DualEditorMode import pl.touk.nussknacker.engine.api.graph.ScenarioGraph import pl.touk.nussknacker.engine.api.test.ScenarioTestData -import pl.touk.nussknacker.engine.api.typed.ImplicitConversionDeterminer +import pl.touk.nussknacker.engine.api.typed.AssignabilityDeterminer import pl.touk.nussknacker.engine.api.typed.typing.Typed import pl.touk.nussknacker.engine.canonicalgraph.CanonicalProcess import pl.touk.nussknacker.engine.definition.test.{TestInfoProvider, TestingCapabilities} @@ -134,7 +134,7 @@ class ScenarioTestService( val adaptedParameters = uiSourceParameter.parameters.map { uiParameter => uiParameter.editor match { case DualParameterEditor(StringParameterEditor, DualEditorMode.RAW) - if uiParameter.typ.canBeImplicitlyConvertedTo(Typed[String]) => + if uiParameter.typ.canBeConvertedTo(Typed[String]) => uiParameter.copy(editor = StringParameterEditor) case _ => uiParameter } From 65f0dbea741ddbc5f1e64100ce57e1574a8815ba Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Mon, 25 Nov 2024 20:49:19 +0100 Subject: [PATCH 20/26] tests for AssignabilityDeterminerSpec --- .../typed/AssignabilityDeterminerSpec.scala | 67 +++++++++++++++++++ .../api/typed/SubclassDeterminerSpec.scala | 13 ---- 2 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala delete mode 100644 components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala new file mode 100644 index 00000000000..619b61f4369 --- /dev/null +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala @@ -0,0 +1,67 @@ +package pl.touk.nussknacker.engine.api.typed + +import cats.data.NonEmptyList +import cats.data.Validated.{Invalid, Valid} +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers +import org.scalatest.prop.TableDrivenPropertyChecks.forAll +import org.scalatest.prop.Tables.Table +import pl.touk.nussknacker.engine.api.typed._ + +class AssignabilityDeterminerSpec extends AnyFunSuite with Matchers { + + // Test data table: (sourceType, targetType, expectedStrict, expectedLoose) + val strictConversionCases = Table( + ("sourceType", "targetType", "expectedStrict", "expectedLoose"), + (typing.Typed[Int], typing.Typed[Int], Valid(()), Valid(())), // Same primitive type + (typing.Typed[Int], typing.Typed[Double], Valid(()), Valid(())), // Primitive widening + (typing.Typed[List[Int]], typing.Typed[List[Int]], Valid(()), Valid(())), // Same generic type + (typing.Typed[List[Int]], typing.Typed[List[Any]], Valid(()), Valid(())), // Generic type variance + (typing.Typed[Map[String, Int]], typing.Typed[Map[String, Int]], Valid(()), Valid(())), // Same map type + (typing.Typed[Map[String, Int]], typing.Typed[Map[Any, Any]], Valid(()), Valid(())) + ) // Different records + + test("isAssignableStrict should pass for strict cases") { + forAll(strictConversionCases) { (sourceType, targetType, expectedStrict, _) => + val result = AssignabilityDeterminer.isAssignableStrict(sourceType, targetType) + result shouldBe expectedStrict + } + } + + test("isAssignableLoose should pass for strict cases") { + forAll(strictConversionCases) { (sourceType, targetType, _, expectedLoose) => + val result = AssignabilityDeterminer.isAssignableLoose(sourceType, targetType) + result shouldBe expectedLoose + } + } + + val looseConversionCases = Table( + ("sourceType", "targetType", "expectedStrict", "expectedLoose"), + (typing.Typed[Long], typing.Typed[Int], Invalid(NonEmptyList.of("")), Valid(())), + (typing.Typed[Double], typing.Typed[Int], Invalid(NonEmptyList.of("")), Invalid(NonEmptyList.of(""))), + (typing.Typed[BigDecimal], typing.Typed[Int], Invalid(NonEmptyList.of("")), Invalid(NonEmptyList.of(""))) + ) + + test("isAssignableStrict should fail for looser numerical cases") { + forAll(looseConversionCases) { (sourceType, targetType, expectedStrict, _) => + val result = AssignabilityDeterminer.isAssignableStrict(sourceType, targetType) + result match { + case Valid(_) if expectedStrict.isValid => succeed + case Invalid(_) if expectedStrict.isInvalid => succeed + case _ => fail(s"Unexpected result: $result for types $sourceType -> $targetType") + } + } + } + + test("isAssignableLoose should pass for looser cases") { + forAll(looseConversionCases) { (sourceType, targetType, _, expectedLoose) => + val result = AssignabilityDeterminer.isAssignableLoose(sourceType, targetType) + result match { + case Valid(_) if expectedLoose.isValid => succeed + case Invalid(_) if expectedLoose.isInvalid => succeed + case _ => fail(s"Unexpected result: $result for types $sourceType -> $targetType") + } + } + } + +} diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala deleted file mode 100644 index 44f7fe938da..00000000000 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/SubclassDeterminerSpec.scala +++ /dev/null @@ -1,13 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -import org.apache.commons.lang3.ClassUtils -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class SubclassDeterminerSpec extends AnyFunSuite with Matchers { - - test("Should validate assignability for decimal types") {} - - test("Should validate assignability for numerical types") {} - -} From ee83fef38a079c6c7490cb281a214863533e39d3 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Tue, 26 Nov 2024 19:41:28 +0100 Subject: [PATCH 21/26] Pr changes to AssignabilityDeterminerSpec and AssignabilityDeterminer. TypeConversionHandler logic branchout --- .../api/typed/AssignabilityDeterminer.scala | 14 ++-- .../api/typed/TypeConversionHandler.scala | 68 +++++++++---------- .../typed/AssignabilityDeterminerSpec.scala | 44 ++++++------ 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala index 05add2e02a9..a5a7595c7b0 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -7,12 +7,14 @@ import org.apache.commons.lang3.ClassUtils import pl.touk.nussknacker.engine.api.typed.typing._ /** - * This class determine if type can be subclass of other type. It basically based on fact that TypingResults are - * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). - * - * This class, like CommonSupertypeFinder is in spirit of "Be type safety as much as possible, but also provide some helpful - * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". - * WARNING: Evaluation of SpEL expressions fit into this spirit, for other language evaluation engines you need to provide such a compatibility. + * This class determine whether we can convert a class to another one. We provide two modes of conversion - + * 1. Loose conversion is based on the fact that TypingResults are + * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). It is basically how SpEL + * can convert things. Like CommonSupertypeFinder it's in the spirit of "Be type safe as much as possible, but also provide some helpful + * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". + * 2. Strict conversion checks whether we can convert the type without losing precision. Eg only widening numerical types + * are allowed ( Int -> Long). For other types it should work the same as a loose conversion. + * */ object AssignabilityDeterminer { diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index 2b36a96c9ab..e5629241152 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala @@ -6,6 +6,7 @@ import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrate import pl.touk.nussknacker.engine.api.typed.supertype.NumberTypesPromotionStrategy.AllNumbers import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, TypedClass, TypedObjectWithValue} +import java.math.BigInteger import java.nio.charset.Charset import java.time._ import java.time.chrono.{ChronoLocalDate, ChronoLocalDateTime} @@ -36,8 +37,8 @@ object TypeConversionHandler { cl } - def canConvert(value: String, superclassCandidate: TypedClass): Boolean = { - ClassUtils.isAssignable(superclassCandidate.klass, klass, true) && Try( + def canConvert(value: String, to: TypedClass): Boolean = { + ClassUtils.isAssignable(to.klass, klass, true) && Try( convert(value) ).isSuccess } @@ -64,26 +65,33 @@ object TypeConversionHandler { StringConversion[ChronoLocalDateTime[_]](LocalDateTime.parse) ) - def canBeLooselyConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = - handleLooseConversion(givenType, superclassCandidate) + def canBeLooselyConvertedTo(from: SingleTypingResult, to: TypedClass): Boolean = + canBeConvertedToAux(from, to) - private def handleLooseConversion( - givenType: SingleTypingResult, - superclassCandidate: TypedClass - ) = { - handleStringToValueClassConversions(givenType, superclassCandidate) || - handleLooseNumberConversions(givenType.runtimeObjType, superclassCandidate) + def canBeStrictlyConvertedTo(from: SingleTypingResult, to: TypedClass): Boolean = + canBeConvertedToAux(from, to, strict = true) + + private def canBeConvertedToAux(from: SingleTypingResult, to: TypedClass, strict: Boolean = false) = { + handleStringToValueClassConversions(from, to) || + handleNumberConversion(from.runtimeObjType, to, strict) + } + + private def handleNumberConversion(from: SingleTypingResult, to: TypedClass, strict: Boolean) = { + val boxedGivenClass = ClassUtils.primitiveToWrapper(from.runtimeObjType.klass) + val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(to.klass) + + if (strict) + handleStrictNumberConversions(boxedGivenClass, boxedSuperclassCandidate) + else + handleLooseNumberConversion(boxedGivenClass, boxedSuperclassCandidate) } // See org.springframework.core.convert.support.NumberToNumberConverterFactory - private def handleLooseNumberConversions( - givenClass: TypedClass, - superclassCandidate: TypedClass + private def handleLooseNumberConversion( + boxedGivenClass: Class[_], + boxedSuperclassCandidate: Class[_] ): Boolean = { - val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) - val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) // We can't check precision here so we need to be loose here - // TODO: Add feature flag: strictNumberPrecisionChecking (default false?) if (NumberTypesPromotionStrategy .isFloatingNumber(boxedSuperclassCandidate) || boxedSuperclassCandidate == classOf[java.math.BigDecimal]) { ClassUtils.isAssignable(boxedGivenClass, classOf[Number], true) @@ -94,21 +102,11 @@ object TypeConversionHandler { } } - def canBeStrictlyConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = - handleStrictConversion(givenType, superclassCandidate) - - private def handleStrictConversion(givenType: SingleTypingResult, superclassCandidate: TypedClass) = { - handleStringToValueClassConversions(givenType, superclassCandidate) || - handleStrictNumberConversions(givenType.runtimeObjType, superclassCandidate) - } - - private def handleStrictNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { - - val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) - val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) - // TODO: This is probably wrong - relying on index of AllNumbers - (boxedGivenClass, boxedSuperclassCandidate) match { - case (f, t) if ClassUtils.isAssignable(f, t, true) => true + private def handleStrictNumberConversions(givenClass: Class[_], to: Class[_]): Boolean = { + (givenClass, to) match { + case (bigInteger, t) + if (bigInteger == classOf[BigInteger] && (t == classOf[BigDecimal] || t == classOf[BigInteger])) => + true case (f, t) if (AllNumbers.contains(f) && AllNumbers.contains(t)) => AllNumbers.indexOf(f) >= AllNumbers.indexOf(t) case _ => false @@ -117,12 +115,12 @@ object TypeConversionHandler { } private def handleStringToValueClassConversions( - givenType: SingleTypingResult, - superclassCandidate: TypedClass + from: SingleTypingResult, + to: TypedClass ): Boolean = - givenType match { + from match { case TypedObjectWithValue(_, str: String) => - stringConversions.exists(_.canConvert(str, superclassCandidate)) + stringConversions.exists(_.canConvert(str, to)) case _ => false } diff --git a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala index 619b61f4369..82d90e679f2 100644 --- a/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala +++ b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminerSpec.scala @@ -6,44 +6,44 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks.forAll import org.scalatest.prop.Tables.Table -import pl.touk.nussknacker.engine.api.typed._ +import pl.touk.nussknacker.engine.api.typed.typing.Typed class AssignabilityDeterminerSpec extends AnyFunSuite with Matchers { - // Test data table: (sourceType, targetType, expectedStrict, expectedLoose) - val strictConversionCases = Table( + val wideningConversionCases = Table( ("sourceType", "targetType", "expectedStrict", "expectedLoose"), - (typing.Typed[Int], typing.Typed[Int], Valid(()), Valid(())), // Same primitive type - (typing.Typed[Int], typing.Typed[Double], Valid(()), Valid(())), // Primitive widening - (typing.Typed[List[Int]], typing.Typed[List[Int]], Valid(()), Valid(())), // Same generic type - (typing.Typed[List[Int]], typing.Typed[List[Any]], Valid(()), Valid(())), // Generic type variance - (typing.Typed[Map[String, Int]], typing.Typed[Map[String, Int]], Valid(()), Valid(())), // Same map type - (typing.Typed[Map[String, Int]], typing.Typed[Map[Any, Any]], Valid(()), Valid(())) - ) // Different records + (Typed[Int], Typed[Int], Valid(()), Valid(())), + (Typed[Int], Typed[Double], Valid(()), Valid(())), + (Typed[List[Int]], Typed[List[Int]], Valid(()), Valid(())), + (Typed[List[Int]], Typed[List[Any]], Valid(()), Valid(())), + (Typed[Map[String, Int]], Typed[Map[String, Int]], Valid(()), Valid(())), + (Typed[Map[String, Int]], Typed[Map[Any, Any]], Valid(()), Valid(())) + ) - test("isAssignableStrict should pass for strict cases") { - forAll(strictConversionCases) { (sourceType, targetType, expectedStrict, _) => + test("isAssignableStrict should pass for widening cases") { + forAll(wideningConversionCases) { (sourceType, targetType, expectedStrict, _) => val result = AssignabilityDeterminer.isAssignableStrict(sourceType, targetType) result shouldBe expectedStrict } } - test("isAssignableLoose should pass for strict cases") { - forAll(strictConversionCases) { (sourceType, targetType, _, expectedLoose) => + test("isAssignableLoose should pass for widening cases") { + forAll(wideningConversionCases) { (sourceType, targetType, _, expectedLoose) => val result = AssignabilityDeterminer.isAssignableLoose(sourceType, targetType) result shouldBe expectedLoose } } - val looseConversionCases = Table( + val narrowingConversionCases = Table( ("sourceType", "targetType", "expectedStrict", "expectedLoose"), - (typing.Typed[Long], typing.Typed[Int], Invalid(NonEmptyList.of("")), Valid(())), - (typing.Typed[Double], typing.Typed[Int], Invalid(NonEmptyList.of("")), Invalid(NonEmptyList.of(""))), - (typing.Typed[BigDecimal], typing.Typed[Int], Invalid(NonEmptyList.of("")), Invalid(NonEmptyList.of(""))) + (Typed[Long], Typed[Int], Invalid(NonEmptyList.of("")), Valid(())), + (Typed[Long], Typed[Short], Invalid(NonEmptyList.of("")), Valid(())), + (Typed[Double], Typed[Float], Invalid(NonEmptyList.of("")), Valid(())), + (Typed[BigDecimal], Typed[Double], Invalid(NonEmptyList.of("")), Valid(())) ) - test("isAssignableStrict should fail for looser numerical cases") { - forAll(looseConversionCases) { (sourceType, targetType, expectedStrict, _) => + test("isAssignableStrict should fail for narrowing numerical cases") { + forAll(narrowingConversionCases) { (sourceType, targetType, expectedStrict, _) => val result = AssignabilityDeterminer.isAssignableStrict(sourceType, targetType) result match { case Valid(_) if expectedStrict.isValid => succeed @@ -53,8 +53,8 @@ class AssignabilityDeterminerSpec extends AnyFunSuite with Matchers { } } - test("isAssignableLoose should pass for looser cases") { - forAll(looseConversionCases) { (sourceType, targetType, _, expectedLoose) => + test("isAssignableLoose should pass for narrowing cases") { + forAll(narrowingConversionCases) { (sourceType, targetType, _, expectedLoose) => val result = AssignabilityDeterminer.isAssignableLoose(sourceType, targetType) result match { case Valid(_) if expectedLoose.isValid => succeed From 751a8edc7e6c56c9ec4b916121200f4160c5492e Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Tue, 26 Nov 2024 22:12:51 +0100 Subject: [PATCH 22/26] Changes to MigrationGuide.md and comment in AssignabilityDeterminer --- .../engine/api/typed/AssignabilityDeterminer.scala | 2 +- docs/MigrationGuide.md | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala index a5a7595c7b0..ff16ef51a52 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -12,7 +12,7 @@ import pl.touk.nussknacker.engine.api.typed.typing._ * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). It is basically how SpEL * can convert things. Like CommonSupertypeFinder it's in the spirit of "Be type safe as much as possible, but also provide some helpful * conversion for types not in the same jvm class hierarchy like boxed Integer to boxed Long and so on". - * 2. Strict conversion checks whether we can convert the type without losing precision. Eg only widening numerical types + * 2. Strict conversion checks whether we can convert to a wider type. Eg only widening numerical types * are allowed ( Int -> Long). For other types it should work the same as a loose conversion. * */ diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index e9d2e4bf89c..ae41ddcc96a 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -62,6 +62,11 @@ To see the biggest differences please consult the [changelog](Changelog.md). * [#7162](https://github.com/TouK/nussknacker/pull/7162) When component declares that requires parameter with either `SpelTemplateParameterEditor` or `SqlParameterEditor` editor, in the runtime, for the expression evaluation result, will be used the new `TemplateEvaluationResult` class instead of `String` class. To access the previous `String` use `TemplateEvaluationResult.renderedTemplate` method. +* [#7246](https://github.com/TouK/nussknacker/pull/7246) + * Typing api changes: + * CanBeSubclassDeterminer.canBeSubclassOf changed to + AssignabilityDeterminer.isAssignableLoose. + * TypingResult.canBeSubclassOf changed to TypingResult.canBeConvertedTo ### REST API changes @@ -78,6 +83,8 @@ To see the biggest differences please consult the [changelog](Changelog.md). * added optional query param `enrichedWithUiConfig` * added `requiredParam` property to the response for parameter config at `components['component-id'].parameters[*]` +* [#7246](https://github.com/TouK/nussknacker/pull/7246) Changes in DictApiEndpoints: + * `DictListRequestDto` `expectedType`: TypingResultInJson -> Json ### Configuration changes * [#6958](https://github.com/TouK/nussknacker/pull/6958) Added message size limit in the "Kafka" exceptionHandler: `maxMessageBytes`. @@ -625,9 +632,6 @@ To see the biggest differences please consult the [changelog](Changelog.md). * `api/parameters/*/validate` request * `scenarioName` is removed * `processProperties` is removed -* [#7115](https://github.com/TouK/nussknacker/pull/7115) Changes in DictApiEndpoints: - * `DictListRequestDto` `expectedType`: TypingResultInJson -> Json - ### Configuration changes * [#4860](https://github.com/TouK/nussknacker/pull/4860) In file-based configuration, the field `scenarioTypes..additionalPropertiesConfig` is renamed to `scenarioTypes..scenarioPropertiesConfig` * [#5077](https://github.com/TouK/nussknacker/pull/5077) In SQL enricher configuration, `connectionProperties` was changed to `dataSourceProperties` From c557cb86c0bfc12a26a0752ddfae28ba3bc9d189 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak <137457433+Diamekod0221@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:00:57 +0100 Subject: [PATCH 23/26] Update designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Słabek --- .../pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala index 3decd5d7d82..c695c49d5f0 100644 --- a/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala +++ b/designer/server/src/test/scala/pl/touk/nussknacker/ui/api/DictApiHttpServiceSpec.scala @@ -31,10 +31,7 @@ class DictApiHttpServiceSpec .post(s"$nuDesignerHttpAddress/api/processDefinitionData/${Streaming.stringify}/dicts") .Then() .statusCode(200) - .equalsJsonBody( - s"""[ - |]""".stripMargin - ) + .equalsJsonBody("[]") } From 8a33cf31b6494dfcb18a56449f67912cabcba6f8 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak <137457433+Diamekod0221@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:01:28 +0100 Subject: [PATCH 24/26] Update components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Słabek --- .../nussknacker/engine/api/typed/AssignabilityDeterminer.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala index ff16ef51a52..8e4cec7ee42 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -7,7 +7,7 @@ import org.apache.commons.lang3.ClassUtils import pl.touk.nussknacker.engine.api.typed.typing._ /** - * This class determine whether we can convert a class to another one. We provide two modes of conversion - + * This class determine whether we can assign one type to another type - that is if its the same class, a subclass or can be converted to another type. We provide two modes of conversion - * 1. Loose conversion is based on the fact that TypingResults are * sets of possible supertypes with some additional restrictions (like TypedObjectTypingResult). It is basically how SpEL * can convert things. Like CommonSupertypeFinder it's in the spirit of "Be type safe as much as possible, but also provide some helpful From 2c855f1d47978cdca7c6287b1095b5413e9f0135 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Tue, 26 Nov 2024 22:39:12 +0100 Subject: [PATCH 25/26] rename to DictKeyWithLabelExpressionParser --- .../dictWithLabel/DictKeyWithLabelExpressionParser.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala index ee07e37e8a1..078bc7e5ba7 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/language/dictWithLabel/DictKeyWithLabelExpressionParser.scala @@ -24,11 +24,11 @@ case class DictKeyWithLabelExpressionTypingInfo(key: String, label: Option[Strin // We should support at least types defined in FragmentParameterValidator#permittedTypesForEditors override def typingResult: TypingResult = expectedType match { - case clazz: TypedClass if clazz.canBeSubclassOf(Typed[Long]) && Try(key.toLong).toOption.isDefined => + case clazz: TypedClass if clazz.canBeConvertedTo(Typed[Long]) && Try(key.toLong).toOption.isDefined => TypedObjectWithValue(clazz.runtimeObjType, key.toLong) - case clazz: TypedClass if clazz.canBeSubclassOf(Typed[Boolean]) && Try(key.toBoolean).toOption.isDefined => + case clazz: TypedClass if clazz.canBeConvertedTo(Typed[Boolean]) && Try(key.toBoolean).toOption.isDefined => TypedObjectWithValue(clazz.runtimeObjType, key.toBoolean) - case clazz: TypedClass if clazz.canBeSubclassOf(Typed[String]) => + case clazz: TypedClass if clazz.canBeConvertedTo(Typed[String]) => TypedObjectWithValue(clazz.runtimeObjType, key) case _ => expectedType } From d4f773572cea18278cb0b48475952c1ce8588674 Mon Sep 17 00:00:00 2001 From: Marcel Philipiak Date: Tue, 26 Nov 2024 22:42:25 +0100 Subject: [PATCH 26/26] patches to SpelExpressionSpec --- .../pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 645ae2fd056..e0b8f635534 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -2044,7 +2044,7 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD val parsedRoundTripExpression = parse[Any](mapExpression + ".toList.toMap", customCtx).validValue parsedRoundTripExpression.evaluateSync[Any](customCtx) shouldBe givenMap val roundTripTypeIsAGeneralizationOfGivenType = - givenMapExpression.returnType canBeSubclassOf parsedRoundTripExpression.returnType + givenMapExpression.returnType canBeConvertedTo parsedRoundTripExpression.returnType roundTripTypeIsAGeneralizationOfGivenType shouldBe true }