Skip to content

Commit

Permalink
finos#1296 add TypeConverter class with simple tests and some default…
Browse files Browse the repository at this point in the history
… converters

finos#1296 support for conversion from primitive to wrapper types for consistency

finos#1296 add TypeConverterContainer and related tests

Also some improvements and refactoring on previous changes

finos#1296 improvements and refactoring on previous changes / TypeConverterContainer enhancements / TypeUtils introduced

finos#1296 add 3 more default type converters + tests for DefaultTypeConverters
  • Loading branch information
junaidzm13 committed Apr 29, 2024
1 parent 3d8df5d commit 97cfa81
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 1 deletion.
1 change: 0 additions & 1 deletion vuu/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ target/
.idea/vcs.xml
.idea/workspace.xml
src/run-config/
src/test/java/
target
target/**
target/generated-test-sources/
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
}
Original file line number Diff line number Diff line change
@@ -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)
}

Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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))
}

}
Original file line number Diff line number Diff line change
@@ -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<String, Integer> typeConverter = Integer::parseInt;
// assertEquals(Integer.valueOf(20), typeConverter.apply("20"));
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Double> {

@Override
public Class<String> fromClass() {
return String.class;
}

@Override
public Class<Double> 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");
}
}
Original file line number Diff line number Diff line change
@@ -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
}
})
}

}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
}

0 comments on commit 97cfa81

Please sign in to comment.