Большой рефакторинг

Из domain выкинуты типы vault, теперь он ничего не знает о Yandex. Объявления провайдеров вынесены в vault-api, а реализации в data
This commit is contained in:
2026-04-27 02:47:02 +03:00
parent 2b1be58a8e
commit 1034e134c2
37 changed files with 527 additions and 219 deletions

View File

@@ -0,0 +1,23 @@
import org.gradle.api.file.DuplicatesStrategy
import org.gradle.api.tasks.bundling.Jar
plugins {
id("java-library")
alias(libs.plugins.jetbrains.kotlin.jvm)
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks.withType<Jar>().configureEach {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
dependencies {
implementation(project(":domain"))
implementation(libs.kotlinx.coroutines.core)
testImplementation(libs.junit)
}

View File

@@ -0,0 +1,11 @@
package com.github.nullptroma.wallenc.vaultapi
/**
* Поддерживаемые облачные провайдеры удалённых vault'ов.
*
* Бренд «локального» устройства тут не указывается — для дискриминации UI
* используется [VaultDescriptor.LocalDevice] vs [VaultDescriptor.LinkedRemote].
*/
enum class CloudBrand {
YANDEX,
}

View File

@@ -0,0 +1,14 @@
package com.github.nullptroma.wallenc.vaultapi
import com.github.nullptroma.wallenc.domain.interfaces.IVault
/**
* Vault, дополнительно знающий свою категорию для UI/маршрутизации.
*
* Все реализации vault'ов в `:data` обязаны быть [DescribedVault] —
* это единственный способ для presentation/app узнать, что это за vault,
* не имея зависимости от `:data`.
*/
interface DescribedVault : IVault {
val descriptor: VaultDescriptor
}

View File

@@ -0,0 +1,20 @@
package com.github.nullptroma.wallenc.vaultapi
/**
* Запуск OAuth-сценария привязки удалённого vault'а для конкретного [CloudBrand].
*
* Реализация в `:app` (привязана к Activity). presentation вызывает [beginLink]
* из контекста Compose-экрана и получает [VaultLinkOutcome] асинхронно.
*
* Намеренно императивный API через колбэк, чтобы корректно встроиться
* в `ActivityResultLauncher` Yandex Auth SDK без удержания Activity синглтоном.
*/
fun interface RemoteVaultAuthenticator {
/**
* Начать сценарий линка для бренда [brand].
*
* Если бренд не поддерживается — реализация должна синхронно вызвать
* `onResult(VaultLinkOutcome.Failed(...))`, не бросая исключение.
*/
fun beginLink(brand: CloudBrand, onResult: (VaultLinkOutcome) -> Unit)
}

View File

@@ -0,0 +1,26 @@
package com.github.nullptroma.wallenc.vaultapi
import com.github.nullptroma.wallenc.domain.interfaces.IVaultInfo
import java.util.UUID
/**
* Категория vault'а для UI и ветвления — sealed-иерархия в одном модуле,
* чтобы `when` был exhaustive.
*
* `domain` про эти подтипы ничего не знает; внешнее кольцо (`:vault-api`)
* расширяет [IVaultInfo] (только `uuid`) до структуры с дополнительной
* категоризацией.
*/
sealed interface VaultDescriptor : IVaultInfo {
/** Vault, физически живущий на устройстве; без привязки к облачному аккаунту. */
data class LocalDevice(
override val uuid: UUID,
) : VaultDescriptor
/** Vault, привязанный к облачному аккаунту через OAuth. */
data class LinkedRemote(
override val uuid: UUID,
val brand: CloudBrand,
val accountDisplayName: String,
) : VaultDescriptor
}

View File

@@ -0,0 +1,18 @@
package com.github.nullptroma.wallenc.vaultapi
/**
* Результат сценария OAuth-линка нового удалённого vault'а.
*
* presentation сводит это к: успех → отдать `registration` в [VaultRegistrar.register];
* cancel → ничего не делать; failure → показать сообщение.
*/
sealed interface VaultLinkOutcome {
/** Пользователь успешно вошёл; в [registration] лежат данные для регистрации. */
data class Success(val registration: VaultRegistration) : VaultLinkOutcome
/** Пользователь отменил вход. */
data object Cancelled : VaultLinkOutcome
/** Ошибка SDK/сети/сервера; [message] годится для показа пользователю. */
data class Failed(val message: String) : VaultLinkOutcome
}

View File

@@ -0,0 +1,19 @@
package com.github.nullptroma.wallenc.vaultapi
import java.util.UUID
/**
* Контракт регистрации/удаления удалённого vault'а.
* Реализуется тем же объектом, что и `domain.IVaultsManager`.
*/
interface VaultRegistrar {
/**
* Зарегистрировать новый удалённый vault по «полезной нагрузке» из
* [VaultLinkOutcome.Success]. Если тип [registration] неизвестен реализации,
* бросает [IllegalArgumentException].
*/
suspend fun register(registration: VaultRegistration)
/** Удалить ранее зарегистрированный vault по идентификатору. */
suspend fun unregister(vaultUuid: UUID)
}

View File

@@ -0,0 +1,14 @@
package com.github.nullptroma.wallenc.vaultapi
/**
* Маркер «полезной нагрузки» для регистрации удалённого vault'а через [VaultRegistrar].
*
* Намеренно НЕ sealed: конкретные реализации (`YandexRegistration`, …) живут в `:data`
* рядом с соответствующими реализациями vault'а, чтобы `:data` не разнесёшь по
* нескольким модулям без необходимости. Цена — отсутствие exhaustive-when через
* границу модуля, лечится fail-fast веткой `else` в `VaultsManager.register(...)`.
*
* presentation/app никогда не «открывают» этот тип — они только перепасовывают
* объект из [VaultLinkOutcome.Success] в [VaultRegistrar.register].
*/
interface VaultRegistration

View File

@@ -0,0 +1,22 @@
package com.github.nullptroma.wallenc.vaultapi
import com.github.nullptroma.wallenc.domain.interfaces.IVault
/**
* Хелперы фильтрации списка vault'ов по [VaultDescriptor].
*
* Все реализации vault'а обязаны быть [DescribedVault];
* [described] отбрасывает те, что не соответствуют контракту (на случай
* вспомогательных тест-двойников и т.п.).
*/
fun List<IVault>.described(): List<DescribedVault> = filterIsInstance<DescribedVault>()
val List<DescribedVault>.locals: List<DescribedVault>
get() = filter { it.descriptor is VaultDescriptor.LocalDevice }
val List<DescribedVault>.remotes: List<DescribedVault>
get() = filter { it.descriptor is VaultDescriptor.LinkedRemote }
fun List<DescribedVault>.byBrand(brand: CloudBrand): List<DescribedVault> = filter {
(it.descriptor as? VaultDescriptor.LinkedRemote)?.brand == brand
}