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..8e4cec7ee42 --- /dev/null +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -0,0 +1,251 @@ +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 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 + * 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 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. + * + */ +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/CanBeSubclassDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala deleted file mode 100644 index 9545ab30dd6..00000000000 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/CanBeSubclassDeterminer.scala +++ /dev/null @@ -1,214 +0,0 @@ -package pl.touk.nussknacker.engine.api.typed - -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.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. - */ -trait CanBeSubclassDeterminer { - - 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( - givenType: SingleTypingResult, - superclassCandidate: SingleTypingResult - ): ValidatedNel[String, Unit] = { - val objTypeRestriction = classCanBeSubclassOf(givenType, superclassCandidate.runtimeObjType) - 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( - canBeSubclassOf(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) - } - - protected def classCanBeSubclassOf( - givenType: SingleTypingResult, - superclassCandidate: TypedClass - ): ValidatedNel[String, Unit] = { - val givenClass = givenType.runtimeObjType - - val equalClassesOrCanAssign = - condNel( - givenClass == superclassCandidate, - (), - f"${givenClass.display} and ${superclassCandidate.display} are not the same" - ) orElse - isAssignable(givenClass.klass, superclassCandidate.klass) - - val canBeSubclass = equalClassesOrCanAssign andThen (_ => typeParametersMatches(givenClass, superclassCandidate)) - 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, - (), - 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) => - canBeSubclassOf(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, - (), - s"Key types of Maps ${givenKeyParam.display} and ${superclassKeyParam.display} are not equals" - ) andThen (_ => canBeSubclassOf(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" - ) - } - } - - 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) - } - - // 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") -} - -object CanBeSubclassDeterminer extends CanBeSubclassDeterminer 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..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.canBeSubclassOf(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.canBeSubclassOf(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/TypeConversionHandler.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/TypeConversionHandler.scala index cc91f255239..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 @@ -3,8 +3,10 @@ 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.math.BigInteger import java.nio.charset.Charset import java.time._ import java.time.chrono.{ChronoLocalDate, ChronoLocalDateTime} @@ -20,8 +22,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?) @@ -35,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 } @@ -63,17 +65,33 @@ object TypeConversionHandler { StringConversion[ChronoLocalDateTime[_]](LocalDateTime.parse) ) - def canBeConvertedTo(givenType: SingleTypingResult, superclassCandidate: TypedClass): Boolean = { - handleNumberConversions(givenType.runtimeObjType, superclassCandidate) || - handleStringToValueClassConversions(givenType, superclassCandidate) + def canBeLooselyConvertedTo(from: SingleTypingResult, to: TypedClass): Boolean = + canBeConvertedToAux(from, to) + + 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 handleNumberConversions(givenClass: TypedClass, superclassCandidate: TypedClass): Boolean = { - val boxedGivenClass = ClassUtils.primitiveToWrapper(givenClass.klass) - val boxedSuperclassCandidate = ClassUtils.primitiveToWrapper(superclassCandidate.klass) + private def handleLooseNumberConversion( + boxedGivenClass: Class[_], + boxedSuperclassCandidate: Class[_] + ): Boolean = { // 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) @@ -84,13 +102,25 @@ object TypeConversionHandler { } } + 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 + + } + } + 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/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..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 @@ -27,15 +27,18 @@ 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 + /** + * 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 = + 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 = + AssignabilityDeterminer.isAssignableStrict(this, typingResult).isValid def valueOpt: Option[Any] @@ -466,7 +469,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(_.canBeConvertedTo(Typed.fromDetailedType[T])) + .map(new TypingResultTypedValue(_)) } } 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..82d90e679f2 --- /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.typing.Typed + +class AssignabilityDeterminerSpec extends AnyFunSuite with Matchers { + + val wideningConversionCases = Table( + ("sourceType", "targetType", "expectedStrict", "expectedLoose"), + (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 widening cases") { + forAll(wideningConversionCases) { (sourceType, targetType, expectedStrict, _) => + val result = AssignabilityDeterminer.isAssignableStrict(sourceType, targetType) + result shouldBe expectedStrict + } + } + + test("isAssignableLoose should pass for widening cases") { + forAll(wideningConversionCases) { (sourceType, targetType, _, expectedLoose) => + val result = AssignabilityDeterminer.isAssignableLoose(sourceType, targetType) + result shouldBe expectedLoose + } + } + + val narrowingConversionCases = Table( + ("sourceType", "targetType", "expectedStrict", "expectedLoose"), + (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 narrowing numerical cases") { + forAll(narrowingConversionCases) { (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 narrowing cases") { + forAll(narrowingConversionCases) { (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/TypedFromInstanceTest.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypedFromInstanceTest.scala index 6e8e4ce6708..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).canBeSubclassOf(Typed(classOf[List[_]])) shouldBe true - Typed.fromInstance(Nil.asJava).canBeSubclassOf(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.canBeSubclassOf(Typed(klass)) shouldBe true + typingResult.canBeConvertedTo(Typed(klass)) shouldBe true typingResult.withoutValue .asInstanceOf[TypedClass] .params .loneElement - .canBeSubclassOf(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 - .canBeSubclassOf(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/TypingResultErrorMessagesSpec.scala b/components-api/src/test/scala/pl/touk/nussknacker/engine/api/typed/TypingResultErrorMessagesSpec.scala index 5483df1a9a8..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 CanBeSubclassDeterminer.canBeSubclassOf + import AssignabilityDeterminer.isAssignable - test("determine if can be subclass for typed object") { + test("determine if can be subclass for simple typed objects") { - canBeSubclassOf( + 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 + } - canBeSubclassOf( + 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") { - canBeSubclassOf(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") { - canBeSubclassOf( + isAssignableLoose( 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 + 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") { - canBeSubclassOf(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") { - canBeSubclassOf(Typed[String], TypedNull) shouldBe + isAssignableLoose(Typed[String], TypedNull) shouldBe "No type can be subclass of Null".invalidNel - canBeSubclassOf(TypedNull, Typed.fromInstance(1)) shouldBe + isAssignableLoose(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 ec38ad9cf2a..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]).canBeSubclassOf( + typeMap("field1" -> Typed[String], "field2" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe true - typeMap("field1" -> Typed[String]).canBeSubclassOf( + typeMap("field1" -> Typed[String]).canBeConvertedTo( typeMap("field1" -> Typed[String], "field2" -> Typed[Int]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[Int]).canBeConvertedTo( typeMap("field1" -> Typed[String]) ) shouldBe false - typeMap("field1" -> Typed[Int]).canBeSubclassOf( + typeMap("field1" -> Typed[Int]).canBeConvertedTo( 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]))).canBeConvertedTo( 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]))).canBeConvertedTo( typeMap("field1" -> list(typeMap("field2" -> Typed[String]))) ) shouldBe false - typeMap("field1" -> Typed[String]).canBeSubclassOf(Typed[java.util.Map[_, _]]) shouldBe true + typeMap("field1" -> Typed[String]).canBeConvertedTo(Typed[java.util.Map[_, _]]) shouldBe true - Typed[java.util.Map[_, _]].canBeSubclassOf(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,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]).canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeConvertedTo(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]).canBeConvertedTo(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.canBeConvertedTo(Typed[Int]) shouldBe true + Typed[Int].canBeConvertedTo(Unknown) shouldBe true - Unknown.canBeSubclassOf(Typed(Typed[String], Typed[Int])) shouldBe true - Typed(Typed[String], Typed[Int]).canBeSubclassOf(Unknown) shouldBe true + Unknown.canBeConvertedTo(Typed(Typed[String], Typed[Int])) shouldBe true + Typed(Typed[String], Typed[Int]).canBeConvertedTo(Unknown) shouldBe true - Unknown.canBeSubclassOf(typeMap("field1" -> Typed[String])) shouldBe true - typeMap("field1" -> Typed[String]).canBeSubclassOf(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]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.List[BigDecimal]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true + .canBeConvertedTo(Typed.fromDetailedType[java.util.List[Number]]) shouldBe true Typed .fromDetailedType[java.util.List[Number]] - .canBeSubclassOf(Typed.fromDetailedType[java.util.List[BigDecimal]]) shouldBe false + .canBeConvertedTo(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 + .canBeConvertedTo(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 + .canBeConvertedTo(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 + .canBeConvertedTo(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 + .canBeConvertedTo(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 + .canBeConvertedTo(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 + .canBeConvertedTo(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 + .canBeConvertedTo(Typed.fromDetailedType[java.util.Map[_, BigDecimal]]) shouldBe true Typed .fromDetailedType[java.util.Map[_, BigDecimal]] - .canBeSubclassOf(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]].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]] + .canBeConvertedTo(Typed.fromDetailedType[Array[BigDecimal]]) shouldBe true + Typed + .fromDetailedType[Array[BigDecimal]] + .canBeConvertedTo(Typed.fromDetailedType[Array[Number]]) shouldBe true + Typed + .fromDetailedType[Array[Number]] + .canBeConvertedTo(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 true - 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].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") { @@ -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 + .canBeConvertedTo(Typed.tagged(Typed.typedClass[String], "tag1")) shouldBe true Typed .tagged(Typed.typedClass[String], "tag1") - .canBeSubclassOf(Typed.tagged(Typed.typedClass[String], "tag2")) shouldBe false + .canBeConvertedTo(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 + .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.canBeSubclassOf(Typed[Int]) shouldBe true - TypedNull.canBeSubclassOf(Typed.fromInstance(4)) shouldBe false - TypedNull.canBeSubclassOf(TypedNull) shouldBe true - Typed[String].canBeSubclassOf(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") { @@ -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).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").canBeSubclassOf(Typed.typedClass[LocalDate]) shouldBe true - Typed.fromInstance("2007-12-03T10:15:30").canBeSubclassOf(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").canBeSubclassOf(Typed.typedClass[LocalDate]) shouldBe false - Typed.fromInstance("2007-12-03").canBeSubclassOf(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") { @@ -443,7 +449,7 @@ class TypingResultSpec logger.trace(s"Checking: ${input.display}") withClue(s"Input: ${input.display};") { - input.canBeSubclassOf(input) shouldBe true + input.canBeConvertedTo(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.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.canBeSubclassOf(superType) + input.canBeConvertedTo(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.canBeConvertedTo(first) shouldBe true + second.canBeConvertedTo(second) shouldBe true val superType = CommonSupertypeFinder.Default.commonSupertype(first, second) withClue(s"Supertype: ${superType.display};") { - first.canBeSubclassOf(superType) - second.canBeSubclassOf(superType) + first.canBeConvertedTo(superType) + second.canBeConvertedTo(superType) } } } diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png index 2faa0fce27b..58650a450c5 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by name with multiple words #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png index 8f79298458d..eb1efbb55f4 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png index a7f073a9601..56cb0e2a071 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should allow filtering by usage #1.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png index 5862bd56258..984ad5d2a56 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should apply filters from query #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png index 70f95efa447..4b7352cf0d6 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display component #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png index e5cc8a1b1b0..af652133866 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should display usages #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png index 821872447cf..753570d5904 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png index 79f4a5c188a..bab27b48f4b 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #1.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png index 5584ba90ae7..454cce15e47 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usage types #2.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png index 8bfa0387e8b..c4b60b68bcf 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Components list should filter usages #0.png differ diff --git a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process should validate process on nodes paste #0.png b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process should validate process on nodes paste #0.png index 77454938c30..5e30a7690c2 100644 Binary files a/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process should validate process on nodes paste #0.png and b/designer/client/cypress/e2e/__image_snapshots__/electron/Linux/Process should validate process on nodes paste #0.png differ 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..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 @@ -56,13 +56,15 @@ 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 { success( dictionaries - .filter { case (id, definition) => definition.valueType(id).canBeSubclassOf(expectedType) } + .filter { case (id, definition) => + definition.valueType(id).canBeStrictlyConvertedTo(expectedType) + } .map { case (id, _) => DictDto(id, id) } .toList ) 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/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..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.CanBeSubclassDeterminer +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.canBeSubclassOf(Typed[String]) => + if uiParameter.typ.canBeConvertedTo(Typed[String]) => uiParameter.copy(editor = StringParameterEditor) case _ => uiParameter } 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..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 @@ -16,17 +16,34 @@ 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("[]") + + } + "return proper list for expected type String" in { given() .when() .basicAuthAllPermUser() .jsonBody("""{ | "expectedType" : { - | "value" : { | "type" : "TypedClass", | "refClazzName" : "java.lang.String", | "params" : [] - | } | } |}""".stripMargin) .post(s"$nuDesignerHttpAddress/api/processDefinitionData/${Streaming.stringify}/dicts") @@ -56,11 +73,33 @@ class DictApiHttpServiceSpec .basicAuthAllPermUser() .jsonBody("""{ | "expectedType" : { - | "value" : { | "type" : "TypedClass", | "refClazzName" : "java.lang.Long", | "params" : [] - | } + | } + |}""".stripMargin) + .post(s"$nuDesignerHttpAddress/api/processDefinitionData/${Streaming.stringify}/dicts") + .Then() + .statusCode(200) + .equalsJsonBody( + s"""[ + | { + | "id" : "long_dict", + | "label" : "long_dict" + | } + |]""".stripMargin + ) + } + + "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") @@ -95,11 +134,9 @@ class DictApiHttpServiceSpec .basicAuthAllPermUser() .jsonBody("""{ | "expectedType" : { - | "value" : { | "type" : "TypedClass", | "refClazzName" : "java.lang.Long", | "params" : [] - | } | } |}""".stripMargin) .post(s"$nuDesignerHttpAddress/api/processDefinitionData/thisProcessingTypeDoesNotExist/dicts") 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))) 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 diff --git a/docs/MigrationGuide.md b/docs/MigrationGuide.md index 8c04b9c82d2..4700fa604b3 100644 --- a/docs/MigrationGuide.md +++ b/docs/MigrationGuide.md @@ -57,6 +57,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 @@ -73,6 +78,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`. @@ -620,7 +627,6 @@ To see the biggest differences please consult the [changelog](Changelog.md). * `api/parameters/*/validate` request * `scenarioName` is removed * `processProperties` is removed - ### 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` 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..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.canBeSubclassOf(typ) - val typedObjectCase = typ.isInstanceOf[TypedObjectTypingResult] && typeFromInstance.canBeSubclassOf( + 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 bdb37f86bee..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.canBeSubclassOf(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.canBeSubclassOf(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,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.canBeConvertedTo( + 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.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 f87e70e735d..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,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.canBeConvertedTo( + 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..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.canBeSubclassOf(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 d3e36185645..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,7 +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).canBeSubclassOf(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) => @@ -82,7 +82,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 + .canBeConvertedTo(Typed[Number]) && typ.canBeConvertedTo(targetType.toTypingResult) => targetType.toTypingResult case (recordType: TypedObjectTypingResult, rowType: RowType) if Set[Class[_]](javaMapClass, rowClass).contains(recordType.runtimeObjType.klass) => @@ -104,10 +105,10 @@ object ToTableTypeSchemaBasedEncoder { case ( TypedObjectTypingResult(_, TypedClass(`javaMapClass`, keyType :: valueType :: Nil), _), multisetType: MultisetType - ) if valueType.canBeSubclassOf(Typed[Int]) => + ) if valueType.canBeConvertedTo(Typed[Int]) => alignMultisetType(keyType, multisetType) case (TypedClass(`javaMapClass`, keyType :: valueType :: Nil), multisetType: MultisetType) - if valueType.canBeSubclassOf(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 d022901ffc8..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(_.canBeSubclassOf(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.canBeSubclassOf(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 e17be319fa6..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(!_.canBeSubclassOf(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 cdceb363052..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,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.canBeConvertedTo( + 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..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.canBeSubclassOf(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 263c406181a..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.canBeSubclassOf(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.canBeSubclassOf(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 bad04d2eca9..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,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.canBeConvertedTo(y) || Typed + .genericTypeClass[java.util.List[_]](tc.params) + .canBeConvertedTo(y) + case (x, Parameter(_, y)) => x.canBeConvertedTo(y) } val checkVarArgs = methodTypeInfo.varArg match { case Some(Parameter(_, t)) => - arguments.drop(methodTypeInfo.noVarArgs.length).forall(_.canBeSubclassOf(t)) + arguments.drop(methodTypeInfo.noVarArgs.length).forall(_.canBeConvertedTo(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.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 c6868bb4bb5..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.canBeSubclassOf(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.canBeSubclassOf(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.canBeSubclassOf(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 2a2eb13bf82..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 } @@ -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.canBeConvertedTo(Typed[Long])) { key.toLong.asInstanceOf[T] - } else if (expectedType.canBeSubclassOf(Typed[Boolean])) { + } else if (expectedType.canBeConvertedTo(Typed[Boolean])) { key.toBoolean.asInstanceOf[T] - } else if (expectedType.canBeSubclassOf(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 333c1ff0a86..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.canBeSubclassOf(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.canBeSubclassOf(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 099e5c44183..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(_.canBeSubclassOf(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.canBeSubclassOf(Typed[String]) => + case indexKey :: Nil if indexKey.canBeConvertedTo(Typed[String]) => if (dynamicPropertyAccessAllowed) valid(Unknown) else invalid(DynamicPropertyAccessError) case _ :: Nil => indexer.children match { @@ -356,7 +356,7 @@ 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.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)) @@ -365,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.canBeSubclassOf(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) @@ -395,18 +395,18 @@ 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.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.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(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.canBeSubclassOf(Typed[Number]) => + case left :: Nil if left.canBeConvertedTo(Typed[Number]) => valid(left) case left :: Nil => invalid(OperatorNonNumericError(e.getOperatorName, left)) @@ -448,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.canBeSubclassOf(Typed[Boolean]) => + case result :: Nil if result.canBeConvertedTo(Typed[Boolean]) => valid(selectionType) case other => invalid(IllegalSelectionTypeError(other), selectionType) @@ -459,7 +459,7 @@ private[spel] class Typer( case condition :: onTrue :: onFalse :: Nil => for { _ <- Option(condition) - .filter(_.canBeSubclassOf(Typed[Boolean])) + .filter(_.canBeConvertedTo(Typed[Boolean])) .map(valid) .getOrElse(invalid(TernaryOperatorNotBooleanError(condition))) } yield CommonSupertypeFinder.Default.commonSupertype(onTrue, onFalse) @@ -520,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.canBeSubclassOf(Typed[java.util.Collection[_]]) || + if tc.runtimeObjType.canBeConvertedTo(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.canBeConvertedTo(Typed[java.util.Map[_, _]]) => Typed.record(Map.empty) case _ => parentType @@ -575,7 +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.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(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)(_)) @@ -594,7 +594,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.canBeConvertedTo(Typed[Number]) => val result = operationOnTypesValue[Number, Any](left)(op).getOrElse(left.withoutValue) valid(result) case left :: Nil => @@ -690,10 +690,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.canBeConvertedTo(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.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 ad0d4bf8ae8..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(_.canBeSubclassOf(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/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 } 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..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 @@ -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.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 2233d31fd93..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.canBeSubclassOf(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 7d946008f7c..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.canBeSubclassOf(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 6d5fc0b8f52..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 @@ -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.canBeConvertedTo(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) =>