Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type map literal values #3771

Merged
merged 3 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import cats.implicits.toTraverseOps
import io.circe.Encoder
import org.apache.commons.lang3.ClassUtils
import pl.touk.nussknacker.engine.api.typed.supertype.{CommonSupertypeFinder, NumberTypesPromotionStrategy, SupertypeClassResolutionStrategy}
import pl.touk.nussknacker.engine.api.typed.typing.Typed.fromInstance
import pl.touk.nussknacker.engine.api.util.{NotNothing, ReflectUtils}

import scala.reflect.ClassTag
import scala.reflect.runtime.universe._
import scala.collection.JavaConverters._
import scala.collection.immutable.ListMap
import scala.language.implicitConversions
import scala.reflect.ClassTag
import scala.reflect.runtime.universe._

object typing {

Expand Down Expand Up @@ -51,8 +52,9 @@ object typing {
def apply(fields: List[(String, TypingResult)], objType: TypedClass): TypedObjectTypingResult =
TypedObjectTypingResult(ListMap(fields: _*), objType)

def apply(fields: ListMap[String, TypingResult]): TypedObjectTypingResult =
TypedObjectTypingResult(fields, TypedClass(classOf[java.util.Map[_, _]], List(Typed[String], Unknown)))
def apply(fields: ListMap[String, TypingResult]): TypedObjectTypingResult = {
TypedObjectTypingResult(fields, stringMapWithValues[java.util.Map[_, _]](fields.toList))
}
}

// Warning, fields are kept in list-like map: 1) order is important 2) lookup has O(n) complexity
Expand Down Expand Up @@ -278,7 +280,7 @@ object typing {
TypedNull
case map: Map[String@unchecked, _] =>
val fieldTypes = typeMapFields(map)
TypedObjectTypingResult(fieldTypes, genericTypeClass(classOf[Map[_, _]], List(Typed[String], Unknown)))
TypedObjectTypingResult(fieldTypes, stringMapWithValues[Map[_, _]](fieldTypes))
case javaMap: java.util.Map[String@unchecked, _] =>
val fieldTypes = typeMapFields(javaMap.asScala.toMap)
TypedObjectTypingResult(fieldTypes)
Expand All @@ -302,14 +304,6 @@ object typing {
case (k, v) => k -> fromInstance(v)
}.toList

private def supertypeOfElementTypes(list: List[_]): TypingResult = {
implicit val numberTypesPromotionStrategy: NumberTypesPromotionStrategy = NumberTypesPromotionStrategy.ToSupertype
val superTypeFinder = new CommonSupertypeFinder(SupertypeClassResolutionStrategy.AnySuperclass, true)
list.map(fromInstance)
.reduceOption(superTypeFinder.commonSupertype(_, _)(NumberTypesPromotionStrategy.ToSupertype))
.getOrElse(Unknown)
}

def apply(possibleTypes: TypingResult*): TypingResult = {
apply(possibleTypes.toSet)
}
Expand All @@ -335,6 +329,20 @@ object typing {

}

private def stringMapWithValues[T: ClassTag](fields: List[(String, TypingResult)]): TypedClass = {
val valueType = superTypeOfTypes(fields.map(_._2))
Typed.genericTypeClass[T](List(Typed[String], valueType))
}

private def supertypeOfElementTypes(list: List[_]): TypingResult = {
superTypeOfTypes(list.map(fromInstance))
}

private def superTypeOfTypes(list: List[TypingResult]) = {
val superTypeFinder = new CommonSupertypeFinder(SupertypeClassResolutionStrategy.AnySuperclass, true)
list.reduceOption(superTypeFinder.commonSupertype(_, _)(NumberTypesPromotionStrategy.ToSupertype)).getOrElse(Unknown)
}

object AdditionalDataValue {

implicit def string(value: String): AdditionalDataValue = StringValue(value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class TypingResultErrorMessagesSpec extends AnyFunSuite with Matchers with Optio
canBeSubclassOf(
typeMap("field1" -> list(typeMap("field2a" -> Typed[String], "field3" -> Typed[Int]))),
typeMap("field1" -> list(typeMap("field2" -> Typed[String])))
) shouldBe NonEmptyList.of("Field 'field1' is of the wrong type. Expected: List[{field2a: String, field3: Integer}], actual: List[{field2: String}]").invalid
) shouldBe NonEmptyList.of("Map[String,List[{field2a: String, field3: Integer}]] cannot be converted to Map[String,List[{field2: String}]]").invalid
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ class RequestResponseInterpreterSpec extends AnyFunSuite with Matchers with Pati
result shouldBe Valid(List("abcd withRandomString"))
}


test("recognizes output types") {

val process = ScenarioBuilder
Expand All @@ -217,7 +218,8 @@ class RequestResponseInterpreterSpec extends AnyFunSuite with Matchers with Pati


val interpreter2 = prepareInterpreter(process = process2)
interpreter2.sinkTypes shouldBe Map(NodeId("endNodeIID") -> TypedObjectTypingResult(ListMap("str" -> Typed[String], "int" -> Typed.fromInstance(15))))
interpreter2.sinkTypes shouldBe Map(NodeId("endNodeIID") -> TypedObjectTypingResult(
ListMap("str" -> Typed[String], "int" -> Typed.fromInstance(15))))

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ import pl.touk.nussknacker.engine.api.typed.supertype.{CommonSupertypeFinder, Nu
import pl.touk.nussknacker.engine.api.typed.typing._
import pl.touk.nussknacker.engine.dict.SpelDictTyper
import pl.touk.nussknacker.engine.expression.NullExpression
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperationError.{DynamicPropertyAccessError, IllegalIndexingOperation, IllegalProjectionSelectionError, IllegalPropertyAccessError, InvalidMethodReference}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperationError._
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ConstructionOfUnknown, NoPropertyError, NonReferenceError, UnresolvedReferenceError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.OperatorError.{BadOperatorConstructionError, DivisionByZeroError, EmptyOperatorError, OperatorMismatchTypeError, OperatorNonNumericError, OperatorNotComparableError, ModuloZeroError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.OperatorError._
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.PartTypeError
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.SelectionProjectionError.{IllegalProjectionError, IllegalSelectionError, IllegalSelectionTypeError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.TernaryOperatorError.{InvalidTernaryOperator, TernaryOperatorMismatchTypesError, TernaryOperatorNotBooleanError}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -938,17 +938,13 @@ class ProcessValidatorSpec extends AnyFunSuite with Matchers with Inside {
.enricher("service-2", "output2", "withCustomValidation",
"age" -> "30",
"fields" -> "{invalid: 'yes'}")
.enricher("service-3", "output3", "withCustomValidation",
"age" -> "30",
"fields" -> "{name: 12}")
.buildSimpleVariable("result-id2", "result", "''").emptySink("end-id2", "sink")

val result = validateWithDef(process, withServiceRef)

result.result shouldBe Invalid(NonEmptyList.of(
CustomNodeError("service-1", "Too young", Some("age")),
CustomNodeError("service-2", "Service is invalid", None),
CustomNodeError("service-3", "All values should be strings", Some("fields"))
))
}

Expand Down Expand Up @@ -1310,8 +1306,6 @@ class ProcessValidatorSpec extends AnyFunSuite with Matchers with Inside {
fields.returnType match {
case TypedObjectTypingResult(fields, _, _) if fields.contains("invalid") =>
Validated.invalidNel(CustomNodeError("Service is invalid", None))
case TypedObjectTypingResult(fields, _, _) if fields.values.exists(_ != Typed.typedClass[String]) =>
Validated.invalidNel(CustomNodeError("All values should be strings", Some("fields")))
case _ => Valid(Typed.typedClass[String])
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ import pl.touk.nussknacker.engine.api.typed.typing.{Typed, TypedNull, TypedObjec
import pl.touk.nussknacker.engine.api.{Context, NodeId, SpelExpressionExcludeList}
import pl.touk.nussknacker.engine.definition.TypeInfos.ClazzDefinition
import pl.touk.nussknacker.engine.dict.SimpleDictRegistry
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.{ArgumentTypeError, ExpressionTypeError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperationError.{InvalidMethodReference, TypeReferenceError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{UnknownClassError, UnknownMethodError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.OperatorError.{DivisionByZeroError, OperatorMismatchTypeError, OperatorNonNumericError, ModuloZeroError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.OperatorError.{DivisionByZeroError, ModuloZeroError, OperatorMismatchTypeError, OperatorNonNumericError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.{ArgumentTypeError, ExpressionTypeError}
import pl.touk.nussknacker.engine.spel.SpelExpressionParser.{Flavour, Standard}
import pl.touk.nussknacker.engine.spel.internal.DefaultSpelConversionsProvider
import pl.touk.nussknacker.engine.types.{GeneratedAvroClass, JavaClassWithVarargs}
Expand Down Expand Up @@ -93,11 +93,20 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD
staticMethodInvocationsChecking: Boolean = defaultStaticMethodInvocationsChecking,
methodExecutionForUnknownAllowed: Boolean = defaultMethodExecutionForUnknownAllowed,
dynamicPropertyAccessAllowed: Boolean = defaultDynamicPropertyAccessAllowed): ValidatedNel[ExpressionParseError, TypedExpression] = {
expressionParser(dictionaries, flavour, strictMethodsChecking, staticMethodInvocationsChecking, methodExecutionForUnknownAllowed, dynamicPropertyAccessAllowed).parse(expr, validationCtx, Typed.fromDetailedType[T])
}

private def expressionParser(dictionaries: Map[String, DictDefinition] = Map.empty,
flavour: Flavour = Standard,
strictMethodsChecking: Boolean = defaultStrictMethodsChecking,
staticMethodInvocationsChecking: Boolean = defaultStaticMethodInvocationsChecking,
methodExecutionForUnknownAllowed: Boolean = defaultMethodExecutionForUnknownAllowed,
dynamicPropertyAccessAllowed: Boolean = defaultDynamicPropertyAccessAllowed) = {
val imports = List(SampleValue.getClass.getPackage.getName)
SpelExpressionParser.default(getClass.getClassLoader, new SimpleDictRegistry(dictionaries), enableSpelForceCompile = true, strictTypeChecking = true,
imports, flavour, strictMethodsChecking = strictMethodsChecking, staticMethodInvocationsChecking = staticMethodInvocationsChecking, typeDefinitionSetWithCustomClasses,
methodExecutionForUnknownAllowed = methodExecutionForUnknownAllowed, dynamicPropertyAccessAllowed = dynamicPropertyAccessAllowed,
spelExpressionExcludeListWithCustomPatterns, DefaultSpelConversionsProvider.getConversionService)(ClassExtractionSettings.Default).parse(expr, validationCtx, Typed.fromDetailedType[T])
spelExpressionExcludeListWithCustomPatterns, DefaultSpelConversionsProvider.getConversionService)(ClassExtractionSettings.Default)
}

private def spelExpressionExcludeListWithCustomPatterns: SpelExpressionExcludeList = {
Expand Down Expand Up @@ -888,7 +897,7 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD
ScalaCheckDrivenPropertyChecks.forAll(oneOperandOp, positiveNumberGen)(checkOneOperand)
ScalaCheckDrivenPropertyChecks.forAll(twoOperandOp, anyNumberGen, anyNumberGen)(checkTwoOperands)
ScalaCheckDrivenPropertyChecks.forAll(twoOperandNonZeroOp, anyNumberGen, nonZeroNumberGen)(checkTwoOperands)
}
}

test("should calculate values of operators on strings") {
checkExpressionWithKnownResult("'a' + 1")
Expand All @@ -900,6 +909,17 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD
parse[Any]("1 / 0").invalidValue shouldBe NonEmptyList.one(DivisionByZeroError("(1 / 0)"))
parse[Any]("1 % 0").invalidValue shouldBe NonEmptyList.one(ModuloZeroError("(1 % 0)"))
}

test("should check map values") {
val parser = expressionParser()
val expected = Typed.genericTypeClass[java.util.Map[_, _]](List(Typed[String], TypedObjectTypingResult(List(("additional" -> Typed[String])))))
inside(parser.parse("""{"aField": {"additional": 1}}""", ValidationContext.empty, expected)) {
case Invalid(NonEmptyList(e: ExpressionTypeError, Nil)) =>
e.expected shouldBe expected
}
parser.parse("""{"aField": {"additional": "str"}}""", ValidationContext.empty, expected) shouldBe 'valid
}

}

case class SampleObject(list: java.util.List[SampleValue])
Expand Down