mirror of
https://github.com/OpenBankProject/OBP-API.git
synced 2026-02-06 18:46:46 +00:00
feature/add_dynamic_endpoints_with_swagger: methodRouting can change request url, and url can have expression
This commit is contained in:
parent
9ac32017c0
commit
fd089a9396
@ -1922,9 +1922,9 @@ object NewStyle {
|
||||
}
|
||||
}
|
||||
}
|
||||
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]],
|
||||
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
|
||||
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = {
|
||||
RestConnector_vMar2019.dynamicEndpointProcess(url, jValue, method, params, callContext)
|
||||
Connector.connector.vend.dynamicEndpointProcess(url, jValue, method, params, pathParams, callContext)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -2866,9 +2866,9 @@ trait APIMethods400 {
|
||||
"GET",
|
||||
"/management/dynamic-endpoints",
|
||||
" Get DynamicEndpoints",
|
||||
s"""Get DynamicEndpoints.
|
||||
s"""
|
||||
|
|
||||
|Get DynamicEndpoints,
|
||||
|Get DynamicEndpoints.
|
||||
|
|
||||
|""",
|
||||
emptyObjectJson,
|
||||
@ -2936,12 +2936,12 @@ trait APIMethods400 {
|
||||
|
||||
|
||||
lazy val dynamicEndpoint: OBPEndpoint = {
|
||||
case DynamicReq(url, json, method, params, role) => { cc =>
|
||||
case DynamicReq(url, json, method, params, pathParams, role) => { cc =>
|
||||
for {
|
||||
(Full(u), callContext) <- authenticatedAccess(cc)
|
||||
_ <- NewStyle.function.hasEntitlement("", u.userId, role, callContext)
|
||||
|
||||
(box, _) <- NewStyle.function.dynamicEndpointProcess(url, json, method, params, callContext)
|
||||
(box, _) <- NewStyle.function.dynamicEndpointProcess(url, json, method, params, pathParams, callContext)
|
||||
} yield {
|
||||
box match {
|
||||
case Full(v) => (v, HttpCode.`200`(Some(cc)))
|
||||
|
||||
@ -70,12 +70,20 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
* extract request body, no matter GET, POST, PUT or DELETE method
|
||||
*/
|
||||
object DynamicReq extends JsonTest with JsonBody {
|
||||
|
||||
private val ExpressionRegx = """\{(.+?)\}""".r
|
||||
/**
|
||||
* unapply Request to (swagger url, json, http method, request parameters, role)
|
||||
* unapply Request to (request url, json, http method, request parameters, path parameters, role)
|
||||
* request url is current request url
|
||||
* json is request body
|
||||
* http method is request http method
|
||||
* request parameters is http request parameters
|
||||
* path parameters: /banks/{bankId}/users/{userId} bankId and userId corresponding key to value
|
||||
* role is current endpoint required entitlement
|
||||
* @param r HttpRequest
|
||||
* @return
|
||||
*/
|
||||
def unapply(r: Req): Option[(String, JValue, AkkaHttpMethod, Map[String, List[String]], ApiRole)] = {
|
||||
def unapply(r: Req): Option[(String, JValue, AkkaHttpMethod, Map[String, List[String]], Map[String, String], ApiRole)] = {
|
||||
val partPath = r.path.partPath
|
||||
if (!testResponse_?(r) || partPath.headOption != Option(urlPrefix))
|
||||
None
|
||||
@ -84,19 +92,29 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
val httpMethod = HttpMethod.valueOf(r.requestType.method)
|
||||
// url that match original swagger endpoint.
|
||||
val url = partPath.tail.mkString("/", "/", "")
|
||||
val foundDynamicEndpoint: Optional[(DynamicEndpointInfo, ResourceDoc)] = dynamicEndpointInfos.stream()
|
||||
.map[Option[(DynamicEndpointInfo, ResourceDoc)]](_.findDynamicEndpoint(httpMethod, url))
|
||||
val foundDynamicEndpoint: Optional[(DynamicEndpointInfo, ResourceDoc, String)] = dynamicEndpointInfos.stream()
|
||||
.map[Option[(DynamicEndpointInfo, ResourceDoc, String)]](_.findDynamicEndpoint(httpMethod, url))
|
||||
.filter(_.isDefined)
|
||||
.findFirst()
|
||||
.map(_.get)
|
||||
|
||||
foundDynamicEndpoint.asScala
|
||||
.flatMap[(String, JValue, AkkaHttpMethod, Map[String, List[String]], ApiRole)] { it =>
|
||||
val (dynamicEndpointInfo, doc) = it
|
||||
.flatMap[(String, JValue, AkkaHttpMethod, Map[String, List[String]], Map[String, String], ApiRole)] { it =>
|
||||
val (dynamicEndpointInfo, doc, originalUrl) = it
|
||||
|
||||
val pathParams: Map[String, String] = if(originalUrl == url) {
|
||||
Map.empty[String, String]
|
||||
} else {
|
||||
val tuples: Array[(String, String)] = StringUtils.split(originalUrl, "/").zip(partPath.tail)
|
||||
tuples.collect {
|
||||
case (ExpressionRegx(name), value) => name->value
|
||||
}.toMap
|
||||
}
|
||||
|
||||
val Some(role::_) = doc.roles
|
||||
body(r).toOption
|
||||
.orElse(Some(JNothing))
|
||||
.map(t => (dynamicEndpointInfo.targetUrl(url), t, akkaHttpMethod, r.params, role))
|
||||
.map(t => (dynamicEndpointInfo.targetUrl(url), t, akkaHttpMethod, r.params, pathParams, role))
|
||||
}
|
||||
|
||||
}
|
||||
@ -132,6 +150,12 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
private def swaggerToResourceDocs(openAPI: OpenAPI, id: String): DynamicEndpointInfo = {
|
||||
val tags: List[ResourceDocTag] = List(ApiTag.apiTagDynamicEndpoint, apiTagApi, apiTagNewStyle)
|
||||
|
||||
val serverUrl = {
|
||||
val servers = openAPI.getServers
|
||||
assert(!servers.isEmpty, s"swagger host is mandatory, but current swagger host is empty, id=$id")
|
||||
servers.get(0).getUrl
|
||||
}
|
||||
|
||||
val paths: mutable.Map[String, PathItem] = openAPI.getPaths.asScala
|
||||
def entitlementSuffix(path: String) = Math.abs(path.hashCode).toString.substring(0, 3) // to avoid different swagger have same entitlement
|
||||
val docs: mutable.Iterable[(ResourceDoc, String)] = for {
|
||||
@ -151,7 +175,34 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
.orElse(Option(op.getDescription))
|
||||
.filter(StringUtils.isNotBlank)
|
||||
.map(_.capitalize)
|
||||
.getOrElse(summary)
|
||||
.getOrElse(summary) +
|
||||
s"""
|
||||
|
|
||||
|MethodRouting settings example:
|
||||
|```
|
||||
|{
|
||||
| "is_bank_id_exact_match":false,
|
||||
| "method_name":"dynamicEndpointProcess",
|
||||
| "connector_name":"rest_vMar2019",
|
||||
| "bank_id_pattern":".*",
|
||||
| "parameters":[
|
||||
| {
|
||||
| "key":"url_pattern",
|
||||
| "value":"$serverUrl$path"
|
||||
| },
|
||||
| {
|
||||
| "key":"http_method",
|
||||
| "value":"$requestVerb"
|
||||
| }
|
||||
| {
|
||||
| "key":"url",
|
||||
| "value":"http://mydomain.com/xxx"
|
||||
| }
|
||||
| ]
|
||||
|}
|
||||
|```
|
||||
|
|
||||
|""".stripMargin
|
||||
val exampleRequestBody: Product = getRequestExample(openAPI, op.getRequestBody)
|
||||
val successResponseBody: Product = getResponseExample(openAPI, op.getResponses)
|
||||
val errorResponseBodies: List[String] = List(
|
||||
@ -169,7 +220,7 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
ApiRole.getOrCreateDynamicApiRole(roleName)
|
||||
))
|
||||
}
|
||||
val connectorMethods = Some(List(s"""dynamicEntityProcess: parameters contains {"key": "entityName", "value": "$summary"}""")) //TODO temp
|
||||
val connectorMethods = Some(List("dynamicEndpointProcess"))
|
||||
val doc = ResourceDoc(
|
||||
partialFunction,
|
||||
implementedInApiVersion,
|
||||
@ -189,18 +240,10 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
(doc, path)
|
||||
}
|
||||
|
||||
val serverUrl = {
|
||||
val servers = openAPI.getServers
|
||||
if(servers.isEmpty) {
|
||||
None
|
||||
} else {
|
||||
Some(servers.get(0).getUrl)
|
||||
}
|
||||
}
|
||||
DynamicEndpointInfo(id, docs, serverUrl)
|
||||
}
|
||||
|
||||
private val PathParamRegx = """\{(.+)\}""".r
|
||||
private val PathParamRegx = """\{(.+?)\}""".r
|
||||
private val WordBoundPattern = Pattern.compile("(?<=[a-z])(?=[A-Z])|-")
|
||||
|
||||
private def buildRequestUrl(path: String): String = {
|
||||
@ -363,7 +406,7 @@ object DynamicEndpointHelper extends RestHelper {
|
||||
* @param docsToUrl ResourceDoc to url that defined in swagger content
|
||||
* @param serverUrl base url that defined in swagger content
|
||||
*/
|
||||
case class DynamicEndpointInfo(id: String, docsToUrl: mutable.Iterable[(ResourceDoc, String)], serverUrl: Option[String]) {
|
||||
case class DynamicEndpointInfo(id: String, docsToUrl: mutable.Iterable[(ResourceDoc, String)], serverUrl: String) {
|
||||
val resourceDocs: mutable.Iterable[ResourceDoc] = docsToUrl.map(_._1)
|
||||
|
||||
private val existsUrlToMethod: mutable.Iterable[(HttpMethod, String, ResourceDoc)] =
|
||||
@ -373,14 +416,14 @@ case class DynamicEndpointInfo(id: String, docsToUrl: mutable.Iterable[(Resource
|
||||
(HttpMethod.valueOf(doc.requestVerb), path, doc)
|
||||
})
|
||||
|
||||
def findDynamicEndpoint(newMethod: HttpMethod, newUrl: String): Option[(DynamicEndpointInfo, ResourceDoc)] = existsUrlToMethod.find(it => {
|
||||
def findDynamicEndpoint(newMethod: HttpMethod, newUrl: String): Option[(DynamicEndpointInfo, ResourceDoc, String)] = existsUrlToMethod.find(it => {
|
||||
val (method, url, _) = it
|
||||
isSameUrl(newUrl, url) && newMethod == method
|
||||
}).map(this -> _._3)
|
||||
}).map(it => (this, it._3, it._2))
|
||||
|
||||
def existsEndpoint(newMethod: HttpMethod, newUrl: String): Boolean = findDynamicEndpoint(newMethod, newUrl).isDefined
|
||||
|
||||
def targetUrl(url: String): String = s"""${serverUrl.get}$url"""
|
||||
def targetUrl(url: String): String = s"""$serverUrl$url"""
|
||||
|
||||
/**
|
||||
* check whether two url is the same:
|
||||
|
||||
@ -59,6 +59,7 @@ import scala.concurrent.duration._
|
||||
import scala.math.{BigDecimal, BigInt}
|
||||
import scala.util.Random
|
||||
import scala.reflect.runtime.universe.{MethodSymbol, typeOf}
|
||||
import _root_.akka.http.scaladsl.model.HttpMethod
|
||||
|
||||
/*
|
||||
So we can switch between different sources of resources e.g.
|
||||
@ -2152,6 +2153,9 @@ trait Connector extends MdcLoggable {
|
||||
requestBody: Option[JObject],
|
||||
entityId: Option[String],
|
||||
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future{(Failure(setUnimplementedError), callContext)}
|
||||
|
||||
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
|
||||
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = Future{(Failure(setUnimplementedError), callContext)}
|
||||
|
||||
def createDirectDebit(bankId: String,
|
||||
accountId: String,
|
||||
|
||||
@ -2,6 +2,7 @@ package code.bankconnectors
|
||||
|
||||
import java.util.Date
|
||||
import java.util.UUID.randomUUID
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import code.DynamicData.DynamicDataProvider
|
||||
import code.DynamicEndpoint.{DynamicEndpointProvider, DynamicEndpointT}
|
||||
@ -80,6 +81,7 @@ import net.liftweb.util.Mailer.{From, PlainMailBodyType, Subject, To}
|
||||
import org.mindrot.jbcrypt.BCrypt
|
||||
import scalacache.ScalaCache
|
||||
import scalacache.guava.GuavaCache
|
||||
|
||||
import scala.collection.immutable.{List, Nil}
|
||||
import com.openbankproject.commons.ExecutionContext.Implicits.global
|
||||
|
||||
@ -88,6 +90,7 @@ import scala.language.postfixOps
|
||||
import scala.math.{BigDecimal, BigInt}
|
||||
import scala.util.Random
|
||||
|
||||
import _root_.akka.http.scaladsl.model.HttpMethod
|
||||
|
||||
object LocalMappedConnector extends Connector with MdcLoggable {
|
||||
|
||||
@ -3041,6 +3044,13 @@ object LocalMappedConnector extends Connector with MdcLoggable {
|
||||
}
|
||||
}
|
||||
|
||||
/* delegate to rest connector
|
||||
*/
|
||||
override def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
|
||||
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = {
|
||||
Connector.getConnectorInstance("rest_vMar2019").dynamicEndpointProcess(url,jValue, method, params, pathParams, callContext)
|
||||
}
|
||||
|
||||
override def createDirectDebit(bankId: String,
|
||||
accountId: String,
|
||||
customerId: String,
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package code
|
||||
|
||||
import java.lang.reflect.Method
|
||||
import java.util.regex.Pattern
|
||||
|
||||
import akka.http.scaladsl.model.HttpMethod
|
||||
import code.api.{APIFailureNewStyle, ApiVersionHolder}
|
||||
import code.api.util.{CallContext, NewStyle}
|
||||
import code.methodrouting.{MethodRouting, MethodRoutingT}
|
||||
@ -97,6 +99,18 @@ package object bankconnectors extends MdcLoggable {
|
||||
NewStyle.function.getMethodRoutings(Some(methodName))
|
||||
.find(_.parameters.exists(it => it.key == "entityName" && it.value == entityName))
|
||||
}
|
||||
case _ if methodName == "dynamicEndpointProcess" => {
|
||||
val Array(url: String, _, method: HttpMethod, _*) = args
|
||||
NewStyle.function.getMethodRoutings(Some(methodName))
|
||||
.find(routing => {
|
||||
routing.parameters.exists(it => it.key == "http_method" && it.value.equalsIgnoreCase(method.value)) &&
|
||||
routing.parameters.exists(it => it.key == "url")&&
|
||||
routing.parameters.exists(
|
||||
it => it.key == "url_pattern" &&
|
||||
(it.value == url || Pattern.compile(it.value).matcher(url).matches())
|
||||
)
|
||||
})
|
||||
}
|
||||
case None => NewStyle.function.getMethodRoutings(Some(methodName), Some(false))
|
||||
.find {routing =>
|
||||
val bankIdPattern = routing.bankIdPattern
|
||||
|
||||
@ -9327,16 +9327,52 @@ trait RestConnector_vMar2019 extends Connector with KafkaHelper with MdcLoggable
|
||||
result
|
||||
}
|
||||
|
||||
//TODO params process
|
||||
def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]],
|
||||
|
||||
override def dynamicEndpointProcess(url: String, jValue: JValue, method: HttpMethod, params: Map[String, List[String]], pathParams: Map[String, String],
|
||||
callContext: Option[CallContext]): OBPReturnType[Box[JValue]] = {
|
||||
val urlInMethodRouting: Option[String] = MethodRoutingHolder.methodRouting match {
|
||||
case _: EmptyBox => None
|
||||
case Full(routing) => routing.parameters.find(_.key == "url").map(_.value)
|
||||
}
|
||||
val targetUrl = urlInMethodRouting.getOrElse(url)
|
||||
val pathVariableRex = """\{:(.+?)\}""".r
|
||||
val targetUrl = urlInMethodRouting.map { urlInRouting =>
|
||||
val tuples: Iterator[(String, String)] = pathVariableRex.findAllMatchIn(urlInRouting).map{ regex =>
|
||||
val expression = regex.group(0)
|
||||
val paramName = regex.group(1)
|
||||
val paramValue =
|
||||
if(paramName.startsWith("body.")) {
|
||||
val path = StringUtils.substringAfter(paramName, "body.")
|
||||
val value = JsonUtils.getValueByPath(jValue, path)
|
||||
JsonUtils.toString(value)
|
||||
} else {
|
||||
pathParams.get(paramName)
|
||||
.orElse(params.get(paramName).flatMap(_.headOption)).getOrElse(throw new RuntimeException(s"param $paramName not exists."))
|
||||
}
|
||||
expression -> paramValue
|
||||
}
|
||||
|
||||
(urlInRouting /: tuples) {(pre, kv)=>
|
||||
pre.replace(kv._1, kv._2)
|
||||
}
|
||||
}.getOrElse(url)
|
||||
|
||||
val paramNameToValue = for {
|
||||
(name, values) <- params
|
||||
value <- values
|
||||
param = s"$name=$value"
|
||||
} yield param
|
||||
|
||||
val paramUrl: String =
|
||||
if(params.isEmpty){
|
||||
targetUrl
|
||||
} else if(targetUrl.contains("?")) {
|
||||
targetUrl + "&" + paramNameToValue.mkString("&")
|
||||
} else {
|
||||
targetUrl + "?" + paramNameToValue.mkString("&")
|
||||
}
|
||||
|
||||
val jsonToSend = if(jValue == JNothing) "" else compactRender(jValue)
|
||||
val request = prepareHttpRequest(url, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(callContext)
|
||||
val request = prepareHttpRequest(paramUrl, method, HttpProtocol("HTTP/1.1"), jsonToSend).withHeaders(callContext)
|
||||
logger.debug(s"RestConnector_vMar2019 request is : $request")
|
||||
val responseFuture = makeHttpRequest(request)
|
||||
|
||||
|
||||
@ -421,7 +421,7 @@ object JsonUtils {
|
||||
* @param pathExpress path, can be prefix by - or !, e.g: "-result.count" "!value.isDeleted"
|
||||
* @return given nested field value
|
||||
*/
|
||||
private def getValueByPath(jValue: JValue, pathExpress: String): JValue = {
|
||||
def getValueByPath(jValue: JValue, pathExpress: String): JValue = {
|
||||
pathExpress match {
|
||||
case str if str.trim == "$root" || str.trim.isEmpty => jValue // if path is "$root" or "", return whole original json
|
||||
case RegexBoolean(b) => JBool(b.toBoolean)
|
||||
@ -480,4 +480,14 @@ object JsonUtils {
|
||||
case v => v.values.toString == expectValue
|
||||
}
|
||||
}
|
||||
|
||||
def toString(jValue: JValue) = jValue match{
|
||||
case JString(s) => s
|
||||
case JInt(num) => num.toString()
|
||||
case JDouble(num) => num.toString()
|
||||
case JBool(b) => b.toString()
|
||||
case JNothing => ""
|
||||
case JNull => "null"
|
||||
case v => json.compactRender(v)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user