Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ plugins {
alias(libs.plugins.detekt) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.oss.licenses) apply false
}

Expand Down
13 changes: 13 additions & 0 deletions core-common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.detekt)
}

kotlin {
jvmToolchain(17)
}

dependencies {
implementation(libs.coroutines.core)
testImplementation(libs.junit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.motionapps.sensorbox.core.error

data class AppError(
val code: AppErrorCode,
val operation: String,
val diagnosticMessage: String,
val cause: Throwable? = null,
val context: Map<String, String> = emptyMap(),
val isRetryable: Boolean = false,
) {
constructor(code: AppErrorCode, operation: String) : this(
code = code,
operation = operation,
diagnosticMessage = "$operation failed",
)

fun within(parentOperation: String, fallbackCode: AppErrorCode = code): AppError = copy(
code = if (code == AppErrorCode.UNKNOWN) fallbackCode else code,
operation = parentOperation,
diagnosticMessage = "$parentOperation failed during $operation",
context = context + ("failedOperation" to operation),
)

companion object {
fun from(code: AppErrorCode, operation: String, cause: Throwable): AppError = AppError(
code = code,
operation = operation,
diagnosticMessage = "$operation failed with ${cause::class.java.simpleName}",
cause = cause,
)
}
}

enum class AppErrorCode {
CONNECTIVITY,
EXTERNAL_ACTION,
MEASUREMENT,
PERMISSION,
PREFERENCES,
STORAGE,
VALIDATION,
TIMEOUT,
CONFLICT,
UNKNOWN,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package com.motionapps.sensorbox.core.error

import kotlinx.coroutines.CancellationException

sealed interface AppResult<out T> {
val isSuccess: Boolean
get() = this is Success

val isFailure: Boolean
get() = this is Failure

fun getOrNull(): T? = when (this) {
is Success -> value
is Failure -> null
}

fun errorOrNull(): AppError? = when (this) {
is Success -> null
is Failure -> error
}

fun <R> fold(onSuccess: (T) -> R, onFailure: (AppError) -> R): R = when (this) {
is Success -> onSuccess(value)
is Failure -> onFailure(error)
}

fun <R> map(transform: (T) -> R): AppResult<R> = when (this) {
is Success -> success(transform(value))
is Failure -> this
}

fun getOrElse(onFailure: (AppError) -> @UnsafeVariance T): T = when (this) {
is Success -> value
is Failure -> onFailure(error)
}

fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) {
is Success -> value
is Failure -> defaultValue
}

fun getOrThrow(): T = when (this) {
is Success -> value
is Failure -> throw IllegalStateException(error.diagnosticMessage, error.cause)
}

fun onSuccess(action: (T) -> Unit): AppResult<T> = apply {
if (this is Success) action(value)
}

fun onFailure(action: (AppError) -> Unit): AppResult<T> = apply {
if (this is Failure) action(error)
}

data class Success<T>(val value: T) : AppResult<T>

data class Failure(val error: AppError) : AppResult<Nothing>

companion object {
fun <T> success(value: T): AppResult<T> = Success(value)

fun failure(error: AppError): AppResult<Nothing> = Failure(error)
}
}

inline fun <T, R> AppResult<T>.flatMap(transform: (T) -> AppResult<R>): AppResult<R> = when (this) {
is AppResult.Success -> transform(value)
is AppResult.Failure -> this
}

suspend inline fun <T, R> AppResult<T>.suspendFlatMap(
crossinline transform: suspend (T) -> AppResult<R>,
): AppResult<R> = when (this) {
is AppResult.Success -> transform(value)
is AppResult.Failure -> this
}

@Suppress("TooGenericExceptionCaught")
inline fun <T> appResult(code: AppErrorCode, operation: String, block: () -> T): AppResult<T> = try {
AppResult.success(block())
} catch (error: CancellationException) {
throw error
} catch (error: Throwable) {
AppResult.failure(AppError.from(code, operation, error))
}

@Suppress("TooGenericExceptionCaught")
suspend inline fun <T> suspendAppResult(
code: AppErrorCode,
operation: String,
crossinline block: suspend () -> T,
): AppResult<T> = try {
AppResult.success(block())
} catch (error: CancellationException) {
throw error
} catch (error: Throwable) {
AppResult.failure(AppError.from(code, operation, error))
}

fun <T> AppResult<T>.withAppError(code: AppErrorCode, operation: String): AppResult<T> = when (this) {
is AppResult.Success -> this
is AppResult.Failure -> AppResult.failure(error.within(operation, code))
}

fun Iterable<AppResult<*>>.combineAppResults(code: AppErrorCode, operation: String): AppResult<Unit> {
val errors = mapNotNull(AppResult<*>::errorOrNull)
if (errors.isEmpty()) return AppResult.success(Unit)
val causes = errors.mapNotNull(AppError::cause)
val firstCause = causes.firstOrNull()
causes.drop(1).forEach { cause -> firstCause?.addSuppressed(cause) }
return AppResult.failure(
AppError(
code = errors.first().code.takeUnless { it == AppErrorCode.UNKNOWN } ?: code,
operation = operation,
diagnosticMessage = "$operation failed in ${errors.size} operation(s)",
cause = firstCause,
context = mapOf(
"failureCount" to errors.size.toString(),
"failedOperations" to errors.joinToString(",") { it.operation },
),
),
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.motionapps.sensorbox.core.error

import java.io.File

enum class DiagnosticSeverity {
INFO,
WARNING,
ERROR,
FATAL,
}

data class DiagnosticEvent(
val severity: DiagnosticSeverity,
val code: AppErrorCode,
val operation: String,
val diagnosticMessage: String,
val cause: Throwable? = null,
val context: Map<String, String> = emptyMap(),
)

fun AppError.toDiagnosticEvent(severity: DiagnosticSeverity = DiagnosticSeverity.ERROR): DiagnosticEvent =
DiagnosticEvent(
severity = severity,
code = code,
operation = operation,
diagnosticMessage = diagnosticMessage,
cause = cause,
context = context,
)

fun interface DiagnosticLogger {
fun record(event: DiagnosticEvent)
}

interface DiagnosticsStore {
fun readText(): AppResult<String>

fun exportFile(): AppResult<File>

fun clear(): AppResult<Unit>
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ detekt = { id = "dev.detekt", version.ref = "detekt" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
oss-licenses = { id = "com.google.android.gms.oss-licenses-plugin", version.ref = "ossLicensesPlugin" }
16 changes: 16 additions & 0 deletions recording-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.detekt)
}

kotlin {
jvmToolchain(17)
}

dependencies {
implementation(project(":core-common"))
implementation(libs.coroutines.core)

testImplementation(libs.coroutines.test)
testImplementation(libs.junit)
}
Loading