From 97cfa81492a99b027c8261bf5e51f07899ac55f7 Mon Sep 17 00:00:00 2001 From: "Malik, Junaid" Date: Mon, 22 Apr 2024 15:56:23 +0800 Subject: [PATCH] #1296 add TypeConverter class with simple tests and some default converters #1296 support for conversion from primitive to wrapper types for consistency #1296 add TypeConverterContainer and related tests Also some improvements and refactoring on previous changes #1296 improvements and refactoring on previous changes / TypeConverterContainer enhancements / TypeUtils introduced #1296 add 3 more default type converters + tests for DefaultTypeConverters --- vuu/.gitignore | 1 - .../DefaultTypeConverters.scala | 24 +++++++++ .../schema/typeConversion/TypeConverter.scala | 28 ++++++++++ .../TypeConverterContainer.scala | 47 ++++++++++++++++ .../schema/typeConversion/TypeUtils.scala | 20 +++++++ .../vuu/util/schema/SchemaMapperJavaTest.java | 14 +++++ .../util/schema/TypeConverterJavaTest.java | 43 +++++++++++++++ .../DefaultTypeConvertersTest.scala | 43 +++++++++++++++ .../TypeConverterContainerTest.scala | 54 +++++++++++++++++++ .../typeConversion/TypeConverterTest.scala | 29 ++++++++++ 10 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConverters.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverter.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainer.scala create mode 100644 vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeUtils.scala create mode 100644 vuu/src/test/java/org/finos/vuu/util/schema/SchemaMapperJavaTest.java create mode 100644 vuu/src/test/java/org/finos/vuu/util/schema/TypeConverterJavaTest.java create mode 100644 vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConvertersTest.scala create mode 100644 vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainerTest.scala create mode 100644 vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterTest.scala diff --git a/vuu/.gitignore b/vuu/.gitignore index 41bcf437f..8fd790ada 100644 --- a/vuu/.gitignore +++ b/vuu/.gitignore @@ -10,7 +10,6 @@ target/ .idea/vcs.xml .idea/workspace.xml src/run-config/ -src/test/java/ target target/** target/generated-test-sources/ diff --git a/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConverters.scala b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConverters.scala new file mode 100644 index 000000000..c3534447b --- /dev/null +++ b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConverters.scala @@ -0,0 +1,24 @@ +package org.finos.vuu.util.schema.typeConversion + +import java.lang._ + +object DefaultTypeConverters { + val stringToDoubleConverter: TypeConverter[String, Double] = TypeConverter(classOf[String], classOf[Double], withNullSafety[String, Double](_, _.toDouble)) + val stringToLongConverter: TypeConverter[String, Long] = TypeConverter[String, Long](classOf[String], classOf[Long], withNullSafety[String, Long](_, _.toLong)) + val stringToIntConverter: TypeConverter[String, Integer] = TypeConverter(classOf[String], classOf[Integer], withNullSafety[String, Integer](_, _.toInt)) + val intToStringConverter: TypeConverter[Integer, String] = TypeConverter(classOf[Integer], classOf[String], withNullSafety[Integer, String](_, _.toString)) + val longToStringConverter: TypeConverter[Long, String] = TypeConverter(classOf[Long], classOf[String], withNullSafety[Long, String](_, _.toString)) + val doubleToStringConverter: TypeConverter[Double, String] = TypeConverter(classOf[Double], classOf[String], withNullSafety[Double, String](_, _.toString)) + + + private def withNullSafety[T1, T2 >: Null](v: T1, fn: T1 => T2): T2 = Option(v).map(fn).orNull + + def getAll: List[TypeConverter[_, _]] = List( + stringToDoubleConverter, + doubleToStringConverter, + stringToLongConverter, + longToStringConverter, + stringToIntConverter, + intToStringConverter, + ) +} diff --git a/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverter.scala b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverter.scala new file mode 100644 index 000000000..12b352d6d --- /dev/null +++ b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverter.scala @@ -0,0 +1,28 @@ +package org.finos.vuu.util.schema.typeConversion + +import org.finos.vuu.util.schema.typeConversion.TypeConverter.buildConverterName +import org.finos.vuu.util.schema.typeConversion.TypeUtils.toWrapperType + +trait TypeConverter[From, To] { + val fromClass: Class[From] + val toClass: Class[To] + def convert(v: From): To + final def name: String = buildConverterName(fromClass, toClass) + override final def toString: String = s"[${this.name}]@${this.hashCode()}" +} + +object TypeConverter { + def apply[From, To](fromClass: Class[From], toClass: Class[To], converter: From => To): TypeConverter[From, To] = + new TypeConverterImpl(fromClass, toClass, converter) + + def buildConverterName(fromClass: Class[_], toClass: Class[_]): String = { + s"${toWrapperType(fromClass).getTypeName}->${toWrapperType(toClass).getTypeName}" + } +} + +private class TypeConverterImpl[From, To](override val fromClass: Class[From], + override val toClass: Class[To], + converter: From => To) extends TypeConverter[From, To] { + override def convert(v: From): To = converter(v) +} + diff --git a/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainer.scala b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainer.scala new file mode 100644 index 000000000..4aba4b748 --- /dev/null +++ b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainer.scala @@ -0,0 +1,47 @@ +package org.finos.vuu.util.schema.typeConversion + +trait TypeConverterContainer { + def convert[From, To](value: From, fromClass: Class[From], toClass: Class[To]): Option[To] + def typeConverter[From, To](name: String): Option[TypeConverter[From, To]] + def typeConverter[From, To](fromClass: Class[From], toClass: Class[To]): Option[TypeConverter[From, To]] +} + +private case class TypeConverterContainerImpl( + private val converters: List[TypeConverter[_, _]] + ) extends TypeConverterContainer { + private val typeConverterByName: Map[String, TypeConverter[_, _]] = converters.map(tc => (tc.name, tc)).toMap + + override def convert[From, To](value: From, fromClass: Class[From], toClass: Class[To]): Option[To] = { + if (TypeUtils.areTypesEqual(fromClass, toClass)) { + return Option(value.asInstanceOf[To]) + } + typeConverter[From, To](fromClass, toClass).map(_.convert(value)) + } + + override def typeConverter[From, To](name: String): Option[TypeConverter[From, To]] = { + typeConverterByName.get(name).map(_.asInstanceOf[TypeConverter[From, To]]) + } + + override def typeConverter[From, To](fromClass: Class[From], toClass: Class[To]): Option[TypeConverter[From, To]] = { + val name = TypeConverter.buildConverterName(fromClass, toClass) + typeConverter[From, To](name) + } +} + +object TypeConverterContainerBuilder { + def apply(): TypeConverterContainerBuilder = new TypeConverterContainerBuilder(List.empty, withDefaults = true) +} + +case class TypeConverterContainerBuilder private (private val converters: List[TypeConverter[_, _]], + private val withDefaults: Boolean) { + def withConverter[From, To](t: TypeConverter[From, To]): TypeConverterContainerBuilder = { + this.copy(converters = converters ++ List(t)) + } + + def withoutDefaults(): TypeConverterContainerBuilder = this.copy(withDefaults = false) + + def build(): TypeConverterContainer = { + val tcs = converters ++ (if (withDefaults) DefaultTypeConverters.getAll else List.empty) + TypeConverterContainerImpl(converters = tcs.distinctBy(_.name)) + } +} \ No newline at end of file diff --git a/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeUtils.scala b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeUtils.scala new file mode 100644 index 000000000..7535e88c1 --- /dev/null +++ b/vuu/src/main/scala/org/finos/vuu/util/schema/typeConversion/TypeUtils.scala @@ -0,0 +1,20 @@ +package org.finos.vuu.util.schema.typeConversion + +object TypeUtils { + def toWrapperType(t: Class[_]): Class[_] = if (t.isPrimitive) primitiveToWrapperType(t) else t + + private val primitiveToWrapperType: Map[Class[_], Class[_]] = Map( + classOf[Char] -> classOf[java.lang.Character], + classOf[Byte] -> classOf[java.lang.Byte], + classOf[Short] -> classOf[java.lang.Short], + classOf[Int] -> classOf[java.lang.Integer], + classOf[Long] -> classOf[java.lang.Long], + classOf[Float] -> classOf[java.lang.Float], + classOf[Double] -> classOf[java.lang.Double], + ) + + def areTypesEqual(fromClass: Class[_], toClass: Class[_]): Boolean = { + toWrapperType(fromClass).equals(toWrapperType(toClass)) + } + +} diff --git a/vuu/src/test/java/org/finos/vuu/util/schema/SchemaMapperJavaTest.java b/vuu/src/test/java/org/finos/vuu/util/schema/SchemaMapperJavaTest.java new file mode 100644 index 000000000..018ff106d --- /dev/null +++ b/vuu/src/test/java/org/finos/vuu/util/schema/SchemaMapperJavaTest.java @@ -0,0 +1,14 @@ +package org.finos.vuu.util.schema; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class SchemaMapperJavaTest { + + @Test + public void test() { +// TypeConverter typeConverter = Integer::parseInt; +// assertEquals(Integer.valueOf(20), typeConverter.apply("20")); + } +} diff --git a/vuu/src/test/java/org/finos/vuu/util/schema/TypeConverterJavaTest.java b/vuu/src/test/java/org/finos/vuu/util/schema/TypeConverterJavaTest.java new file mode 100644 index 000000000..0c8d3182f --- /dev/null +++ b/vuu/src/test/java/org/finos/vuu/util/schema/TypeConverterJavaTest.java @@ -0,0 +1,43 @@ +package org.finos.vuu.util.schema; + +import org.finos.vuu.util.schema.typeConversion.TypeConverter; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TypeConverterJavaTest { + + @Test + public void test_quick_instantiation_through_TypeConverter_apply() { + final var tc = TypeConverter.apply(String.class, Integer.class, Integer::parseInt); + + assertEquals(Integer.valueOf(20), tc.convert("20")); + assertEquals(tc.name(), "java.lang.String->java.lang.Integer"); + } + + @Test + public void test_instantiation_through_interface_implementation() { + class MyTypeConverter implements TypeConverter { + + @Override + public Class fromClass() { + return String.class; + } + + @Override + public Class toClass() { + return double.class; + } + + @Override + public Double convert(String v) { + return Double.valueOf(v); + } + } + + final var tc = new MyTypeConverter(); + + assertEquals(Double.valueOf(20.56), tc.convert("20.56")); + assertEquals(tc.name(), "java.lang.String->java.lang.Double"); + } +} diff --git a/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConvertersTest.scala b/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConvertersTest.scala new file mode 100644 index 000000000..5ef525b54 --- /dev/null +++ b/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/DefaultTypeConvertersTest.scala @@ -0,0 +1,43 @@ +package org.finos.vuu.util.schema.typeConversion + +import org.finos.vuu.util.schema.typeConversion.DefaultTypeConverters._ +import org.scalatest.prop.TableDrivenPropertyChecks._ +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +class DefaultTypeConvertersTest extends AnyFeatureSpec with Matchers { + + Feature("Handle converting valid value to the given type") { + + forAll(Table( + ("title", "converter", "input", "expected output"), + ("String to Double", stringToDoubleConverter, "10.5", 10.5), + ("Double to String", doubleToStringConverter, 10.5, "10.5"), + ("String to Long", stringToLongConverter, "10000", 10_000L), + ("Long to String", longToStringConverter, 10_000L, "10000"), + ("Int to String", intToStringConverter, 10, "10"), + ("String to Int", stringToIntConverter, "10", 10), + ))((title, converter, input, expectedOutput) => { + Scenario(title) { + converter.asInstanceOf[TypeConverter[Any, Any]].convert(input) shouldEqual expectedOutput + } + }) + } + + Feature("Handle null inputs") { + forAll(Table( + ("title", "converter"), + ("String to Double", stringToDoubleConverter), + ("Double to String", doubleToStringConverter), + ("String to Long", stringToLongConverter), + ("Long to String", longToStringConverter), + ("Int to String", intToStringConverter), + ("String to Int", stringToIntConverter), + ))((title, converter) => { + Scenario(title) { + converter.convert(null) shouldEqual null + } + }) + } + +} diff --git a/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainerTest.scala b/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainerTest.scala new file mode 100644 index 000000000..64bb82823 --- /dev/null +++ b/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterContainerTest.scala @@ -0,0 +1,54 @@ +package org.finos.vuu.util.schema.typeConversion + +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +import java.math.BigDecimal + +class TypeConverterContainerTest extends AnyFeatureSpec with Matchers { + private val userDefinedConverter = TypeConverter[BigDecimal, Double](classOf[BigDecimal], classOf[Double], _.doubleValue()) + + Feature("Instantiation with both default and user-defined converters") { + val userDefinedConverterOverridesADefault = TypeConverter[String, Int](classOf[String], classOf[Int], _.toInt + 50) + val tcContainer = TypeConverterContainerBuilder() + .withConverter(userDefinedConverter) + .withConverter(userDefinedConverterOverridesADefault) + .build() + + Scenario("contains default converters") { + tcContainer.typeConverter(classOf[String], classOf[Long]).nonEmpty should be(true) + tcContainer.typeConverter(classOf[String], classOf[Double]).nonEmpty should be(true) + } + + Scenario("contains user defined converters") { + tcContainer.typeConverter(userDefinedConverter.fromClass, userDefinedConverter.toClass).nonEmpty should be(true) + tcContainer.typeConverter( + userDefinedConverterOverridesADefault.fromClass, userDefinedConverterOverridesADefault.toClass + ).nonEmpty shouldBe true + } + + Scenario("user defined overrides any defaults converters for the same types") { + val defaultConverter = DefaultTypeConverters.stringToIntConverter + + tcContainer.typeConverter(classOf[String], classOf[Int]).get should not equal defaultConverter + tcContainer.typeConverter(classOf[String], classOf[Int]).get should equal(userDefinedConverterOverridesADefault) + } + } + + Feature("Instantiation with only user-defined converters") { + val tcContainer = TypeConverterContainerBuilder() + .withoutDefaults() + .withConverter(userDefinedConverter) + .build() + + Scenario("contains no default converters") { + val defaultConverters = DefaultTypeConverters.getAll + + defaultConverters.exists(tc => tcContainer.typeConverter(tc.name).nonEmpty) shouldBe false + } + + Scenario("contains added user defined converter") { + tcContainer.typeConverter(userDefinedConverter.name).nonEmpty shouldBe true + } + } +} diff --git a/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterTest.scala b/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterTest.scala new file mode 100644 index 000000000..36574b7fd --- /dev/null +++ b/vuu/src/test/scala/org/finos/vuu/util/schema/typeConversion/TypeConverterTest.scala @@ -0,0 +1,29 @@ +package org.finos.vuu.util.schema.typeConversion + +import org.scalatest.featurespec.AnyFeatureSpec +import org.scalatest.matchers.should.Matchers + +class TypeConverterTest extends AnyFeatureSpec with Matchers { + + Feature("TypeConverter") { + Scenario("quick instantiation through TypeConverter.apply") { + val tc: TypeConverter[Int, String] = TypeConverter(classOf[Int], classOf[String], _.toString) + + tc.convert(101) should equal("101") + tc.name should equal("java.lang.Integer->java.lang.String") + } + + Scenario("instantiation through trait implementation") { + class MyTypeConverter extends TypeConverter[Double, String] { + override val fromClass: Class[Double] = classOf[Double] + override val toClass: Class[String] = classOf[String] + override def convert(v: Double): String = v.toString + } + + val tc: TypeConverter[Double, String] = new MyTypeConverter() + + tc.convert(10.56) should equal("10.56") + tc.name should equal("java.lang.Double->java.lang.String") + } + } +}