aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Module.md2
-rw-r--r--kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/JsonRpc.kt37
-rw-r--r--kvision-common/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt58
-rw-r--r--kvision-server/src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt270
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/remote/CallAgent.kt122
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/remote/RemoteAgent.kt114
-rw-r--r--src/main/kotlin/pl/treksoft/kvision/remote/ServiceManager.kt45
7 files changed, 462 insertions, 186 deletions
diff --git a/Module.md b/Module.md
index 0fabb75e..d286b17b 100644
--- a/Module.md
+++ b/Module.md
@@ -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
}