Skip to content

Commit

Permalink
review fixes 7
Browse files Browse the repository at this point in the history
  • Loading branch information
Łukasz Bigorajski committed Oct 30, 2024
1 parent ec6ad01 commit 73c6ef5
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ class ExpressionSuggesterSpec
)
}

test("should suggest parameters for casts methods") {
test("should suggest parameters for casts/conversions methods on unknown") {
spelSuggestionsFor("#unknown.to('')", column = 13) should contain theSameElementsAs List(
suggestion("Duration", Typed[Duration]),
suggestion("LocalDateTime", Typed[LocalDateTime]),
Expand All @@ -790,15 +790,43 @@ class ExpressionSuggesterSpec
suggestion("Util", Typed[Util]),
suggestion("WithList", Typed[WithList]),
suggestion("BigDecimal", Typed[java.math.BigDecimal]),
suggestion("BigInteger", Typed[java.math.BigInteger]),
suggestion("Boolean", Typed[java.lang.Boolean]),
suggestion("Double", Typed[java.lang.Double]),
suggestion("Float", Typed[java.lang.Float]),
suggestion("Long", Typed[java.lang.Long]),
suggestion("Integer", Typed[java.lang.Integer]),
suggestion("Short", Typed[java.lang.Short]),
suggestion("Byte", Typed[java.lang.Byte]),
suggestion("String", Typed[java.lang.String]),
suggestion("List", Typed.genericTypeClass[java.util.List[_]](List(Unknown))),
suggestion("Map", Typed.genericTypeClass[java.util.Map[_, _]](List(Unknown, Unknown))),
)
}

test("should suggest parameters for casts/conversions methods on string") {
spelSuggestionsFor("'11'.to('')", column = 9) should contain theSameElementsAs List(
suggestion("BigDecimal", Typed[java.math.BigDecimal]),
suggestion("BigInteger", Typed[java.math.BigInteger]),
suggestion("Boolean", Typed[java.lang.Boolean]),
suggestion("Double", Typed[java.lang.Double]),
suggestion("Float", Typed[java.lang.Float]),
suggestion("Long", Typed[java.lang.Long]),
suggestion("Integer", Typed[java.lang.Integer]),
suggestion("Short", Typed[java.lang.Short]),
suggestion("Byte", Typed[java.lang.Byte]),
)
}

test("should suggest parameters for casts/conversions methods on list to map") {
spelSuggestionsFor("{{key: 'a', value: 1}}.to('')", column = 27) should contain theSameElementsAs List(
suggestion(
"Map",
Typed.genericTypeClass[java.util.Map[_, _]](List(Typed.typedClass[String], Typed.typedClass[Integer]))
),
)
}

test("should suggest the same methods for list and array") {
val suggester = new ExpressionSuggester(
expressionConfig,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ class CastOrConversionExt(target: Any, classesBySimpleName: Map[String, Class[_]

def to(className: String): Any =
orElse(tryCast(className), tryConvert(className))
.getOrElse(throw new IllegalStateException(s"Cannot cast or convert value: $target to: '$className'"))
.getOrElse(throw new IllegalStateException(s"Cannot cast or convert value: $target to: '$className'"))

def toOrNull(className: String): Any =
orElse(tryCast(className), tryConvert(className))
.getOrElse(null)
.getOrElse(null)

private def tryCast(className: String): Either[Throwable, Any] = getClass(className) match {
case Some(clazz) => Try(clazz.cast(target)).toEither
Expand All @@ -43,17 +43,18 @@ class CastOrConversionExt(target: Any, classesBySimpleName: Map[String, Class[_]
// scala 2.12 does not support either.orElse
private def orElse(e1: Either[Throwable, Any], e2: => Either[Throwable, Any]): Either[Throwable, Any] =
e1 match {
case Left(_) => e2
case r@Right(_) => r
case Left(_) => e2
case r @ Right(_) => r
}

}

object CastOrConversionExt extends ExtensionMethodsHandler {
private val isMethodName = "is"
private val toMethodName = "to"
private val toOrNullMethodName = "toOrNull"
private val castOrToConversionMethods = Set(isMethodName, toMethodName, toOrNullMethodName)
private val stringClass = classOf[String]
private val isMethodName = "is"
private val toMethodName = "to"
private val toOrNullMethodName = "toOrNull"
private val castOrConversionMethods = Set(isMethodName, toMethodName, toOrNullMethodName)
private val stringClass = classOf[String]

private val conversionsRegistry: List[Conversion] = List(
ToLongConversionExt,
Expand All @@ -63,6 +64,11 @@ object CastOrConversionExt extends ExtensionMethodsHandler {
ToStringConversion,
ToMapConversionExt,
ToListConversionExt,
ToByteConversion,
ToShortConversion,
ToIntegerConversion,
ToFloatConversion,
ToBigIntegerConversion,
)

private val conversionsByType: Map[String, Conversion] = conversionsRegistry
Expand All @@ -72,8 +78,8 @@ object CastOrConversionExt extends ExtensionMethodsHandler {
override type ExtensionMethodInvocationTarget = CastOrConversionExt
override val invocationTargetClass: Class[CastOrConversionExt] = classOf[CastOrConversionExt]

def isCastOrToConversionMethod(methodName: String): Boolean =
castOrToConversionMethods.contains(methodName)
def isCastOrConversionMethod(methodName: String): Boolean =
castOrConversionMethods.contains(methodName)

def allowedConversions(clazz: Class[_]): List[Conversion] = conversionsRegistry.filter(_.appliesToConversion(clazz))

Expand Down Expand Up @@ -128,22 +134,23 @@ object CastOrConversionExt extends ExtensionMethodsHandler {
).groupBy(_.name)

private def convertToTyping(allowedClasses: Map[Class[_], TypingResult])(
invocationTarget: TypingResult,
arguments: List[TypingResult]
invocationTarget: TypingResult,
arguments: List[TypingResult]
): ValidatedNel[GenericFunctionTypingError, typing.TypingResult] = arguments match {
case TypedObjectWithValue(_, clazzName: String) :: Nil => allowedClasses
.find(_._1.equalsScalaClassNameIgnoringCase(clazzName))
.map(_._2.validNel)
.orElse(getConversion(clazzName).map(_.typingFunction(invocationTarget)).toOption) match {
case TypedObjectWithValue(_, clazzName: String) :: Nil =>
allowedClasses
.find(_._1.equalsScalaClassNameIgnoringCase(clazzName))
.map(_._2.validNel)
.orElse(getConversion(clazzName).map(_.typingFunction(invocationTarget)).toOption) match {
case Some(result) => result
case _ => GenericFunctionTypingError.OtherError(s"Cannot cast or convert to: '$clazzName'").invalidNel
}
case _ => GenericFunctionTypingError.ArgumentTypeError.invalidNel
}

private def canConvertToTyping(allowedClasses: Map[Class[_], TypingResult])(
invocationTarget: TypingResult,
arguments: List[TypingResult]
invocationTarget: TypingResult,
arguments: List[TypingResult]
): ValidatedNel[GenericFunctionTypingError, TypingResult] =
convertToTyping(allowedClasses)(invocationTarget, arguments).map(_ => Typed.typedClass[Boolean])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ package pl.touk.nussknacker.engine.extension

import cats.data.ValidatedNel
import cats.implicits.catsSyntaxValidatedId
import org.springframework.util.NumberUtils
import pl.touk.nussknacker.engine.api.generics.GenericFunctionTypingError
import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypingResult}
import pl.touk.nussknacker.engine.util.classes.Extensions.ClassExtensions

import java.lang.{Boolean => JBoolean, Number => JNumber}
import java.lang.{
Boolean => JBoolean,
Byte => JByte,
Float => JFloat,
Integer => JInteger,
Number => JNumber,
Short => JShort
}
import java.math.{BigDecimal => JBigDecimal, BigInteger => JBigInteger}
import java.util.{Collection => JCollection}
import scala.util.{Success, Try}
Expand Down Expand Up @@ -35,18 +43,9 @@ trait Conversion {
typingResult.validNel
}

trait NumericConversion extends Conversion {
private val numberClass = classOf[JNumber]
private val stringClass = classOf[String]
private val unknownClass = classOf[Object]

override def appliesToConversion(clazz: Class[_]): Boolean =
clazz != resultTypeClass && (clazz.isAOrChildOf(numberClass) || clazz == stringClass || clazz == unknownClass)
}

object Conversion {

private[extension] def toNumber(stringOrNumber: Any): JNumber = stringOrNumber match {
private[extension] def toNumberEither(stringOrNumber: Any): Either[Throwable, JNumber] = stringOrNumber match {
case s: CharSequence =>
val ss = s.toString
// we pick the narrowest type as possible to reduce the amount of memory and computations overheads
Expand All @@ -62,9 +61,9 @@ object Conversion {
.collectFirst { case Success(value) =>
value
}
.getOrElse(new JBigDecimal(ss))
.toRight(new IllegalArgumentException(s"Cannot convert: '$stringOrNumber' to Number"))

case n: JNumber => n
case n: JNumber => Right(n)
}

}
Expand All @@ -76,10 +75,85 @@ object ToStringConversion extends Conversion {
override def convertEither(value: Any): Either[Throwable, String] = Right(value.toString)
}

trait CollectionConversion extends Conversion {
trait ToCollectionConversion extends Conversion {
private val unknownClass = classOf[Object]
private val collectionClass = classOf[JCollection[_]]

override def appliesToConversion(clazz: Class[_]): Boolean =
clazz != resultTypeClass && (clazz.isAOrChildOf(collectionClass) || clazz == unknownClass || clazz.isArray)
}

trait ToNumericConversion extends Conversion {
private val numberClass = classOf[JNumber]
private val stringClass = classOf[String]
private val unknownClass = classOf[Object]

override def appliesToConversion(clazz: Class[_]): Boolean =
clazz != resultTypeClass && (clazz.isAOrChildOf(numberClass) || clazz == stringClass || clazz == unknownClass)
}

object ToByteConversion extends ToNumericConversion {
override type ResultType = JByte
override val resultTypeClass: Class[JByte] = classOf[JByte]

override def convertEither(value: Any): Either[Throwable, JByte] = value match {
case v: JByte => Right(v)
case v: JNumber => Try(NumberUtils.convertNumberToTargetClass(v, resultTypeClass)).toEither
case v: String => Conversion.toNumberEither(v).flatMap(convertEither)
case _ => Left(new IllegalArgumentException(s"Cannot convert: $value to Byte"))
}

}

object ToShortConversion extends ToNumericConversion {
override type ResultType = JShort
override val resultTypeClass: Class[JShort] = classOf[JShort]

override def convertEither(value: Any): Either[Throwable, JShort] = value match {
case v: JShort => Right(v)
case v: JNumber => Try(NumberUtils.convertNumberToTargetClass(v, resultTypeClass)).toEither
case v: String => Conversion.toNumberEither(v).flatMap(convertEither)
case _ => Left(new IllegalArgumentException(s"Cannot convert: $value to Short"))
}

}

object ToIntegerConversion extends ToNumericConversion {
override type ResultType = JInteger
override val resultTypeClass: Class[JInteger] = classOf[JInteger]

override def convertEither(value: Any): Either[Throwable, JInteger] = value match {
case v: JInteger => Right(v)
case v: JNumber => Try(NumberUtils.convertNumberToTargetClass(v, resultTypeClass)).toEither
case v: String => Conversion.toNumberEither(v).flatMap(convertEither)
case _ => Left(new IllegalArgumentException(s"Cannot convert: $value to Integer"))
}

}

object ToFloatConversion extends ToNumericConversion {
override type ResultType = JFloat
override val resultTypeClass: Class[JFloat] = classOf[JFloat]

override def convertEither(value: Any): Either[Throwable, JFloat] = value match {
case v: JFloat => Right(v)
case v: JNumber => Try(NumberUtils.convertNumberToTargetClass(v, resultTypeClass)).toEither
case v: String => Conversion.toNumberEither(v).flatMap(convertEither)
case _ => Left(new IllegalArgumentException(s"Cannot convert: $value to Float"))
}

}

object ToBigIntegerConversion extends ToNumericConversion {
override type ResultType = JBigInteger
override val resultTypeClass: Class[JBigInteger] = classOf[JBigInteger]

override def convertEither(value: Any): Either[Throwable, JBigInteger] = value match {
case v: JBigInteger => Right(v)
case v: JBigDecimal => Right(v.toBigInteger)
case v: JNumber => Try(NumberUtils.convertNumberToTargetClass(v, resultTypeClass)).toEither
case v: String => Conversion.toNumberEither(v).flatMap(convertEither)
case _ => Left(new IllegalArgumentException(s"Cannot convert: $value to BigInteger"))
}

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package pl.touk.nussknacker.engine.extension

import pl.touk.nussknacker.engine.api.generics.MethodTypeInfo
import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypingResult}
import pl.touk.nussknacker.engine.definition.clazz.{ClassDefinitionSet, MethodDefinition, StaticMethodDefinition}
import org.springframework.util.NumberUtils
import pl.touk.nussknacker.engine.api.typed.typing.Typed
import pl.touk.nussknacker.engine.definition.clazz.{ClassDefinitionSet, MethodDefinition}

import java.lang.{Boolean => JBoolean}
import java.math.{BigDecimal => JBigDecimal, BigInteger => JBigInteger}
Expand All @@ -15,9 +15,13 @@ class ToBigDecimalConversionExt(target: Any) {
def toBigDecimalOrNull(): JBigDecimal = ToBigDecimalConversionExt.convertOrNull(target)
}

object ToBigDecimalConversionExt extends ExtensionMethodsHandler with NumericConversion {
object ToBigDecimalConversionExt extends ToNumericConversionExt {
override type ExtensionMethodInvocationTarget = ToBigDecimalConversionExt
override val invocationTargetClass: Class[ToBigDecimalConversionExt] = classOf[ToBigDecimalConversionExt]
override type ResultType = JBigDecimal
override val resultTypeClass: Class[JBigDecimal] = classOf[JBigDecimal]

private val definitions = List(
override val definitions: Map[String, List[MethodDefinition]] = List(
definition(Typed.typedClass[JBoolean], "isBigDecimal", Some("Check whether can be convert to a BigDecimal")),
definition(
Typed.typedClass[JBigDecimal],
Expand All @@ -31,40 +35,18 @@ object ToBigDecimalConversionExt extends ExtensionMethodsHandler with NumericCon
),
).groupBy(_.name)

override type ExtensionMethodInvocationTarget = ToBigDecimalConversionExt
override val invocationTargetClass: Class[ToBigDecimalConversionExt] = classOf[ToBigDecimalConversionExt]

override def createConverter(
set: ClassDefinitionSet
): ToExtensionMethodInvocationTargetConverter[ToBigDecimalConversionExt] =
(target: Any) => new ToBigDecimalConversionExt(target)

override def extractDefinitions(clazz: Class[_], set: ClassDefinitionSet): Map[String, List[MethodDefinition]] = {
if (appliesToConversion(clazz)) {
definitions
} else {
Map.empty
}
}

override def appliesToClassInRuntime(clazz: Class[_]): Boolean = true

override type ResultType = JBigDecimal
override val resultTypeClass: Class[JBigDecimal] = classOf[JBigDecimal]

override def convertEither(value: Any): Either[Throwable, JBigDecimal] =
value match {
case v: JBigDecimal => Right(v)
case v: JBigInteger => Right(new JBigDecimal(v))
case v: Number => Try(NumberUtils.convertNumberToTargetClass(v, resultTypeClass)).toEither
case v: String => Try(new JBigDecimal(v)).toEither
case v: Number => Try(new JBigDecimal(v.toString)).toEither
case _ => Left(new IllegalArgumentException(s"Cannot convert: $value to BigDecimal"))
}

private def definition(result: TypingResult, methodName: String, desc: Option[String]) = StaticMethodDefinition(
signature = MethodTypeInfo.noArgTypeInfo(result),
name = methodName,
description = desc
)

}
Loading

0 comments on commit 73c6ef5

Please sign in to comment.