diff options
7 files changed, 462 insertions, 186 deletions
@@ -72,7 +72,7 @@ Bootstrap progress bar component. # Package pl.treksoft.kvision.remote -A set of components for creating multiplatform automatic connectivity with backend servers. +A set of components for creating multiplatform automatic JSON-RPC connectivity with a backend server. # Package pl.treksoft.kvision.routing diff --git a/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/JsonRpc.kt b/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/JsonRpc.kt new file mode 100644 index 00000000..7953ea01 --- /dev/null +++ b/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/JsonRpc.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import kotlinx.serialization.Serializable + +@Serializable +data class JsonRpcRequest(val id: Int, val method: String, val params: List<String?>, val jsonrpc: String = "2.0") { + constructor() : this(0, "", listOf()) +} + +@Serializable +data class JsonRpcResponse( + val id: Int? = null, + val result: String? = null, + val error: String? = null, + val jsonrpc: String = "2.0" +) diff --git a/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt b/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt index 4a6be56b..b3db5d0e 100644 --- a/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt +++ b/kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt @@ -23,7 +23,20 @@ package pl.treksoft.kvision.remote import kotlinx.coroutines.experimental.Deferred -const val SERVICE_PREFIX = "/kv_service" +enum class RpcHttpMethod { + POST, + PUT, + DELETE, + OPTIONS +} + +enum class HttpMethod { + GET, + POST, + PUT, + DELETE, + OPTIONS +} /** * Multiplatform service manager. @@ -33,57 +46,84 @@ expect open class ServiceManager<out T>(service: T? = null) { * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ - protected inline fun <reified RET> bind(route: String, noinline function: T.(Request?) -> Deferred<RET>) + protected inline fun <reified RET> bind( + route: String, + noinline function: T.(Request?) -> Deferred<RET>, + method: RpcHttpMethod = RpcHttpMethod.POST, + prefix: String = "/" + ) /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected inline fun <reified PAR, reified RET> bind( route: String, - noinline function: T.(PAR, Request?) -> Deferred<RET> + noinline function: T.(PAR, Request?) -> Deferred<RET>, + method: RpcHttpMethod = RpcHttpMethod.POST, + prefix: String = "/" ) /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected inline fun <reified PAR1, reified PAR2, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, Request?) -> Deferred<RET>, + method: RpcHttpMethod = RpcHttpMethod.POST, + prefix: String = "/" ) /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, Request?) -> Deferred<RET>, + method: RpcHttpMethod = RpcHttpMethod.POST, + prefix: String = "/" ) /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, PAR4, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, PAR4, Request?) -> Deferred<RET>, + method: RpcHttpMethod = RpcHttpMethod.POST, + prefix: String = "/" ) /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified PAR5, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> Deferred<RET>, + method: RpcHttpMethod = RpcHttpMethod.POST, + prefix: String = "/" ) /** @@ -93,7 +133,7 @@ expect open class ServiceManager<out T>(service: T? = null) { fun applyRoutes(k: JoobyServer) /** - * Returns the list of defined bindings. + * Returns the map of defined paths. */ - fun getCalls(): Map<String, String> + fun getCalls(): Map<String, Pair<String, RpcHttpMethod>> } diff --git a/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt b/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt index 45a995ac..7b339621 100644 --- a/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt +++ b/kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt @@ -24,7 +24,9 @@ package pl.treksoft.kvision.remote import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import kotlinx.coroutines.experimental.Deferred import kotlinx.coroutines.experimental.runBlocking +import org.jooby.Response import org.jooby.Status +import org.slf4j.LoggerFactory /** * Multiplatform service manager. @@ -32,6 +34,10 @@ import org.jooby.Status @Suppress("EXPERIMENTAL_FEATURE_WARNING") actual open class ServiceManager<out T> actual constructor(val service: T?) { + companion object { + val LOG = LoggerFactory.getLogger(ServiceManager::class.java.name) + } + protected val routes: MutableList<JoobyServer.() -> Unit> = mutableListOf() val mapper = jacksonObjectMapper() @@ -39,24 +45,33 @@ actual open class ServiceManager<out T> actual constructor(val service: T?) { * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified RET> bind( route: String, - noinline function: T.(Request?) -> Deferred<RET> + noinline function: T.(Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { routes.add({ - post("$SERVICE_PREFIX/$route") { req, res -> + call(method, "$prefix$route") { req, res -> if (service != null) { + val jsonRpcRequest = req.body(JsonRpcRequest::class.java) try { - res.send(runBlocking { function.invoke(service, req).await() }) + val result = runBlocking { function.invoke(service, req).await() } + res.send( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) } catch (e: Exception) { - e.printStackTrace() - res.status(500).send(e.message ?: "Error") + LOG.error(e.message, e) + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = e.message ?: "Error")) } } else { - res.status(Status.BAD_REQUEST) + res.status(Status.SERVER_ERROR) } - } + }.invoke(this) }) } @@ -64,29 +79,38 @@ actual open class ServiceManager<out T> actual constructor(val service: T?) { * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR, reified RET> bind( route: String, - noinline function: T.(PAR, Request?) -> Deferred<RET> + noinline function: T.(PAR, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { routes.add({ - post("$SERVICE_PREFIX/$route") { req, res -> - val param = try { - req.body(PAR::class.java) - } catch (e: Exception) { - null as PAR - } + call(method, "$prefix$route") { req, res -> if (service != null) { - try { - res.send(runBlocking { function.invoke(service, param, req).await() }) - } catch (e: Exception) { - e.printStackTrace() - res.status(500).send(e.message ?: "Error") + val jsonRpcRequest = req.body(JsonRpcRequest::class.java) + if (jsonRpcRequest.params.size == 1) { + val param = getParameter<PAR>(jsonRpcRequest.params[0]) + try { + val result = runBlocking { function.invoke(service, param, req).await() } + res.send( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + } catch (e: Exception) { + LOG.error(e.message, e) + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = e.message ?: "Error")) + } + } else { + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters")) } } else { - res.status(Status.BAD_REQUEST) + res.status(Status.SERVER_ERROR) } - } + }.invoke(this) }) } @@ -94,28 +118,39 @@ actual open class ServiceManager<out T> actual constructor(val service: T?) { * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR1, reified PAR2, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { routes.add({ - post("$SERVICE_PREFIX/$route") { req, res -> - val str = req.body(String::class.java) - val tree = mapper.readTree(str) - if (tree.size() == 2 && service != null) { - val p1 = mapper.treeToValue(tree[0], PAR1::class.java) - val p2 = mapper.treeToValue(tree[1], PAR2::class.java) - try { - res.send(runBlocking { function.invoke(service, p1, p2, req).await() }) - } catch (e: Exception) { - e.printStackTrace() - res.status(500).send(e.message ?: "Error") + call(method, "$prefix$route") { req, res -> + if (service != null) { + val jsonRpcRequest = req.body(JsonRpcRequest::class.java) + if (jsonRpcRequest.params.size == 2) { + val param1 = getParameter<PAR1>(jsonRpcRequest.params[0]) + val param2 = getParameter<PAR2>(jsonRpcRequest.params[1]) + try { + val result = runBlocking { function.invoke(service, param1, param2, req).await() } + res.send( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + } catch (e: Exception) { + LOG.error(e.message, e) + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = e.message ?: "Error")) + } + } else { + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters")) } } else { - res.status(Status.BAD_REQUEST) + res.status(Status.SERVER_ERROR) } - } + }.invoke(this) }) } @@ -123,29 +158,40 @@ actual open class ServiceManager<out T> actual constructor(val service: T?) { * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { routes.add({ - post("$SERVICE_PREFIX/$route") { req, res -> - val str = req.body(String::class.java) - val tree = mapper.readTree(str) - if (tree.size() == 3 && service != null) { - val p1 = mapper.treeToValue(tree[0], PAR1::class.java) - val p2 = mapper.treeToValue(tree[1], PAR2::class.java) - val p3 = mapper.treeToValue(tree[2], PAR3::class.java) - try { - res.send(runBlocking { function.invoke(service, p1, p2, p3, req).await() }) - } catch (e: Exception) { - e.printStackTrace() - res.status(500).send(e.message ?: "Error") + call(method, "$prefix$route") { req, res -> + if (service != null) { + val jsonRpcRequest = req.body(JsonRpcRequest::class.java) + if (jsonRpcRequest.params.size == 3) { + val param1 = getParameter<PAR1>(jsonRpcRequest.params[0]) + val param2 = getParameter<PAR2>(jsonRpcRequest.params[1]) + val param3 = getParameter<PAR3>(jsonRpcRequest.params[2]) + try { + val result = runBlocking { function.invoke(service, param1, param2, param3, req).await() } + res.send( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + } catch (e: Exception) { + LOG.error(e.message, e) + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = e.message ?: "Error")) + } + } else { + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters")) } } else { - res.status(Status.BAD_REQUEST) + res.status(Status.SERVER_ERROR) } - } + }.invoke(this) }) } @@ -153,30 +199,42 @@ actual open class ServiceManager<out T> actual constructor(val service: T?) { * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, PAR4, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, PAR4, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { routes.add({ - post("$SERVICE_PREFIX/$route") { req, res -> - val str = req.body(String::class.java) - val tree = mapper.readTree(str) - if (tree.size() == 4 && service != null) { - val p1 = mapper.treeToValue(tree[0], PAR1::class.java) - val p2 = mapper.treeToValue(tree[1], PAR2::class.java) - val p3 = mapper.treeToValue(tree[2], PAR3::class.java) - val p4 = mapper.treeToValue(tree[3], PAR4::class.java) - try { - res.send(runBlocking { function.invoke(service, p1, p2, p3, p4, req).await() }) - } catch (e: Exception) { - e.printStackTrace() - res.status(500).send(e.message ?: "Error") + call(method, "$prefix$route") { req, res -> + if (service != null) { + val jsonRpcRequest = req.body(JsonRpcRequest::class.java) + if (jsonRpcRequest.params.size == 4) { + val param1 = getParameter<PAR1>(jsonRpcRequest.params[0]) + val param2 = getParameter<PAR2>(jsonRpcRequest.params[1]) + val param3 = getParameter<PAR3>(jsonRpcRequest.params[2]) + val param4 = getParameter<PAR4>(jsonRpcRequest.params[3]) + try { + val result = + runBlocking { function.invoke(service, param1, param2, param3, param4, req).await() } + res.send( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + } catch (e: Exception) { + LOG.error(e.message, e) + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = e.message ?: "Error")) + } + } else { + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters")) } } else { - res.status(Status.BAD_REQUEST) + res.status(Status.SERVER_ERROR) } - } + }.invoke(this) }) } @@ -184,34 +242,76 @@ actual open class ServiceManager<out T> actual constructor(val service: T?) { * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ - protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified PAR5, reified RET> bind( + protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, + reified PAR4, reified PAR5, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> Deferred<RET>, + method: RpcHttpMethod, + prefix: String ) { routes.add({ - post("$SERVICE_PREFIX/$route") { req, res -> - val str = req.body(String::class.java) - val tree = mapper.readTree(str) - if (tree.size() == 5 && service != null) { - val p1 = mapper.treeToValue(tree[0], PAR1::class.java) - val p2 = mapper.treeToValue(tree[1], PAR2::class.java) - val p3 = mapper.treeToValue(tree[2], PAR3::class.java) - val p4 = mapper.treeToValue(tree[3], PAR4::class.java) - val p5 = mapper.treeToValue(tree[4], PAR5::class.java) - try { - res.send(runBlocking { function.invoke(service, p1, p2, p3, p4, p5, req).await() }) - } catch (e: Exception) { - e.printStackTrace() - res.status(500).send(e.message ?: "Error") + call(method, "$prefix$route") { req, res -> + if (service != null) { + val jsonRpcRequest = req.body(JsonRpcRequest::class.java) + if (jsonRpcRequest.params.size == 5) { + val param1 = getParameter<PAR1>(jsonRpcRequest.params[0]) + val param2 = getParameter<PAR2>(jsonRpcRequest.params[1]) + val param3 = getParameter<PAR3>(jsonRpcRequest.params[2]) + val param4 = getParameter<PAR4>(jsonRpcRequest.params[3]) + val param5 = getParameter<PAR5>(jsonRpcRequest.params[4]) + try { + val result = + runBlocking { + function.invoke(service, param1, param2, param3, param4, param5, req).await() + } + res.send( + JsonRpcResponse( + id = jsonRpcRequest.id, + result = mapper.writeValueAsString(result) + ) + ) + } catch (e: Exception) { + LOG.error(e.message, e) + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = e.message ?: "Error")) + } + } else { + res.send(JsonRpcResponse(id = jsonRpcRequest.id, error = "Invalid parameters")) } } else { - res.status(Status.BAD_REQUEST) + res.status(Status.SERVER_ERROR) } - } + }.invoke(this) }) } + fun call( + method: RpcHttpMethod, + path: String, + handler: (Request, Response) -> Unit + ): JoobyServer.() -> Unit { + return { + when (method) { + RpcHttpMethod.POST -> post(path, handler) + RpcHttpMethod.PUT -> put(path, handler) + RpcHttpMethod.DELETE -> delete(path, handler) + RpcHttpMethod.OPTIONS -> options(path, handler) + } + } + } + + protected inline fun <reified T> getParameter(str: String?): T { + return str?.let { + if (T::class == String::class) { + str as T + } else { + mapper.readValue(str, T::class.java) + } + } ?: null as T + } + /** * Applies all defined routes to the given server. * @param k a Jooby server @@ -226,5 +326,5 @@ actual open class ServiceManager<out T> actual constructor(val service: T?) { * Returns the list of defined bindings. * Not used on the jvm platform. */ - actual fun getCalls(): Map<String, String> = mapOf() + actual fun getCalls(): Map<String, Pair<String, RpcHttpMethod>> = mapOf() } diff --git a/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt b/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt new file mode 100644 index 00000000..2f85a51f --- /dev/null +++ b/src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2017-present Robert Jaros + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package pl.treksoft.kvision.remote + +import kotlinx.serialization.json.JSON +import pl.treksoft.jquery.JQueryAjaxSettings +import pl.treksoft.jquery.JQueryXHR +import pl.treksoft.jquery.jQuery +import pl.treksoft.kvision.utils.obj +import kotlin.js.Promise +import kotlin.js.undefined +import kotlin.js.JSON as NativeJSON + +/** + * An agent responsible for remote calls. + */ +open class CallAgent { + + private var counter = 1 + + /** + * Makes an JSON-RPC call to the remote server. + * @param url an URL address + * @param method a HTTP method + * @param data data to be sent + * @return a promise of the result + */ + @Suppress("UnsafeCastFromDynamic") + fun jsonRpcCall( + url: String, + data: List<String?> = listOf(), + method: RpcHttpMethod = RpcHttpMethod.POST + ): Promise<String> { + val jsonRpcRequest = JsonRpcRequest(counter++, url, data) + val jsonData = JSON.stringify(jsonRpcRequest) + return Promise({ resolve, reject -> + jQuery.ajax(url, obj { + this.contentType = "application/json" + this.data = jsonData + this.method = method.name + this.success = + { data: dynamic, _: Any, _: Any -> + if (data.id != jsonRpcRequest.id) { + reject(Exception("Invalid response ID")) + } else if (data.error != null) { + reject(Exception(data.error.toString())) + } else if (data.result != null) { + resolve(data.result) + } else { + reject(Exception("Invalid response")) + } + } + this.error = + { xhr: JQueryXHR, _: String, errorText: String -> + val message = if (xhr.responseJSON != null && xhr.responseJSON != undefined) { + xhr.responseJSON.toString() + } else { + errorText + } + reject(Exception(message)) + } + }) + }) + } + + /** + * Makes a remote call to the remote server. + * @param url an URL address + * @param method a HTTP method + * @param data data to be sent + * @return a promise of the result + */ + @Suppress("UnsafeCastFromDynamic") + fun remoteCall( + url: String, + data: dynamic = null, + method: HttpMethod = HttpMethod.GET, + contentType: String = "application/json", + beforeSend: ((JQueryXHR, JQueryAjaxSettings) -> Boolean)? = null + ): Promise<dynamic> { + return Promise({ resolve, reject -> + jQuery.ajax(url, obj { + this.contentType = contentType + this.data = data + this.method = method.name + this.success = + { data: dynamic, _: Any, _: Any -> + resolve(data) + } + this.error = + { xhr: JQueryXHR, _: String, errorText: String -> + val message = if (xhr.responseJSON != null && xhr.responseJSON != undefined) { + xhr.responseJSON.toString() + } else { + errorText + } + reject(Exception(message)) + } + this.beforeSend = beforeSend + }) + }) + } +} diff --git a/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt b/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt index 3f7276f0..4c6ecf82 100644 --- a/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt +++ b/src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt @@ -32,30 +32,28 @@ import kotlinx.serialization.internal.StringSerializer import kotlinx.serialization.json.JSON import kotlinx.serialization.list import kotlinx.serialization.serializer -import pl.treksoft.jquery.JQueryXHR -import pl.treksoft.jquery.jQuery -import pl.treksoft.kvision.utils.obj import kotlin.js.Promise import kotlin.js.js -import kotlin.js.undefined import kotlin.reflect.KClass import kotlin.js.JSON as NativeJSON internal class NonStandardTypeException(type: String) : Exception("Non standard type: $type!") /** - * Client side agent for remote calls. + * Client side agent for JSON-RPC remote calls. */ @Suppress("EXPERIMENTAL_FEATURE_WARNING", "LargeClass", "TooManyFunctions") open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { + val callAgent = CallAgent() + /** * Executes defined call to a remote web service. */ inline fun <reified RET : Any, T> call(noinline function: T.(Request?) -> Deferred<RET>): Promise<RET> { - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, null).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, method = method).then { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -68,9 +66,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { * Executes defined call to a remote web service. */ inline fun <reified RET : Any, T> call(noinline function: T.(Request?) -> Deferred<List<RET>>): Promise<List<RET>> { - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, null).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, method = method).then { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -87,9 +85,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { p: PAR, serializer: KSerializer<PAR>? = null ): Promise<RET> { val data = serialize(p, serializer) - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data), method).then { try { @Suppress("UNCHECKED_CAST") deserialize<RET>(it, (RET::class as KClass<Any>).js.name) @@ -107,9 +105,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { p: PAR, serializer: KSerializer<PAR>? = null ): Promise<List<RET>> { val data = serialize(p, serializer) - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data), method).then { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -127,10 +125,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { ): Promise<RET> { val data1 = serialize(p1, serializer1) val data2 = serialize(p2, serializer2) - val data = "[ $data1 , $data2 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2), method).then { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -148,10 +145,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { ): Promise<List<RET>> { val data1 = serialize(p1, serializer1) val data2 = serialize(p2, serializer2) - val data = "[ $data1 , $data2 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2), method).then { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -171,10 +167,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { val data1 = serialize(p1, serializer1) val data2 = serialize(p2, serializer2) val data3 = serialize(p3, serializer3) - val data = "[ $data1 , $data2 , $data3 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2, data3), method).then { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -194,10 +189,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { val data1 = serialize(p1, serializer1) val data2 = serialize(p2, serializer2) val data3 = serialize(p3, serializer3) - val data = "[ $data1 , $data2 , $data3 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2, data3), method).then { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -224,10 +218,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { val data2 = serialize(p2, serializer2) val data3 = serialize(p3, serializer3) val data4 = serialize(p4, serializer4) - val data = "[ $data1 , $data2 , $data3 , $data4 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4), method).then { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -254,10 +247,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { val data2 = serialize(p2, serializer2) val data3 = serialize(p3, serializer3) val data4 = serialize(p4, serializer4) - val data = "[ $data1 , $data2 , $data3 , $data4 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4), method).then { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -289,10 +281,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { val data3 = serialize(p3, serializer3) val data4 = serialize(p4, serializer4) val data5 = serialize(p5, serializer5) - val data = "[ $data1 , $data2 , $data3 , $data4 , $data5 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4, data5), method).then { try { deserialize<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -324,10 +315,9 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { val data3 = serialize(p3, serializer3) val data4 = serialize(p4, serializer4) val data5 = serialize(p5, serializer5) - val data = "[ $data1 , $data2 , $data3 , $data4 , $data5 ]" - val url = - serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") - return ajaxCall(url, data).then { + val (url, method) = + serviceManager.getCalls()[function.toString()] ?: throw IllegalStateException("Function not specified!") + return callAgent.jsonRpcCall(url, listOf(data1, data2, data3, data4, data5), method).then { try { deserializeLists<RET>(it, RET::class.js.name) } catch (t: NonStandardTypeException) { @@ -336,32 +326,6 @@ open class RemoteAgent<out T>(val serviceManager: ServiceManager<T>) { } } - /** - * @suppress - * Internal function - */ - @Suppress("UnsafeCastFromDynamic") - fun ajaxCall(url: String, data: Any?): Promise<String> = - Promise({ resolve, reject -> - jQuery.ajax(url, obj { - this.contentType = "application/json" - this.data = data - this.method = "POST" - this.success = - { data: Any, _: Any, _: Any -> - resolve(NativeJSON.stringify(data)) - } - this.error = - { xhr: JQueryXHR, _: String, errorText: String -> - val message = if (xhr.responseJSON != null && xhr.responseJSON != undefined) { - xhr.responseJSON.toString() - } else { - errorText - } - reject(Exception(message)) - } - }) - }) /** * @suppress diff --git a/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt b/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt index cd42f038..df3245c3 100644 --- a/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt +++ b/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt @@ -27,79 +27,93 @@ import kotlinx.coroutines.experimental.Deferred * Multiplatform service manager. */ actual open class ServiceManager<out T> actual constructor(service: T?) { - protected val calls: MutableMap<String, String> = mutableMapOf() + protected val calls: MutableMap<String, Pair<String, RpcHttpMethod>> = mutableMapOf() /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified RET> bind( route: String, - noinline function: T.(Request?) -> Deferred<RET> + noinline function: T.(Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { - calls[function.toString()] = "$SERVICE_PREFIX/$route" + calls[function.toString()] = Pair("$prefix$route", method) } /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR, reified RET> bind( route: String, - noinline function: T.(PAR, Request?) -> Deferred<RET> + noinline function: T.(PAR, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { - calls[function.toString()] = "$SERVICE_PREFIX/$route" + calls[function.toString()] = Pair("$prefix$route", method) } /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR1, reified PAR2, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { - calls[function.toString()] = "$SERVICE_PREFIX/$route" + calls[function.toString()] = Pair("$prefix$route", method) } /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { - calls[function.toString()] = "$SERVICE_PREFIX/$route" + calls[function.toString()] = Pair("$prefix$route", method) } /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, PAR4, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, PAR4, Request?) -> Deferred<RET>, method: RpcHttpMethod, prefix: String ) { - calls[function.toString()] = "$SERVICE_PREFIX/$route" + calls[function.toString()] = Pair("$prefix$route", method) } /** * Binds a given route with a function of the receiver. * @param route a route * @param function a function of the receiver + * @param method a HTTP method + * @param prefix an URL address prefix */ protected actual inline fun <reified PAR1, reified PAR2, reified PAR3, reified PAR4, reified PAR5, reified RET> bind( route: String, - noinline function: T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> Deferred<RET> + noinline function: T.(PAR1, PAR2, PAR3, PAR4, PAR5, Request?) -> Deferred<RET>, + method: RpcHttpMethod, + prefix: String ) { - calls[function.toString()] = "$SERVICE_PREFIX/$route" + calls[function.toString()] = Pair("$prefix$route", method) } /** @@ -110,8 +124,7 @@ actual open class ServiceManager<out T> actual constructor(service: T?) { } /** - * Returns the list of defined bindings. - * Not used on the jvm platform. + * Returns the map of defined paths. */ - actual fun getCalls(): Map<String, String> = calls + actual fun getCalls(): Map<String, Pair<String, RpcHttpMethod>> = calls } |