refactor, add missing unit test

This commit is contained in:
shuang 2019-09-07 21:46:37 +08:00
parent f3f155d507
commit 22aebe1426
8 changed files with 354 additions and 44 deletions

View File

@ -124,8 +124,6 @@
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.version}</artifactId>
<version>3.0.5</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-server -->
<dependency>

View File

@ -9429,37 +9429,28 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable
*/
private def convertId[T](obj: T, customerIdConverter: String=> String, accountIdConverter: String=> String): T = {
//1st: We must not convert when connector == mapped. this will ignore the implicitly_convert_ids props.
//2rd: if connector != mapped, we still need the `implicitly_convert_ids == true`
if(APIUtil.getPropsValue("connector","mapped") != "mapped" && APIUtil.getPropsAsBoolValue("implicitly_convert_ids",false)){
ReflectUtils.operateNestedValues(obj)(fieldMirror => {
val fieldValue = fieldMirror.get
val fieldSymbol: TermSymbol = fieldMirror.symbol
val fieldType: Type = fieldSymbol.info
val fieldName: String = fieldSymbol.name.toString.trim.toLowerCase
val ownerSymbol: Type = fieldSymbol.owner.asType.toType
if(fieldValue == null) {
// do nothing
} else if (ownerSymbol <:< typeOf[CustomerId] ||
(fieldName == "customerid" && fieldType =:= typeOf[String]) ||
(ownerSymbol <:< typeOf[Customer] && fieldName == "id" && fieldType =:= typeOf[String])
) {
val customerRef = customerIdConverter(fieldValue.asInstanceOf[String])
fieldMirror.set(customerRef)
} else if(ownerSymbol <:< typeOf[AccountId] ||
(fieldName == "accountid" && fieldType =:= typeOf[String]) ||
(ownerSymbol <:< typeOf[CoreAccount] && fieldName == "id" && fieldType =:= typeOf[String])||
(ownerSymbol <:< typeOf[AccountBalance] && fieldName == "id" && fieldType =:= typeOf[String])||
(ownerSymbol <:< typeOf[AccountHeld] && fieldName == "id" && fieldType =:= typeOf[String])
) {
val accountRef = accountIdConverter(fieldValue.asInstanceOf[String])
fieldMirror.set(accountRef)
}
})
obj
} else
obj
//2rd: if connector != mapped, we still need the `implicitly_convert_ids == true`
def isCustomerId(fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type) = {
ownerType =:= typeOf[CustomerId] ||
(fieldName.equalsIgnoreCase("customerId") && fieldType =:= typeOf[String]) ||
(ownerType <:< typeOf[Customer] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])
}
def isAccountId(fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type) = {
ownerType <:< typeOf[AccountId] ||
(fieldName.equalsIgnoreCase("accountId") && fieldType =:= typeOf[String])
(ownerType <:< typeOf[CoreAccount] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])||
(ownerType <:< typeOf[AccountBalance] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])||
(ownerType <:< typeOf[AccountHeld] && fieldName.equalsIgnoreCase("id") && fieldType =:= typeOf[String])
}
ReflectUtils.resetNestedFields(obj){
case (fieldName, fieldType, fieldValue: String, ownerType) if isCustomerId(fieldName, fieldType, fieldValue, ownerType) => customerIdConverter(fieldValue)
case (fieldName, fieldType, fieldValue: String, ownerType) if isAccountId(fieldName, fieldType, fieldValue, ownerType) => accountIdConverter(fieldValue)
}
obj
}
/**

View File

@ -13,6 +13,14 @@
<packaging>jar</packaging>
<name>Open Bank Project Commons</name>
<repositories>
<repository>
<id>artima</id>
<name>Artima Maven Repository</name>
<url>http://repo.artima.com/releases</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.liftweb</groupId>
@ -27,13 +35,23 @@
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.version}</artifactId>
<version>3.0.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalactic</groupId>
<artifactId>scalactic_${scala.version}</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${scala.version}</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
@ -47,6 +65,13 @@
<scalaCompatVersion>${scala.version}</scalaCompatVersion>
<recompileMode>incremental</recompileMode>
<useZincServer>true</useZincServer>
<compilerPlugins>
<compilerPlugin>
<groupId>com.artima.supersafe</groupId>
<artifactId>supersafe_${scala.compiler}</artifactId>
<version>1.1.8</version>
</compilerPlugin>
</compilerPlugins>
</configuration>
<executions>
<execution>

View File

@ -71,8 +71,6 @@ object OBPEnumeration {
def getValuesByClass[T <: EnumValue](clazz: Class[T]): List[T] = getEnumContainer(clazz).values
def getValues[T <: EnumValue: TypeTag]: List[T] = getEnumContainer(typeTag[T].tpe).values.map(_.asInstanceOf[T])
def withNameOption(tp: Type, name: String): Option[EnumValue] = getEnumContainer(tp).withNameOption(name).map(_.asInstanceOf[EnumValue])
def withNameOption[T <: EnumValue](clazz: Class[T], name: String): Option[T] = getEnumContainer(clazz).withNameOption(name)

View File

@ -56,14 +56,17 @@ object ReflectUtils {
def setField[T](obj: AnyRef, fieldName: String, fieldValue: T): T = operateField[T](obj, fieldName, _.set(fieldValue))
/**
* modify given instance nested values
* modify given instance nested fields value
* @param obj given instance to modify
* @param predicate check whether current field value need to modify
* @param fn modify function
* @param fn modify function, signature is (fieldName: String, fieldType: Type, fieldValue: Any, ownerType: Type): Any
* fn result is calculated new value
* @return modified instance
*
* @note be carefully, this method will modify immutable object state, before you call this method, you should very sure it is safe for your logic.
*/
def operateNestedValues(obj: Any, predicate: Any => Boolean = isObpObject)(fn: ru.FieldMirror => Unit): Any = {
val recurseCallback = operateNestedValues(_: Any, predicate)(fn)
def resetNestedFields(obj: Any, predicate: Any => Boolean = isObpObject)(fn: PartialFunction[(String, Type, Any, Type), Any]): Any = {
val recurseCallback = resetNestedFields(_: Any, predicate)(fn)
obj match {
case null | None | Empty | Nil => obj
case _: Unit => obj
@ -85,7 +88,7 @@ object ReflectUtils {
case v: Array[_] => v.map(recurseCallback)
case v: Map[_, _] => v.values.map(recurseCallback)
case v: Iterable[_] => v.map(recurseCallback)
case v if(!predicate(v)) => v
case v if !predicate(v) => v
case _ => {
val tp = this.getType(obj)
val instanceMirror: ru.InstanceMirror = mirror.reflect(obj)
@ -98,8 +101,18 @@ object ReflectUtils {
constructFieldNames.foreach(it => {
val fieldSymbol: ru.TermSymbol = getType(obj).member(ru.TermName(it)).asTerm.accessed.asTerm
val fieldMirror: ru.FieldMirror = instanceMirror.reflectField(fieldSymbol)
recurseCallback(fieldMirror.get)
fn(fieldMirror)
val fieldValue: Any = fieldMirror.get
recurseCallback(fieldValue)
//check whether field should modify, if PartialFunction check result is true, just modify it with new Value
val fieldName: String = it
val fieldType: Type = fieldSymbol.info
val ownerType: Type = fieldSymbol.owner.asType.toType
if(fn.isDefinedAt(fieldName, fieldType, fieldValue, ownerType)) {
val newValue = fn(fieldName, fieldType, fieldValue, ownerType)
fieldMirror.set(newValue)
}
})
obj
}

View File

@ -0,0 +1,241 @@
package com.openbankproject.commons.util
import com.openbankproject.commons.util.Color.Color
import com.openbankproject.commons.util.Shape.Shape
import org.scalatest._
import scala.reflect.runtime.universe._
import org.scalatest.Tag
// to show bad design of scala enumeration
object Shape extends Enumeration {
type Shape = Value
val Circle = Value
val Square = Value
val Other = Value
}
object Color extends Enumeration {
type Color = Value
val Red = Value
val Green = Value
val Other = Value
}
object OBPEnumTag extends Tag("OBPEnumeration")
/**
* just for demonstrate what problem of scala enumeration, so here just set to ignore
*/
@Ignore
class ScalaEnumerationTest extends FlatSpec with Matchers {
it should "legal to create two overloaded methods with parameter Shape and Color" taggedAs(OBPEnumTag) in {
// if remove the comment of process method, will can't compile
object OverloadTest{
def process(shape: Shape) = ???
// def process(Color: Color) = ???
}
}
it should "have compile warnings when match case is not exhaustive with enumeration" taggedAs(OBPEnumTag) in {
val shape: Shape = Shape.Other
shape match {
case Shape.Other => println("hi other")
}
}
"shape.isInstanceOf[Color] " should "return false" taggedAs(OBPEnumTag) in {
// the worst: confused type check
val shape: Shape = Shape.Other
// shape is not Color, isColor should be false
val isColor = shape.isInstanceOf[Color]
isColor shouldBe false
}
"shape cast to Color" should "throw ClassCastException" taggedAs(OBPEnumTag) in {
val shape: Shape = Shape.Other
// shape cast to Color should throw ClassCastException
a[ClassCastException] should be thrownBy {
val color: Color = shape.asInstanceOf[Color]
}
}
"if shape can be cast to Color, casted value" should "match Color value" taggedAs(OBPEnumTag) in {
val shape: Shape = Shape.Other
val color: Color = shape.asInstanceOf[Color]
val wrong: String = color match {
case Color.Other => "match Color#other"
case _ => "NOT MATCH"
}
color.toString shouldBe "Other"
wrong shouldBe "match Color#other"
}
}
// to demonstrate OBPEnumeration
sealed trait OBPShape extends EnumValue
object OBPShape extends OBPEnumeration[OBPShape]{
object Circle extends OBPShape
object Square extends OBPShape
object Other extends OBPShape
}
sealed trait OBPColor extends EnumValue
object OBPColor extends OBPEnumeration[OBPColor]{
object Red extends OBPColor
object Green extends OBPColor
object Other extends OBPColor
}
class OBPEnumerationTest extends FlatSpec with Matchers {
it should "legal to create two overloaded methods with parameter OBPShape and OBPColor" taggedAs(OBPEnumTag) in {
// first bad: can't overload for different enumeration
object OverloadTest{
def process(shape: OBPShape) = ???
def process(Color: OBPColor) = ???
}
}
it should "have compile warnings when match case is not exhaustive with enumeration" taggedAs(OBPEnumTag) in {
val shape: OBPShape = OBPShape.Other
shape match {
case OBPShape.Other => "hi other"
}
}
"shape.isInstanceOf[OBPColor] " should "return false" taggedAs(OBPEnumTag) in {
// the worst: confused type check
val shape: OBPShape = OBPShape.Other
// shape is not Color, isColor should be false
val isColor = shape.isInstanceOf[OBPColor]
isColor shouldBe false
}
"shape cast to Color" should "throw ClassCastException" taggedAs(OBPEnumTag) in {
val shape: OBPShape = OBPShape.Other
// shape cast to Color should throw ClassCastException
a [ClassCastException] should be thrownBy {
val color = shape.asInstanceOf[OBPColor]
}
}
"values" should "contains all values" taggedAs(OBPEnumTag) in {
OBPShape.values should contain only (OBPShape.Square, OBPShape.Circle, OBPShape.Other)
}
"example" should "be one value of OBPShape enumeration" taggedAs(OBPEnumTag) in {
OBPShape.example shouldBe a [OBPShape]
}
"nameToValue" should "be name map to value of OBPShape enumeration" taggedAs(OBPEnumTag) in {
OBPShape.nameToValue should contain theSameElementsAs Map("Square" -> OBPShape.Square, "Circle" -> OBPShape.Circle, "Other" -> OBPShape.Other)
}
it should "get a Some(v) value when call withNameOption to retrieve exists OBPShape value" taggedAs(OBPEnumTag) in {
OBPShape.withNameOption("Square") shouldBe Some(OBPShape.Square)
}
it should "get a None value when call withNameOption to retrieve not exists OBPShape value" taggedAs(OBPEnumTag) in {
OBPShape.withNameOption("NOT_EXISTS") shouldBe None
}
it should "get a value when call withName to retrieve exists OBPShape value" taggedAs(OBPEnumTag) in {
OBPShape.withName("Circle") shouldBe OBPShape.Circle
}
it should "throw NoSuchElementException when call withName to retrieve not exists OBPShape value" taggedAs(OBPEnumTag) in {
a [NoSuchElementException] should be thrownBy {
OBPShape.withName("NOT_EXISTS") shouldBe OBPShape.Circle
}
}
// the follow test is for OBPEnumeration utils functions
"call OBPEnumeration.getValuesByType with an OBPEnumeration type" should "get all values" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPColor]
OBPEnumeration.getValuesByType(unknownType) shouldBe(OBPColor.values)
}
"call OBPEnumeration.getValuesByClass with an OBPEnumeration type" should "get all values" taggedAs(OBPEnumTag) in {
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.getValuesByClass(unknownClazz) should be(OBPShape.values)
}
"call OBPEnumeration.withNameOption with an OBPEnumeration type OR class and exists name" should "get correct Some(v)" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPShape]
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withNameOption(unknownType, "Square") shouldBe Some(OBPShape.Square)
OBPEnumeration.withNameOption(unknownClazz, "Circle") shouldBe Some(OBPShape.Circle)
}
"call OBPEnumeration.withNameOption with an OBPEnumeration type OR class and not exists name" should "get correct None" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPShape]
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withNameOption(unknownType, "NOT_EXISTS") shouldBe empty
OBPEnumeration.withNameOption(unknownClazz, "NOT_EXISTS") shouldBe empty
}
"call OBPEnumeration.withName with an OBPEnumeration type OR class and exists name" should "get correct value" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPShape]
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withName(unknownType, "Square") shouldBe OBPShape.Square
OBPEnumeration.withName(unknownClazz, "Circle") shouldBe OBPShape.Circle
}
"call OBPEnumeration.withName with an OBPEnumeration type OR class and not exists name" should "throw NoSuchElementException" taggedAs(OBPEnumTag) in {
a [NoSuchElementException] should be thrownBy {
val unknownType = typeOf[OBPShape]
OBPEnumeration.withName(unknownType, "NOT_EXISTS")
}
a [NoSuchElementException] should be thrownBy {
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withName(unknownClazz, "NOT_EXISTS")
}
}
"call OBPEnumeration.withIndexOption with an OBPEnumeration type OR class and exists taggedAs(OBPEnumTag) index" should "get correct Some(v)" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPShape]
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withIndexOption(unknownType, 0) shouldBe Some(OBPShape.Circle)
OBPEnumeration.withIndexOption(unknownClazz, 1) shouldBe Some(OBPShape.Square)
}
"call OBPEnumeration.withIndexOption with an OBPEnumeration type OR class and not exists taggedAs(OBPEnumTag) index" should "get correct None" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPShape]
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withIndexOption(unknownType, 4) shouldBe empty
OBPEnumeration.withIndexOption(unknownClazz, -1) shouldBe empty
}
"call OBPEnumeration.withIndex with an OBPEnumeration type OR class and exists taggedAs(OBPEnumTag) index" should "get correct value" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPShape]
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withIndex(unknownType, 0) shouldBe OBPShape.Circle
OBPEnumeration.withIndex(unknownClazz, 1) shouldBe OBPShape.Square
}
"call OBPEnumeration.withIndex with an OBPEnumeration type OR class and not exists taggedAs(OBPEnumTag) index" should "throw NoSuchElementException" taggedAs(OBPEnumTag) in {
a [NoSuchElementException] should be thrownBy {
val unknownType = typeOf[OBPShape]
OBPEnumeration.withIndex(unknownType, 8)
}
a [NoSuchElementException] should be thrownBy {
val unknownClazz = classOf[OBPShape].asInstanceOf[Class[EnumValue]]
OBPEnumeration.withIndex(unknownClazz, -1)
}
}
"call OBPEnumeration.getExampleByType" should "get one of values" taggedAs(OBPEnumTag) in {
val unknownType = typeOf[OBPShape]
OBPEnumeration.getExampleByType(unknownType) shouldBe(OBPShape.Circle)
}
"call OBPEnumeration.getExampleByClass" should "get one of values" taggedAs(OBPEnumTag) in {
val unknownClazz = classOf[OBPColor].asInstanceOf[Class[EnumValue]]
OBPEnumeration.getExampleByClass(unknownClazz) shouldBe(OBPColor.Red)
}
}

