From 04eb93b48d254af1fc86a533f230bca1f29c144a Mon Sep 17 00:00:00 2001 From: shuang Date: Mon, 15 Feb 2021 20:39:42 +0800 Subject: [PATCH] feature/dynamic_resourceDoc: add unit test for deserialize dynamic case class, and fix some bugs. --- .../scala/code/util/DynamicUtilsTest.scala | 68 +++++++++++++++++++ .../commons/util/JsonUtils.scala | 51 ++++++-------- .../commons/util/ReflectUtils.scala | 20 +++++- .../commons/util/JsonUtilsTest.scala | 8 +-- 4 files changed, 112 insertions(+), 35 deletions(-) create mode 100644 obp-api/src/test/scala/code/util/DynamicUtilsTest.scala diff --git a/obp-api/src/test/scala/code/util/DynamicUtilsTest.scala b/obp-api/src/test/scala/code/util/DynamicUtilsTest.scala new file mode 100644 index 000000000..21d06e856 --- /dev/null +++ b/obp-api/src/test/scala/code/util/DynamicUtilsTest.scala @@ -0,0 +1,68 @@ +package code.util + +import code.api.util.DynamicUtil +import com.openbankproject.commons.util.{JsonUtils, ReflectUtils} +import net.liftweb.common.Box +import net.liftweb.json +import org.scalatest.{FlatSpec, Matchers, Tag} + +class DynamicUtilsTest extends FlatSpec with Matchers { + object DynamicUtilsTag extends Tag("DynamicUtils") + implicit val formats = code.api.util.CustomJsonFormats.formats + val zson = { + """ + |{ + | "name": "Sam", + | "age": [12], + | "isMarried": true, + | "weight": 12.11, + | "class": "2", + | "def": 12, + | "email": ["abc@def.com", "hijk@abc.com"], + | "address": [{ + | "name": "jieji", + | "code": 123123, + | "street":{"road": "gongbin", "number": 123}, + | "_optional_fields_": ["code"] + | }], + | "street": {"name": "hongqi", "width": 12.11}, + | "_optional_fields_": ["age", "weight", "address"] + |} + |""".stripMargin + } + val zson2 = """{"road": "gongbin", "number": 123}""" + + def buildFunction(jsonStr: String): String => Any = { + val caseClasses = JsonUtils.toCaseClasses(json.parse(jsonStr)) + + val code = + s""" + | $caseClasses + | + | // throws exception: net.liftweb.json.MappingException: + | //No usable value for name + | //Did not find value which can be converted into java.lang.String + | + |implicit val formats = code.api.util.CustomJsonFormats.formats + |(str: String) => { + | net.liftweb.json.parse(str).extract[RootJsonClass] + |} + |""".stripMargin + + val fun: Box[String => Any] = DynamicUtil.compileScalaCode(code) + fun.orNull + } + + "Parse json to dynamic case object" should "success" taggedAs DynamicUtilsTag in { + + val func = buildFunction(zson) + val func2 = buildFunction(zson2) + val value1 = func.apply(zson) + val value2 = func2.apply(zson2) + + ReflectUtils.getNestedField(value1.asInstanceOf[AnyRef], "street", "name") should be ("hongqi") + ReflectUtils.getField(value1.asInstanceOf[AnyRef], "weight") shouldEqual Some(12.11) + ReflectUtils.getField(value2.asInstanceOf[AnyRef], "number") shouldEqual (123) + + } +} \ No newline at end of file diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala index 38ac2f255..bd0a05bac 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/JsonUtils.scala @@ -672,30 +672,24 @@ object JsonUtils { private val optionalFieldName = "_optional_fields_" + // if Option[Boolean], Option[Double], Option[Long] will lost type argument when do deserialize with lift-json: Option[Object] + // this will make generated scala code can't extract json to this object, So use java.lang.Xxx for these type in Option. private def toScalaTypeName(jValue: JValue, isOptional: Boolean = false) = jValue match { - case _: JBool => if(isOptional) "Option[Boolean]" else "Boolean" - case _: JString => if(isOptional) "Option[String]" else "String" - case _: JDouble => if(isOptional) "Option[BigDecimal]" else "BigDecimal" - case _: JInt => if(isOptional) "Option[Long]" else "Long" - case _: JObject => if(isOptional) "Option[AnyRef]" else "AnyRef" - case _: JArray => if(isOptional) "Option[List[Any]]" else "List[Any]" + case _: JBool if isOptional => "Option[java.lang.Boolean]" + case _: JBool => "Boolean" + case _: JDouble if isOptional => "Option[java.lang.Double]" + case _: JDouble => "Double" + case _: JInt if isOptional => "Option[java.lang.Long]" + case _: JInt => "Long" + case _: JString if isOptional => "Option[String]" + case _: JString => "String" + case _: JObject if isOptional => "Option[AnyRef]" + case _: JObject => "AnyRef" + case _: JArray if isOptional => "Option[List[Any]]" + case _: JArray => "List[Any]" case null | JNull | JNothing => throw new IllegalArgumentException(s"Json value must not be null") } - private def getJArrayItemTypeName(values: List[JValue], fieldName: String): String = { - values match { - case JBool(_) :: _ => "List[Boolean]" - case JString(_) :: _ => "List[String]" - case JDouble(_) :: _ => "List[BigDecimal]" - case JInt(_) :: _ => "List[Long]" - case JObject(_) :: _ => s"List[${fieldName.capitalize}JsonClass]" - case JArray(arr) :: _ => - val itemType = getJArrayItemTypeName(arr, fieldName) - s"List[List[$itemType]]" - case (null | JNull | JNothing) :: _ => throw new IllegalArgumentException(s"Fields $fieldName have null item, items should not be null") - } - } - /** * validate any nested array type field have the same structure, and not empty, and have not null item * @param void @@ -775,7 +769,7 @@ object JsonUtils { jvalue match { case _: JBool => "type RootJsonClass = Boolean" case _: JString => "type RootJsonClass = String" - case _: JDouble => "type RootJsonClass = BigDecimal" + case _: JDouble => "type RootJsonClass = Double" case _: JInt => "type RootJsonClass = Long" case jObject: JObject => validateJArray(jObject) @@ -788,7 +782,7 @@ object JsonUtils { def buildArrayType(jArray: JArray):String = jArray.arr.head match { case _: JBool => "List[Boolean]" case _: JString => "List[String]" - case _: JDouble => "List[BigDecimal]" + case _: JDouble => "List[Double]" case _: JInt => "List[Long]" case v: JObject => val itemsType = jObjectToCaseClass(v, "RootItem") @@ -839,21 +833,20 @@ object JsonUtils { s"$escapedFieldsName: $fieldType" case JField(name, arr: JArray) if name != optionalFieldName => - + val isOption: Boolean = optionalFields.contains(name) def buildArrayType(jArray: JArray): String = jArray.arr.head match { - case _: JBool => "List[Boolean]" + case _: JBool => "List[java.lang.Boolean]" + case _: JDouble => "List[java.lang.Double]" + case _: JInt => "List[java.lang.Long]" case _: JString => "List[String]" - case _: JDouble => "List[BigDecimal]" - case _: JInt => "List[Long]" - case _: JObject => s"List[${toCaseClassName(name)}]" + case _: JObject => s"List[${toCaseClassName(name)}]" case v: JArray => val nestedItmType = buildArrayType(v) s"List[$nestedItmType]" } - val fieldType = if (optionalFields.contains(name)) s"Option[${buildArrayType(arr)}]" - else buildArrayType(arr) + val fieldType = if (isOption) s"Option[${buildArrayType(arr)}]" else buildArrayType(arr) val escapedFieldsName = reservedToEscaped.getOrElse(name, name) s"$escapedFieldsName: $fieldType" diff --git a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala index 32878b473..bda7df656 100644 --- a/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala +++ b/obp-commons/src/main/scala/com/openbankproject/commons/util/ReflectUtils.scala @@ -113,6 +113,20 @@ object ReflectUtils { * @return the field value of obj */ def getField(obj: AnyRef, fieldName: String): Any = operateField[Any](obj, fieldName)(Functions.doNothingFn) + /** + * get given object nested field value + * @param obj + * @param fieldName field name + * @return the field value of obj + */ + def getNestedField(obj: AnyRef, rootField: String, nestedFields: String*): Any = { + nestedFields.foldLeft(getField(obj, rootField)) { (parentObject, field) => + assert(parentObject != null, s"Can't read `$field` value from null.") + assert(parentObject.isInstanceOf[AnyRef], s"Value $parentObject must be AnyRef type.") + + getField(parentObject.asInstanceOf[AnyRef], field) + } + } /** * according object name get corresponding field value @@ -618,12 +632,14 @@ object ReflectUtils { tp.typeSymbol.isClass && !tp.typeSymbol.asClass.isTrait match { case false => Map.empty[String, ru.Type] case true => { - getPrimaryConstructor(tp) + import scala.collection.immutable.ListMap + val paramNameToTypeList = getPrimaryConstructor(tp) .paramLists .headOption .getOrElse(Nil) .map(it => (it.name.toString, it.info)) - .toMap + + ListMap(paramNameToTypeList:_*) } } /** diff --git a/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala b/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala index 88a0a2156..79b668639 100644 --- a/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala +++ b/obp-commons/src/test/scala/com/openbankproject/commons/util/JsonUtilsTest.scala @@ -61,7 +61,7 @@ class JsonUtilsTest extends FlatSpec with Matchers { | "code": 123123, | "street":{"road": "gongbin", "number": 123} | }], - | "street": {"name": "hongqi", "width": 12.11} + | "street": {"name": "hongqi", "width": 12.11}, | "_optional_fields_": ["age", "weight", "address"] |} |""".stripMargin @@ -69,8 +69,8 @@ class JsonUtilsTest extends FlatSpec with Matchers { val expectedCaseClass = """case class AddressStreetJsonClass(road: String, number: Long) |case class AddressJsonClass(name: String, code: Long, street: AddressStreetJsonClass) - |case class StreetJsonClass(name: String, width: BigDecimal) - |case class RootJsonClass(name: String, age: Option[Long], isMarried: Boolean, weight: Option[BigDecimal], `class`: String, `def`: Long, email: List[String], address: Option[List[AddressJsonClass]], street: StreetJsonClass)""".stripMargin + |case class StreetJsonClass(name: String, width: Double) + |case class RootJsonClass(name: String, age: Option[java.lang.Long], isMarried: Boolean, weight: Option[java.lang.Double], `class`: String, `def`: Long, email: List[String], address: Option[List[AddressJsonClass]], street: StreetJsonClass)""".stripMargin val generatedCaseClass = toCaseClass(zson) @@ -98,7 +98,7 @@ class JsonUtilsTest extends FlatSpec with Matchers { | "weight": 21.43 | } |]""".stripMargin - val expectedCaseClass = """case class RootItemJsonClass(name: String, weight: BigDecimal) + val expectedCaseClass = """case class RootItemJsonClass(name: String, weight: Double) | type RootJsonClass = List[RootItemJsonClass]""".stripMargin val generatedCaseClass = toCaseClass(listObjectJson)