dynamic_entity_refactor_simplify: add doc for restconnector dynamicEntityProcess method, fix all found bugs, and refactor code

This commit is contained in:
shuang 2019-09-21 13:47:56 +08:00
parent d0c8e10d38
commit 74fc6f5bfc
10 changed files with 278 additions and 117 deletions

View File

@ -36,9 +36,10 @@ object ErrorMessages {
// DynamicEntity Exceptions (OBP-09XXX)
val DynamicEntityNotFoundByDynamicEntityId = "OBP-09001: DynamicEntity not found. Please specify a valid value for dynamic_entity_id."
val DynamicEntityEntityNameAlreadyExists = "OBP-09002: DynamicEntity's entityName already exists. Please specify a different value for entityName."
val DynamicEntityEntityNotExists = "OBP-09003: DynamicEntity not exists. Please check entityName."
val DynamicEntityNameAlreadyExists = "OBP-09002: DynamicEntity's entityName already exists. Please specify a different value for entityName."
val DynamicEntityNotExists = "OBP-09003: DynamicEntity not exists. Please check entityName."
val DynamicEntityMissArgument = "OBP-09004: DynamicEntity process related argument is missing."
val EntityNotFoundByEntityId = "OBP-09005: Entity not found. Please specify a valid value for entityId."
// General messages (OBP-10XXX)
val InvalidJsonFormat = "OBP-10001: Incorrect json format."

View File

@ -1413,7 +1413,7 @@ object NewStyle {
case false => {
val entityNameExists = existsDynamicEntity.isDefined
// validate whether entityName is exists
val errorMsg = s"$DynamicEntityEntityNameAlreadyExists current entityName is '${dynamicEntity.entityName}'."
val errorMsg = s"$DynamicEntityNameAlreadyExists current entityName is '${dynamicEntity.entityName}'."
Helper.booleanToFuture(errorMsg)(!entityNameExists).map { _ =>
DynamicEntityProvider.connectorMethodProvider.vend.createOrUpdate(dynamicEntity)
}

View File

@ -1,6 +1,5 @@
package code.api.v4_0_0
import code.api.ChargePolicy
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON._
import code.api.util.APIUtil._
import code.api.util.ApiRole._
@ -12,7 +11,8 @@ import code.api.util._
import code.api.v1_4_0.JSONFactory1_4_0.{ChallengeAnswerJSON, TransactionRequestAccountJsonV140}
import code.api.v2_1_0._
import code.api.v3_1_0.ListResult
import code.dynamicEntity.{DynamicEntityCommons, DynamicEntityDefinition}
import code.api.{APIFailureNewStyle, ChargePolicy}
import code.dynamicEntity.DynamicEntityCommons
import code.model.dataAccess.AuthUser
import code.model.toUserExtended
import code.transactionrequests.TransactionRequests.TransactionChallengeTypes._
@ -23,7 +23,7 @@ import com.github.dwickern.macros.NameOf.nameOf
import com.openbankproject.commons.model._
import com.openbankproject.commons.model.enums.DynamicEntityFieldType
import com.openbankproject.commons.model.enums.DynamicEntityOperation._
import net.liftweb.common.{Box, Failure, Full}
import net.liftweb.common.{Box, Full, ParamFailure}
import net.liftweb.http.rest.RestHelper
import net.liftweb.json.JsonAST.JValue
import net.liftweb.json.Serialization.write
@ -718,13 +718,6 @@ trait APIMethods400 {
}
}
private def validateDynamicEntityJson(metadataJson: JValue) = {
val jFields = metadataJson.asInstanceOf[JObject].obj
require(jFields.size == 1, "json format for create or update DynamicEntity is not correct, it should have a single key value for structure definition")
val JField(_, definition) = jFields.head
definition.extract[DynamicEntityDefinition]
}
resourceDocs += ResourceDoc(
createDynamicEntity,
implementedInApiVersion,
@ -761,17 +754,12 @@ trait APIMethods400 {
for {
(Full(u), callContext) <- authorizedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", u.userId, canCreateDynamicEntity, callContext)
failMsg = s"$InvalidJsonFormat The Json body should be the same structure as request body example."
postedData <- NewStyle.function.tryons(failMsg, 400, callContext) {
validateDynamicEntityJson(json)
val JField(entityName, _) = json.asInstanceOf[JObject].obj.head
DynamicEntityCommons(entityName, compactRender(json))
}
Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(postedData, callContext)
jsonObject = json.asInstanceOf[JObject]
dynamicEntity = DynamicEntityCommons(jsonObject, None)
Full(result) <- NewStyle.function.createOrUpdateDynamicEntity(dynamicEntity, callContext)
} yield {
val commonsData: DynamicEntityCommons = dynamicEntity
val commonsData: DynamicEntityCommons = result
(commonsData.jValue, HttpCode.`201`(callContext))
}
}
@ -815,18 +803,11 @@ trait APIMethods400 {
(Full(u), callContext) <- authorizedAccess(cc)
_ <- NewStyle.function.hasEntitlement("", u.userId, canUpdateDynamicEntity, callContext)
failMsg = s"$InvalidJsonFormat The Json body should be the same structure as request body example."
putData <- NewStyle.function.tryons(failMsg, 400, callContext) {
validateDynamicEntityJson(json)
val JField(name, _) = json.asInstanceOf[JObject].obj.head
DynamicEntityCommons(name, compactRender(json), Some(dynamicEntityId))
}
(_, _) <- NewStyle.function.getDynamicEntityById(dynamicEntityId, callContext)
Full(dynamicEntity) <- NewStyle.function.createOrUpdateDynamicEntity(putData, callContext)
jsonObject = json.asInstanceOf[JObject]
dynamicEntity = DynamicEntityCommons(jsonObject, Some(dynamicEntityId))
Full(result) <- NewStyle.function.createOrUpdateDynamicEntity(dynamicEntity, callContext)
} yield {
val commonsData: DynamicEntityCommons = dynamicEntity
val commonsData: DynamicEntityCommons = result
(commonsData.jValue, HttpCode.`200`(callContext))
}
}
@ -868,9 +849,11 @@ trait APIMethods400 {
}
}
}
private def unboxResult[T](box: Box[T]): T = {
if(box.isInstanceOf[Failure]) {
throw new Exception(box.asInstanceOf[Failure].msg)
if(box.isInstanceOf[ParamFailure[APIFailureNewStyle]]) {
fullBoxOrException[Any](box)
}
box.openOrThrowException("impossible error")
@ -880,7 +863,6 @@ trait APIMethods400 {
val listName = StringHelpers.snakify(English.plural(entityName))
for {
(box: Box[JArray], _) <- NewStyle.function.invokeDynamicConnector(GET_ALL, entityName, None, None, Some(cc))
// resultList = APIUtil.unboxFullOrFail(box, Some(cc))
resultList = unboxResult(box)
} yield {
import net.liftweb.json.JsonDSL._
@ -891,7 +873,6 @@ trait APIMethods400 {
case EntityName(entityName, id) JsonGet req => {cc =>
for {
(box: Box[JObject], _) <- NewStyle.function.invokeDynamicConnector(GET_ONE, entityName, None, Some(id), Some(cc))
// entity = APIUtil.unboxFullOrFail(box, Some(cc))
entity = unboxResult(box)
} yield {
(entity, HttpCode.`200`(Some(cc)))
@ -900,7 +881,6 @@ trait APIMethods400 {
case EntityName(entityName) :: Nil JsonPost json -> _ => {cc =>
for {
(box: Box[JObject], _) <- NewStyle.function.invokeDynamicConnector(CREATE, entityName, Some(json.asInstanceOf[JObject]), None, Some(cc))
// entity = APIUtil.unboxFullOrFail(box, Some(cc))
entity = unboxResult(box)
} yield {
(entity, HttpCode.`201`(Some(cc)))
@ -909,7 +889,6 @@ trait APIMethods400 {
case EntityName(entityName, id) JsonPut json -> _ => { cc =>
for {
(box: Box[JObject], _) <- NewStyle.function.invokeDynamicConnector(UPDATE, entityName, Some(json.asInstanceOf[JObject]), Some(id), Some(cc))
// entity = APIUtil.unboxFullOrFail(box, Some(cc))
entity = unboxResult(box)
} yield {
(entity, HttpCode.`200`(Some(cc)))
@ -918,7 +897,6 @@ trait APIMethods400 {
case EntityName(entityName, id) JsonDelete req => { cc =>
for {
(box: Box[JValue], _) <- NewStyle.function.invokeDynamicConnector(DELETE, entityName, None, Some(id), Some(cc))
// deleteResult = APIUtil.unboxFullOrFail(box, Some(cc))
deleteResult = unboxResult(box)
} yield {
(deleteResult, HttpCode.`200`(Some(cc)))

View File

@ -220,16 +220,8 @@ case class DynamicEntityInfo(definition: String, entityName: String) {
val fieldNameToType: Map[String, Class[_]] = fieldNameToTypeName
.mapValues(jsonTypeMap(_))
val requiredFieldNames: Set[String] = (entity \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s).toSet
val fields = result.obj.filter(it => fieldNameToType.keySet.contains(it.name))
def check(v: Boolean, msg: String) = if (!v) throw new RuntimeException(msg)
// if there are field type are not match the definitions, there must be bug.
fields.foreach(it => check(fieldNameToType(it.name).isInstance(it.value), s"""$InvalidJsonFormat "${it.name}" required type is "${fieldNameToTypeName(it.name)}"."""))
// if there are required field not presented, must be some bug.
requiredFieldNames.foreach(it => check(fields.exists(_.name == it), s"""$InvalidJsonFormat required field "$it" not presented."""))
(id, fields.exists(_.name == idName)) match {
case (Some(idValue), false) => JObject(JField(idName, JString(idValue)) :: fields)
case _ => JObject(fields)

View File

@ -17,6 +17,7 @@ import net.liftweb.json.JValue
import net.liftweb.json.JsonAST.JNothing
import org.apache.commons.lang3.StringUtils
import scala.annotation.tailrec
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.language.postfixOps
@ -55,16 +56,14 @@ object ConnectorEndpoints extends RestHelper{
val value = invokeMethod(methodSymbol, paramValues :_*)
// convert any to Future[(Box[_], Option[CallContext])] type
val futureValue: Future[(Box[_], Option[CallContext])] = toStandaredFuture(value)
val futureValue: Future[(Box[_], Option[CallContext])] = toStandardFuture(value)
for {
(Full(data), callContext) <- futureValue.map {it =>
APIUtil.fullBoxOrException(it._1 ~> APIFailureNewStyle("", 400, optionCC.map(_.toLight)))
it
}
(boxedData, _) <- futureValue
data = APIUtil.fullBoxOrException(boxedData ~> APIFailureNewStyle("", 400, optionCC.map(_.toLight)))
inboundAdapterCallContext = nameOf(InboundAdapterCallContext)
//convert first letter to small case
inboundAdapterCallContextKey = Character.toLowerCase(inboundAdapterCallContext.charAt(0)) + inboundAdapterCallContext.substring(1)
inboundAdapterCallContextKey = StringUtils.uncapitalize(inboundAdapterCallContext)
inboundAdapterCallContextValue = InboundAdapterCallContext(cc.correlationId)
} yield {
// NOTE: if any filed type is BigDecimal, it is can't be serialized by lift json
@ -184,7 +183,8 @@ object ConnectorEndpoints extends RestHelper{
mirrorObj.reflectMethod(method).apply(args :_*)
}
def toStandaredFuture(obj: Any): Future[(Box[_], Option[CallContext])] = {
@tailrec
def toStandardFuture(obj: Any): Future[(Box[_], Option[CallContext])] = {
obj match {
case null => Future((Empty, None))
case future: Future[_] => {
@ -202,10 +202,10 @@ object ConnectorEndpoints extends RestHelper{
}
case Full(data) => {
data match {
case _: (_, _) => toStandaredFuture(Future(obj))
case _: (_, _) => toStandardFuture(Future(obj))
case _ => {
val fillCallContext = obj.asInstanceOf[Box[_]].map((_, None))
toStandaredFuture(Future(fillCallContext))
toStandardFuture(Future(fillCallContext))
}
}
}

View File

@ -23,7 +23,7 @@ import code.cards.MappedPhysicalCard
import code.context.{UserAuthContextProvider, UserAuthContextUpdateProvider}
import code.customer._
import code.customeraddress.CustomerAddressX
import code.dynamicEntity.DynamicEntityProvider
import code.dynamicEntity.{DynamicEntityProvider, DynamicEntityT}
import code.fx.{FXRate, MappedFXRate, fx}
import code.kycchecks.KycChecks
import code.kycdocuments.KycDocuments
@ -60,8 +60,7 @@ import com.openbankproject.commons.model.{AccountApplication, AccountAttribute,
import com.tesobe.CacheKeyFromArguments
import com.tesobe.model.UpdateBankAccount
import net.liftweb.common._
import net.liftweb.json
import net.liftweb.json.{JArray, JBool, JDouble, JInt, JObject, JString, JValue}
import net.liftweb.json.{JArray, JBool, JObject, JValue}
import net.liftweb.mapper.{By, _}
import net.liftweb.util.Helpers.{tryo, _}
import net.liftweb.util.Mailer
@ -2801,50 +2800,30 @@ object LocalMappedConnector extends Connector with MdcLoggable {
val dynamicEntityBox = DynamicEntityProvider.connectorMethodProvider.vend.getByEntityName(entityName)
// do validate, any validate process fail will return immediately
if(dynamicEntityBox.isEmpty) {
return Helper.booleanToFuture(s"$DynamicEntityEntityNotExists entity's name is '$entityName'")(dynamicEntityBox.isDefined)
return Helper.booleanToFuture(s"$DynamicEntityNotExists entity's name is '$entityName'")(false)
.map(it => (it.map(_.asInstanceOf[JValue]), callContext))
} else if(entityId.isDefined && !persistedEntities.contains(entityId.get -> entityName)) {
val id = entityId.get
val idName = StringUtils.uncapitalize(entityName) + "Id"
}
return Helper.booleanToFuture(s"$InvalidUrl not exists $entityName of $idName = $id")(false)
.map(it => (it.map(_.asInstanceOf[JValue]), callContext))
} else if(requestBody.isDefined) {
val dynamicEntity = dynamicEntityBox.openOrThrowException(DynamicEntityEntityNotExists)
val jsonTypeMap = Map[String, Class[_]](
("boolean", classOf[JBool]),
("string", classOf[JString]),
("array", classOf[JArray]),
("integer", classOf[JInt]),
("number", classOf[JDouble]),
)
val definitionJson = json.parse(dynamicEntity.metadataJson).asInstanceOf[JObject]
val entity = (definitionJson \ entityName).asInstanceOf[JObject]
val requiredFieldNames: Set[String] = (entity \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s).toSet
val fieldNameToTypeName: Map[String, String] = (entity \ "properties")
.asInstanceOf[JObject]
.obj
.map(field => (field.name, (field.value \ "type").asInstanceOf[JString].s))
.toMap
val fieldNameToType: Map[String, Class[_]] = fieldNameToTypeName
.mapValues(jsonTypeMap(_))
val bodyJson = requestBody.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument please supply the requestBody."))
val fields = bodyJson.obj.filter(it => fieldNameToType.keySet.contains(it.name))
// if there are field type are not match the definitions, there must be bug.
val invalidTypes = fields.filterNot(it => fieldNameToType(it.name).isInstance(it.value))
val invalidTypeNames = invalidTypes.map(_.name).mkString("[", ",", "]")
val missingRequiredFields = requiredFieldNames.filterNot(it => fields.exists(_.name == it))
val missingFieldNames = missingRequiredFields.mkString("[", ",", "]")
if(invalidTypes.nonEmpty) {
return Helper.booleanToFuture(s"$InvalidJsonFormat these field type not correct: $invalidTypeNames")(invalidTypes.isEmpty)
if(operation == CREATE || operation == UPDATE) {
if(requestBody.isEmpty) {
return Helper.booleanToFuture(s"$InvalidJsonFormat requestBody is required for $operation operation.")(false)
.map(it => (it.map(_.asInstanceOf[JValue]), callContext))
} else if(missingRequiredFields.nonEmpty) {
return Helper.booleanToFuture(s"$InvalidJsonFormat some required fields are missing: $missingFieldNames")(missingRequiredFields.isEmpty)
}
val dynamicEntity: DynamicEntityT = dynamicEntityBox.openOrThrowException(DynamicEntityNotExists)
val validateResult: Either[String, Unit] = dynamicEntity.validateEntityJson(requestBody.get)
if(validateResult.isLeft) {
return Helper.booleanToFuture(s"$InvalidJsonFormat details: ${validateResult.left.get}")(validateResult.isRight)
.map(it => (it.map(_.asInstanceOf[JValue]), callContext))
}
}
if(operation == GET_ONE || operation == UPDATE || operation == DELETE) {
if (entityId.isEmpty) {
return Helper.booleanToFuture(s"$InvalidJsonFormat entityId is required for $operation operation.")(entityId.isEmpty || StringUtils.isBlank(entityId.get))
.map(it => (it.map(_.asInstanceOf[JValue]), callContext))
}
if (!persistedEntities.contains(entityId.get -> entityName)) {
val id = entityId.get
return Helper.booleanToFuture(s"$EntityNotFoundByEntityId please check: entityId = $id", 404)(false)
.map(it => (it.map(_.asInstanceOf[JValue]), callContext))
}
}
@ -2860,7 +2839,8 @@ object LocalMappedConnector extends Connector with MdcLoggable {
}
case CREATE | UPDATE => {
val body = requestBody.getOrElse(throw new RuntimeException(s"$DynamicEntityMissArgument please supply the requestBody."))
val persistedEntity = MockerConnector.persist(entityName, body, entityId)
val id = if(operation == CREATE) None else entityId
val persistedEntity = MockerConnector.persist(entityName, body, id)
Full(persistedEntity)
}
case DELETE => {

View File

@ -9242,6 +9242,61 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable
}
//---------------- dynamic end ---------------------please don't modify this line
messageDocs += dynamicEntityProcessDoc
def dynamicEntityProcessDoc = MessageDoc(
process = "obp.dynamicEntityProcess",
messageFormat = messageFormat,
description = "operate commited dynamic entity data",
outboundTopic = None,
inboundTopic = None,
exampleOutboundMessage = (
OutBoundDynamicEntityProcessDoc(outboundAdapterCallContext= OutboundAdapterCallContext(correlationId=correlationIdExample.value,
sessionId=Some(sessionIdExample.value),
consumerId=Some(consumerIdExample.value),
generalContext=Some(List( BasicGeneralContext(key=keyExample.value,
value=valueExample.value))),
outboundAdapterAuthInfo=Some( OutboundAdapterAuthInfo(userId=Some(userIdExample.value),
username=Some(usernameExample.value),
linkedCustomers=Some(List( BasicLinkedCustomer(customerId=customerIdExample.value,
customerNumber=customerNumberExample.value,
legalName=legalNameExample.value))),
userAuthContext=Some(List( BasicUserAuthContext(key=keyExample.value,
value=valueExample.value))),
authViews=Some(List( AuthView(view= ViewBasic(id=viewIdExample.value,
name=viewNameExample.value,
description=viewDescriptionExample.value),
account= AccountBasic(id=accountIdExample.value,
accountRoutings=List( AccountRouting(scheme=accountRoutingSchemeExample.value,
address=accountRoutingAddressExample.value)),
customerOwners=List( InternalBasicCustomer(bankId=bankIdExample.value,
customerId=customerIdExample.value,
customerNumber=customerNumberExample.value,
legalName=legalNameExample.value,
dateOfBirth=parseDate(dateOfBirthExample.value).getOrElse(sys.error("dateOfBirthExample.value is not validate date format.")))),
userOwners=List( InternalBasicUser(userId=userIdExample.value,
emailAddress=emailExample.value,
name=usernameExample.value))))))))),
operation = DynamicEntityOperation.UPDATE,
entityName = "FooBar",
requestBody = Some(FooBar(name = "James Brown", number = 1234567890)),
entityId = Some("foobar-id-value"))
),
exampleInboundMessage = (
InBoundDynamicEntityProcessDoc(inboundAdapterCallContext= InboundAdapterCallContext(correlationId=correlationIdExample.value,
sessionId=Some(sessionIdExample.value),
generalContext=Some(List( BasicGeneralContext(key=keyExample.value,
value=valueExample.value)))),
status= Status(errorCode=statusErrorCodeExample.value,
backendMessages=List( InboundStatusMessage(source=sourceExample.value,
status=inboundStatusMessageStatusExample.value,
errorCode=inboundStatusMessageErrorCodeExample.value,
text=inboundStatusMessageTextExample.value))),
data=FooBar(name = "James Brown", number = 1234567890, fooBarId = Some("foobar-id-value")))
),
adapterImplementation = Some(AdapterImplementation("- Core", 1))
)
override def dynamicEntityProcess(operation: DynamicEntityOperation,
entityName: String,
requestBody: Option[JObject],
@ -9365,7 +9420,7 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable
val future: Future[Box[Box[T]]] = extractBody(entity) map { msg =>
tryo {
val errorMsg = parse(msg).extract[ErrorMessage]
val failure: Box[T] = ParamFailure(errorMsg.message, "")
val failure: Box[T] = ParamFailure("", APIFailureNewStyle(errorMsg.message, status.intValue()))
failure
} ~> APIFailureNewStyle(msg, status.intValue())
}

View File

@ -1,11 +1,11 @@
package code.dynamicEntity
import code.api.util.ErrorMessages.InvalidJsonFormat
import com.openbankproject.commons.model.enums.DynamicEntityFieldType
import com.openbankproject.commons.model.{Converter, JsonFieldReName}
import net.liftweb.common.Box
import net.liftweb.json.JsonAST.JString
import net.liftweb.json.JsonDSL._
import net.liftweb.json.{JField, JObject, JsonAST}
import net.liftweb.json.{JArray, JBool, JDouble, JField, JInt, JNothing, JNull, JObject, JString, compactRender, parse}
import net.liftweb.util.SimpleInjector
object DynamicEntityProvider extends SimpleInjector {
@ -19,13 +19,11 @@ trait DynamicEntityT {
def dynamicEntityId: Option[String]
def entityName: String
def metadataJson: String
}
case class DynamicEntityCommons(entityName: String,
metadataJson: String,
dynamicEntityId: Option[String] = None,
) extends DynamicEntityT with JsonFieldReName {
private val definition: JObject = net.liftweb.json.parse(metadataJson).asInstanceOf[JObject]
//---------util methods
private lazy val definition: JObject = parse(metadataJson).asInstanceOf[JObject]
//convert metadataJson to JValue, so the final json field metadataJson have no escaped " to \", have good readable
lazy val jValue = dynamicEntityId match {
case Some(id) => {
@ -35,18 +33,156 @@ case class DynamicEntityCommons(entityName: String,
}
case None => definition
}
/**
* validate the commit json whether fulfil DynamicEntity schema
* @param entityJson commit json object to add new instance of given dynamic entity
* @return return Success[Unit], or return Left[String] error message
*/
def validateEntityJson(entityJson: JObject): Either[String, Unit] = {
val required: List[String] = (definition \ entityName \ "required").asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s)
val missingProperties = required diff entityJson.obj.map(_.name)
if(missingProperties.nonEmpty) {
return Left(s"$InvalidJsonFormat The 'required' field's not be fulfilled, missing properties: ${missingProperties.mkString(", ")}")
}
val invalidPropertyMsg = (definition \ entityName \ "properties").asInstanceOf[JObject].obj
.map(it => {
val JField(propertyName, propertyDef: JObject) = it
val propertyTypeName = (propertyDef \ "type").asInstanceOf[JString].s
(propertyName, propertyTypeName)
})
.map(it => {
val (propertyName, propertyType) = it
val propertyValue = entityJson \ propertyName
propertyType match {
case _ if propertyValue == JNothing || propertyValue == JNull => "" // required properties already checked.
case "string" if !propertyValue.isInstanceOf[JString] => s"$InvalidJsonFormat The type of '$propertyName' should be string"
case "number" if !propertyValue.isInstanceOf[JDouble] => s"$InvalidJsonFormat The type of '$propertyName' should be number"
case "integer" if !propertyValue.isInstanceOf[JInt] => s"$InvalidJsonFormat The type of '$propertyName' should be integer"
case "boolean" if !propertyValue.isInstanceOf[JBool] => s"$InvalidJsonFormat The type of '$propertyName' should be boolean"
case "array" if !propertyValue.isInstanceOf[JArray] => s"$InvalidJsonFormat The type of '$propertyName' should be array"
case "object" if !propertyValue.isInstanceOf[JObject] => s"$InvalidJsonFormat The type of '$propertyName' should be object"
case _ => ""
}
})
.filter(_.nonEmpty)
.mkString("; ")
if(invalidPropertyMsg.nonEmpty) {
Left(invalidPropertyMsg)
} else {
Right(Unit)
}
}
}
case class DynamicEntityCommons(entityName: String,
metadataJson: String,
dynamicEntityId: Option[String] = None
) extends DynamicEntityT with JsonFieldReName
object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommons] {
/**
* create DynamicEntityCommons object, and do validation
*
* @param jsonObject the follow schema json:
* {{{
* {
* "FooBar": {
* "required": [
* "name"
* ],
* "properties": {
* "name": {
* "type": "string",
* "example": "James Brown"
* },
* "number": {
* "type": "integer",
* "example": "698761728934"
* }
* }
* }
* }
* }}}
* @param dynamicEntityId
* @return object of DynamicEntityCommons
*/
def apply(jsonObject: JObject, dynamicEntityId: Option[String]): DynamicEntityCommons = {
val fields = jsonObject.obj
// validate whether json is object and have a single field, currently support one entity definition
require(fields.nonEmpty, s"$InvalidJsonFormat The Json root object should have a single entity, but current have none.")
require(fields.size == 1, s"$InvalidJsonFormat The Json root object should have a single entity, but current entityNames: ${fields.map(_.name).mkString(", ")}")
val JField(entityName, metadataJson) = fields.head
// validate entityName corresponding value is json object
val metadataStr = compactRender(metadataJson)
require(metadataJson.isInstanceOf[JObject], s"$InvalidJsonFormat The $entityName should have an object value, but current value is: $metadataStr")
val required = metadataJson \ "required"
// validate 'required' field exists and is a json array[string]
require(required != JNothing , s"$InvalidJsonFormat There must be 'required' field in $entityName, and type is json array[string]")
require(required.isInstanceOf[JArray] && required.asInstanceOf[JArray].arr.forall(_.isInstanceOf[JString]), s"$InvalidJsonFormat The 'required' field's type of $entityName should be array[string]")
val properties = metadataJson \ "properties"
// validate 'properties' field exists and is json object
require(properties != JNothing , s"$InvalidJsonFormat There must be 'required' field in $entityName, and type is array[string]")
require(properties.isInstanceOf[JObject], s"$InvalidJsonFormat The 'properties' field's type of $entityName should be json object")
val propertiesObj = properties.asInstanceOf[JObject]
val requiredFields = required.asInstanceOf[JArray].arr.map(_.asInstanceOf[JString].s)
val allFields = propertiesObj.obj
// validate there is no required field missing in properties
val notFoundRequiredField = requiredFields.diff(allFields.map(_.name))
require(metadataJson.isInstanceOf[JObject], s"$InvalidJsonFormat In the $entityName, all 'required' fields should be present, these are missing: ${notFoundRequiredField.mkString(", ")}")
// validate all properties have a type and example
allFields.foreach(field => {
val JField(fieldName, value) = field
require(value.isInstanceOf[JObject], s"$InvalidJsonFormat The property of $fieldName's type should be json object")
// 'type' exists and value should be one of allowed type
val fieldType = value \ "type"
require(fieldType.isInstanceOf[JString] && fieldType.asInstanceOf[JString].s.nonEmpty, s"$InvalidJsonFormat The property of $fieldName's 'type' field should be exists and type is json string")
require(allowedFieldType.contains(fieldType.asInstanceOf[JString].s), s"$InvalidJsonFormat The property of $fieldName's 'type' field should be json string and value should be one of: ${allowedFieldType.mkString(", ")}")
// example is exists
val fieldExample = value \ "example"
require(fieldExample != JNothing, s"$InvalidJsonFormat The property of $fieldName's 'example' field should be exists")
})
DynamicEntityCommons(entityName, compactRender(jsonObject), dynamicEntityId)
}
private val allowedFieldType: Set[String] = Set(
"string",
"number",
"integer",
"boolean",
"array",
// "object",
)
}
/**
* an example schema of DynamicEntity, this is as request body example usage
* example case classes, as an example schema of DynamicEntity, for request body example usage
* @param FooBar
*/
case class DynamicEntityFooBar(FooBar: DynamicEntityDefinition, dynamicEntityId: Option[String] = None)
case class DynamicEntityDefinition(required: List[String],properties: DynamicEntityFullBarFields)
case class DynamicEntityFullBarFields(name: DynamicEntityTypeExample, number: DynamicEntityTypeExample)
case class DynamicEntityTypeExample(`type`: DynamicEntityFieldType, example: String)
object DynamicEntityCommons extends Converter[DynamicEntityT, DynamicEntityCommons]
//-------------------example case class end
trait DynamicEntityProvider {

View File

@ -1165,4 +1165,13 @@ case class OutBoundDynamicEntityProcess (outboundAdapterCallContext: OutboundAda
entityName: String,
requestBody: Option[JObject],
entityId: Option[String]) extends TopicTrait
case class InBoundDynamicEntityProcess (inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: JValue) extends InBoundTrait[JValue]
case class InBoundDynamicEntityProcess (inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: JValue) extends InBoundTrait[JValue]
// because swagger generate not support JValue type, so here supply too xxxDoc TO generate correct request and response body example
case class FooBar(name: String, number: Int, fooBarId: Option[String] = None)
case class OutBoundDynamicEntityProcessDoc (outboundAdapterCallContext: OutboundAdapterCallContext,
operation: DynamicEntityOperation,
entityName: String,
requestBody: Option[FooBar],
entityId: Option[String]) extends TopicTrait
case class InBoundDynamicEntityProcessDoc (inboundAdapterCallContext: InboundAdapterCallContext, status: Status, data: FooBar) extends InBoundTrait[FooBar]

View File

@ -1,6 +1,6 @@
package com.openbankproject.commons.util
import net.liftweb.common.{Box, Empty, Full}
import net.liftweb.common.{Box, Empty, Failure, Full}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.annotation.tailrec
@ -10,6 +10,7 @@ import scala.language.postfixOps
import scala.reflect.runtime.universe._
import scala.reflect.runtime.{universe => ru}
import scala.util.Success
import net.liftweb.json.JValue
object ReflectUtils {
private[this] val mirror: ru.Mirror = ru.runtimeMirror(getClass().getClassLoader)
@ -506,9 +507,18 @@ object ReflectUtils {
def toValueObject(t: Any): Any = {
t match {
case null => null
case v: JValue => v
case Some(v) => toValueObject(v)
case Full(v) => toValueObject(v)
case None|Empty => null
case v: Failure => v
case Left(v) => Left(toValueObject(v))
case v: Right[_, _] => v.map(toValueObject)
case v: Success[_]=> v.map(toValueObject)
case scala.util.Failure(v) => v
case it: Iterable[_] => it.map(toValueObject)
case array: Array[_] => array.map(toValueObject)
case v if(getType(v).typeSymbol.asClass.isCaseClass) => v
case v if getType(v).typeSymbol.asClass.isCaseClass => v
case other => {
val mirrorObj = mirror.reflect(other)
mirrorObj.symbol.info.decls