View File

@ -0,0 +1,32 @@
package com.openbankproject.commons.util
import org.scalatest.{FlatSpec, Matchers}
import scala.reflect.runtime.universe._
import org.scalatest.Tag
class ReflectUtilsTest extends FlatSpec with Matchers {
object ReflectUtilsTag extends Tag("ReflectUtils")
case class Aperson(id: String, age: Int)
case class Agroup(manager: Aperson, id: Int, members: List[Aperson])
"when modify Apersion#id to append suffix" should "all the not null id be end with suffix" taggedAs(ReflectUtilsTag) in {
val members = List(Aperson(null, 10), Aperson("p1-id", 20), Aperson("p2-id", 3))
val group = Agroup(Aperson("m-id", 11), 3, members)
val someGroup = Some(group)
val idSuffix = "---END"
ReflectUtils.resetNestedFields(someGroup){
case (fieldName, fieldType, fieldValue: String, ownerType) if(fieldName == "id" && ownerType =:= typeOf[Aperson]) =>
fieldValue + idSuffix
}
group.manager.id should endWith (idSuffix)
group.id shouldBe(3)
group.members.head.id shouldBe null
group.members.lift(1).get.id should endWith (idSuffix)
group.members.lift(2).get.id should endWith (idSuffix)
}
}

12
pom.xml
View File

@ -81,6 +81,18 @@
<artifactId>lift-common_${scala.version}</artifactId>
<version>${lift.version}</version>
</dependency>
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_${scala.version}</artifactId>
<version>3.0.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.scalactic</groupId>
<artifactId>scalactic_${scala.version}</artifactId>
<version>3.0.8</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>