diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt b/src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt index 9e37077..4b3cefc 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/Adapter.kt @@ -7,15 +7,15 @@ import kotlinx.serialization.json.Json import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.boss_serialization_mp.decodeBoss -import net.sergeych.cloudoc.api.ApiError.BAD_RESPONSE_PACKAGE -import net.sergeych.cloudoc.api.ApiError.UNKNOWN_ERROR -import net.sergeych.cloudoc.api.ApiException import net.sergeych.cloudoc.api.Package import net.sergeych.mp_logger.LogTag import net.sergeych.mp_logger.debug import net.sergeych.mp_logger.exception import net.sergeych.mp_logger.warning import net.sergeych.mptools.toDump +import net.sergeych.parsec3.ExceptionsRegistry +import net.sergeych.parsec3.InvalidFrameException +import net.sergeych.parsec3.ParsecException /** * Create adapter, an interface to provide local API commands and invoke remote API commands @@ -66,11 +66,15 @@ import net.sergeych.mptools.toDump * @param instance any instance that represent the state of the interface. Could be `Unit` for stateless. * @param commandHost the Api __this adapter provides to a remote__. It differs from the interface expected on the * remote side. + * @param exceptionRegistry allows to transform serialized parsec exception (see [ParsecException]) to + * application-specific exception classes. Default implementation only decodes + * parsec3 built-in exceptions. * @param sendEncoded a method that performs actual sending of the packed binary frame to the remote side */ open class Adapter( private val instance: T, private val commandHost: CommandHost, + private val exceptionRegistry: ExceptionsRegistry = ExceptionsRegistry(), private val sendEncoded: suspend (data: ByteArray) -> Unit, ) : LogTag("ADPTR") { @@ -113,15 +117,13 @@ open class Adapter( val handler = commandHost.handler(pe.name) val result = handler.invoke(instance, pe.args) sendPackage( - Package.Response( - pe.id, result - ) + Package.Response(pe.id, result) ) - } catch (ae: ApiException) { + } catch (ae: ParsecException) { sendPackage(Package.Response(pe.id, null, ae.code, ae.text)) } catch (ex: Throwable) { ex.printStackTrace() - sendPackage(Package.Response(pe.id, null, UNKNOWN_ERROR, ex.toString())) + sendPackage(Package.Response(pe.id, null, "UNKNOWN_ERROR", ex.toString())) } } @@ -134,8 +136,8 @@ open class Adapter( dr.complete(pe.result) else dr.completeExceptionally( - pe.errorCode?.let { ApiException(it, pe.errorText) } - ?: ApiException(BAD_RESPONSE_PACKAGE) + pe.errorCode?.let { exceptionRegistry.getException(it, pe.errorText) } + ?: InvalidFrameException("invalid package: no result, no error code") ) } } diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt b/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt index f865675..04b637b 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/CommandHost.kt @@ -2,8 +2,8 @@ package channel import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization_mp.BossEncoder -import net.sergeych.cloudoc.api.ApiError import net.sergeych.parsec3.AdapterDelegate +import net.sergeych.parsec3.CommandNotFoundException import kotlin.reflect.typeOf /** @@ -46,7 +46,7 @@ open class CommandHost { } } - fun handler(name: String) = handlers.get(name) ?: ApiError.NOT_FOUND.raise("command not found") + fun handler(name: String) = handlers.get(name) ?: throw CommandNotFoundException(name) /** * Provide a command delegate that creates type-safe command descriptor containint command name and diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/ExceptionsRegistry.kt b/src/commonMain/kotlin/net.sergeych.parsec3/ExceptionsRegistry.kt new file mode 100644 index 0000000..018b518 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/ExceptionsRegistry.kt @@ -0,0 +1,52 @@ +package net.sergeych.parsec3 + +/** + * Registry to restore exceptions from parsec block data. Serializing exceptions is dangerous: being a OS-bound + * objects, exceptions can carry too much sensitive or useless information (e.g. call stack), and serializng + * actual exceptions could be a pain, so parsec3 serializes exception information as 2 parameters: a code string + * which is very much like old good (and awful in a way) `C ERRNO`, and an optional message. + * + * This class reconstructs exceptions from these parameters using a registry, that is pre-filled with application + * codes and actual exception classes. Then [Adapter] uses it to restore and throw actual exception on the calling + * party side. + * + * The good idea is to share one object inheriting from refistry to hold all exceptions info in one place + * and share it betweem client and server code. + */ +open class ExceptionsRegistry { + + private val handlers = mutableMapOfThrowable>().also { + // predefined exceptions: + it[commandNotFoundCode] = { CommandNotFoundException(it ?: "???") } + it[unknownErrorCode] = { UnknownException(it ?: "???") } + } + + /** + * Register an exception with a code with a handler that creates its instance. Note that the + * handler _should not throw anything_ but rather create an instance of the exception. + */ + fun register(code: String, block: (message: String?) -> T) { + handlers[code] = block + } + + /** + * raise the exception using the proper handler. Throws [UnknownCodeException] of there is no handler + * for a given code. + */ + internal fun raise(code: String,message: String?): Nothing { + throw getException(code, message) + } + + /** + * create the exception instanceusing the proper handler, or an [UnknownCodeException] if handler not found + */ + internal fun getException(code: String,message: String?): Throwable = + handlers[code]?.let { it(message) } ?: UnknownCodeException(code, message) + + companion object { + val commandNotFoundCode = "_COMMAND_NOT_FOUND" + val unknownErrorCode = "_UNKNOWN_ERROR" + val invalidFrameCode = "_FRAME_INVALID" + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/Package.kt b/src/commonMain/kotlin/net.sergeych.parsec3/Package.kt index 3a4dda1..c8f20a8 100644 --- a/src/commonMain/kotlin/net.sergeych.parsec3/Package.kt +++ b/src/commonMain/kotlin/net.sergeych.parsec3/Package.kt @@ -12,7 +12,7 @@ sealed class Package { data class Response( val toId: Int, val result: ByteArray? = null, - val errorCode: ApiError? = null, + val errorCode: String? = null, val errorText: String? = null, ) : Package() { init { diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/ParsecException.kt b/src/commonMain/kotlin/net.sergeych.parsec3/ParsecException.kt new file mode 100644 index 0000000..ec68f37 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.parsec3/ParsecException.kt @@ -0,0 +1,19 @@ +package net.sergeych.parsec3 + +/** + * The base class for exceptions that pass through parsec3 channel. Application exceptinos + * intended to be transmitted through this channel should inherit from it and be registered + * with proper [ExceptionsRegistry] instance provided to the exceptions-receiving adaper (better + * to both sides the same). + */ +open class ParsecException(val code: String, val text: String?=null, reason: Throwable?=null) : Exception( + text?.let { "($code): $text" } ?: code, reason +) + +class CommandNotFoundException(name: String): ParsecException(ExceptionsRegistry.commandNotFoundCode,name) + +class InvalidFrameException(text: String): ParsecException(ExceptionsRegistry.invalidFrameCode, text) + +class UnknownCodeException(code: String,message: String?): ParsecException(code, message) + +class UnknownException(message: String?): ParsecException(ExceptionsRegistry.unknownErrorCode, message) \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.parsec3/errors.kt b/src/commonMain/kotlin/net.sergeych.parsec3/errors.kt deleted file mode 100644 index 638dfd6..0000000 --- a/src/commonMain/kotlin/net.sergeych.parsec3/errors.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.sergeych.cloudoc.api - -enum class ApiError { - UNKNOWN_ERROR, - INTERNAL_ERROR, - BAD_RESPONSE_PACKAGE, - NOT_FOUND, - BAD_LOGIN, - OBJECT_ALREADY_EXISTS, - ACCESS_FORBIDDEN, - ILLEGAL_STATE, - NOT_LOGGED_IN, - BAD_PARAMETER, - EMAIL_IN_USE, - NAME_IN_USE; - - fun raise(text: String? = null): Nothing { - throw ApiException(this, text ?: defaultText()) - } - - fun defaultText(): String = name.lowercase().replace('_', ' ') -} - -class ApiException(val code: ApiError, _text: String? = null, cause: Throwable? = null): -Exception(_text ?: code.defaultText(), cause) { - val text by lazy { _text ?: code.defaultText() } -}