483 lines
13 KiB
Markdown
483 lines
13 KiB
Markdown
# Sealed Class Catalog
|
|
|
|
Comprehensive list of sealed types in AmethystMultiplatform with usage patterns.
|
|
|
|
## Table of Contents
|
|
- [State Management](#state-management)
|
|
- [Result Types](#result-types)
|
|
- [Tag Variants](#tag-variants)
|
|
- [Sealed Class vs Sealed Interface](#sealed-class-vs-sealed-interface)
|
|
- [Patterns](#patterns)
|
|
|
|
---
|
|
|
|
## State Management
|
|
|
|
### AccountState (Sealed Class)
|
|
|
|
**File:** `commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-46`
|
|
|
|
```kotlin
|
|
sealed class AccountState {
|
|
data object LoggedOut : AccountState()
|
|
|
|
data class LoggedIn(
|
|
val signer: NostrSigner,
|
|
val pubKeyHex: String,
|
|
val npub: String,
|
|
val nsec: String?,
|
|
val isReadOnly: Boolean
|
|
) : AccountState()
|
|
}
|
|
```
|
|
|
|
**Why sealed class:**
|
|
- Two distinct states with different data
|
|
- `LoggedIn` holds data, `LoggedOut` doesn't
|
|
- No need for generics or multiple inheritance
|
|
|
|
**Usage:**
|
|
|
|
```kotlin
|
|
fun handleAccountState(state: AccountState) {
|
|
when (state) {
|
|
is AccountState.LoggedOut -> showLogin()
|
|
is AccountState.LoggedIn -> {
|
|
showFeed(
|
|
pubkey = state.pubKeyHex,
|
|
canSign = !state.isReadOnly
|
|
)
|
|
}
|
|
} // Exhaustive - compiler enforces
|
|
}
|
|
```
|
|
|
|
### VerificationState (Sealed Class)
|
|
|
|
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip03Timestamp/VerificationState.kt`
|
|
|
|
```kotlin
|
|
sealed class VerificationState {
|
|
data object NotStarted : VerificationState()
|
|
data object Started : VerificationState()
|
|
data class Failed(val reason: String) : VerificationState()
|
|
data object Verified : VerificationState()
|
|
}
|
|
```
|
|
|
|
**Pattern:**
|
|
- State machine (NotStarted → Started → Failed/Verified)
|
|
- Only `Failed` carries data (reason)
|
|
- Rest are singletons (`data object`)
|
|
|
|
---
|
|
|
|
## Result Types
|
|
|
|
### SignerResult (Sealed Interface with Generics)
|
|
|
|
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip46RemoteSigner/signer/SignerResult.kt:25-46`
|
|
|
|
```kotlin
|
|
sealed interface SignerResult<T : IResult> {
|
|
sealed interface RequestAddressed<T : IResult> : SignerResult<T> {
|
|
class Successful<T : IResult>(val result: T) : RequestAddressed<T>
|
|
class Rejected<T : IResult> : RequestAddressed<T>
|
|
class TimedOut<T : IResult> : RequestAddressed<T>
|
|
class ReceivedButCouldNotPerform<T : IResult>(
|
|
val message: String? = null
|
|
) : RequestAddressed<T>
|
|
class ReceivedButCouldNotParseEventFromResult<T : IResult>(
|
|
val eventJson: String
|
|
) : RequestAddressed<T>
|
|
class ReceivedButCouldNotVerifyResultingEvent<T : IResult>(
|
|
val invalidEvent: Event
|
|
) : RequestAddressed<T>
|
|
}
|
|
}
|
|
|
|
interface IResult
|
|
|
|
data class SignResult(val event: Event) : IResult
|
|
data class EncryptionResult(val ciphertext: String) : IResult
|
|
data class DecryptionResult(val plaintext: String) : IResult
|
|
```
|
|
|
|
**Why sealed interface:**
|
|
- Generic result type `<T : IResult>`
|
|
- Nested sealed hierarchy (RequestAddressed)
|
|
- Need covariance for flexible result types
|
|
|
|
**Usage:**
|
|
|
|
```kotlin
|
|
suspend fun signEvent(event: Event): SignerResult<SignResult> {
|
|
return when (val result = remoteSigner.sign(event)) {
|
|
is SignerResult.RequestAddressed.Successful -> result
|
|
is SignerResult.RequestAddressed.Rejected -> {
|
|
logger.warn("Signing rejected")
|
|
result
|
|
}
|
|
is SignerResult.RequestAddressed.TimedOut -> {
|
|
logger.error("Signing timed out")
|
|
result
|
|
}
|
|
is SignerResult.RequestAddressed.ReceivedButCouldNotPerform -> {
|
|
logger.error("Signer error: ${result.message}")
|
|
result
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### CacheResults (Sealed Class with Generics)
|
|
|
|
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/caches/CacheResults.kt`
|
|
|
|
```kotlin
|
|
sealed class CacheResults<T> {
|
|
data class Found<T>(val value: T) : CacheResults<T>()
|
|
class NotFound<T> : CacheResults<T>()
|
|
}
|
|
```
|
|
|
|
**Pattern:**
|
|
- Simple binary result (found/not found)
|
|
- `Found` carries data, `NotFound` doesn't
|
|
- Generic for reusability
|
|
|
|
---
|
|
|
|
## Tag Variants
|
|
|
|
### MuteTag (Sealed Class)
|
|
|
|
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/muteList/tags/MuteTag.kt`
|
|
|
|
```kotlin
|
|
sealed class MuteTag(
|
|
val nameOrNull: String?,
|
|
val valueOrNull: String?
|
|
) {
|
|
class Event(eventId: String) : MuteTag("e", eventId)
|
|
class Profile(pubkey: String) : MuteTag("p", pubkey)
|
|
class Word(word: String) : MuteTag("word", word)
|
|
class Thread(threadId: String) : MuteTag("thread", threadId)
|
|
|
|
companion object {
|
|
fun parse(tag: Array<String>): MuteTag? {
|
|
return when (tag.getOrNull(0)) {
|
|
"e" -> tag.getOrNull(1)?.let { Event(it) }
|
|
"p" -> tag.getOrNull(1)?.let { Profile(it) }
|
|
"word" -> tag.getOrNull(1)?.let { Word(it) }
|
|
"thread" -> tag.getOrNull(1)?.let { Thread(it) }
|
|
else -> null
|
|
}
|
|
}
|
|
}
|
|
|
|
fun toArray(): Array<String> {
|
|
return arrayOf(nameOrNull ?: "", valueOrNull ?: "")
|
|
}
|
|
}
|
|
```
|
|
|
|
**Pattern:**
|
|
- Common base class with shared properties
|
|
- Each variant represents different tag type
|
|
- Factory method `parse()` for parsing
|
|
- `toArray()` for serialization
|
|
|
|
### BookmarkIdTag (Sealed Class)
|
|
|
|
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip51Lists/bookmarkList/tags/BookmarkIdTag.kt`
|
|
|
|
```kotlin
|
|
sealed class BookmarkIdTag {
|
|
abstract val id: String
|
|
abstract val marker: String?
|
|
|
|
data class Event(override val id: String, override val marker: String?) : BookmarkIdTag()
|
|
data class Profile(override val id: String, override val marker: String?) : BookmarkIdTag()
|
|
data class Address(override val id: String, override val marker: String?) : BookmarkIdTag()
|
|
|
|
companion object {
|
|
fun parse(tag: Array<String>): BookmarkIdTag? {
|
|
val marker = tag.getOrNull(3)
|
|
return when (tag.getOrNull(0)) {
|
|
"e" -> tag.getOrNull(1)?.let { Event(it, marker) }
|
|
"p" -> tag.getOrNull(1)?.let { Profile(it, marker) }
|
|
"a" -> tag.getOrNull(1)?.let { Address(it, marker) }
|
|
else -> null
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Pattern:**
|
|
- Abstract properties in sealed class
|
|
- Data classes implement abstract properties
|
|
- Parse factory returns sealed variant
|
|
|
|
---
|
|
|
|
## Exception Hierarchies
|
|
|
|
### SignerExceptions (Sealed Class)
|
|
|
|
**File:** `quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/signers/SignerExceptions.kt`
|
|
|
|
```kotlin
|
|
sealed class SignerExceptions(message: String) : Exception(message) {
|
|
class UnableToSign(message: String) : SignerExceptions(message)
|
|
class UnableToDecrypt(message: String) : SignerExceptions(message)
|
|
class UnableToEncrypt(message: String) : SignerExceptions(message)
|
|
class UnableToGetPublicKey(message: String) : SignerExceptions(message)
|
|
}
|
|
```
|
|
|
|
**Pattern:**
|
|
- Sealed exception hierarchy
|
|
- Extends `Exception` base class
|
|
- Type-safe error handling
|
|
|
|
**Usage:**
|
|
|
|
```kotlin
|
|
try {
|
|
signer.sign(event)
|
|
} catch (e: SignerExceptions) {
|
|
when (e) {
|
|
is SignerExceptions.UnableToSign -> logger.error("Signing failed: ${e.message}")
|
|
is SignerExceptions.UnableToDecrypt -> logger.error("Decryption failed: ${e.message}")
|
|
is SignerExceptions.UnableToEncrypt -> logger.error("Encryption failed: ${e.message}")
|
|
is SignerExceptions.UnableToGetPublicKey -> logger.error("No public key: ${e.message}")
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Sealed Class vs Sealed Interface
|
|
|
|
### When to Use Sealed Class
|
|
|
|
**Examples from codebase:**
|
|
|
|
1. **AccountState** - State variants with different data
|
|
2. **VerificationState** - State machine
|
|
3. **MuteTag** - Tag variants with common base properties
|
|
4. **SignerExceptions** - Exception hierarchy
|
|
|
|
**Characteristics:**
|
|
- Need common constructor parameters
|
|
- Single inheritance only
|
|
- State variants
|
|
- Exception hierarchies
|
|
|
|
### When to Use Sealed Interface
|
|
|
|
**Examples from codebase:**
|
|
|
|
1. **SignerResult<T>** - Generic result types needing variance
|
|
2. **RelayUrlNormalizer.Result** - Binary result with no shared state
|
|
|
|
**Characteristics:**
|
|
- Need generics with variance (`out`, `in`)
|
|
- No common state needed
|
|
- Multiple inheritance possible
|
|
- Contract/capability representation
|
|
|
|
---
|
|
|
|
## Patterns
|
|
|
|
### Pattern: State Machine
|
|
|
|
```kotlin
|
|
sealed class ConnectionState {
|
|
data object Disconnected : ConnectionState()
|
|
data object Connecting : ConnectionState()
|
|
data class Connected(val relay: String) : ConnectionState()
|
|
data class Failed(val error: String) : ConnectionState()
|
|
}
|
|
|
|
// Allowed transitions
|
|
fun transition(from: ConnectionState, event: Event): ConnectionState {
|
|
return when (from) {
|
|
is ConnectionState.Disconnected -> {
|
|
when (event) {
|
|
is Event.Connect -> ConnectionState.Connecting
|
|
else -> from
|
|
}
|
|
}
|
|
is ConnectionState.Connecting -> {
|
|
when (event) {
|
|
is Event.Success -> ConnectionState.Connected(event.relay)
|
|
is Event.Error -> ConnectionState.Failed(event.message)
|
|
is Event.Cancel -> ConnectionState.Disconnected
|
|
else -> from
|
|
}
|
|
}
|
|
is ConnectionState.Connected -> {
|
|
when (event) {
|
|
is Event.Disconnect -> ConnectionState.Disconnected
|
|
is Event.Error -> ConnectionState.Failed(event.message)
|
|
else -> from
|
|
}
|
|
}
|
|
is ConnectionState.Failed -> {
|
|
when (event) {
|
|
is Event.Retry -> ConnectionState.Connecting
|
|
is Event.Cancel -> ConnectionState.Disconnected
|
|
else -> from
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern: Result Type
|
|
|
|
```kotlin
|
|
sealed interface Result<out T> {
|
|
data class Success<T>(val data: T) : Result<T>
|
|
data class Error(val exception: Exception) : Result<Nothing>
|
|
data object Loading : Result<Nothing>
|
|
}
|
|
|
|
// Extension functions
|
|
fun <T> Result<T>.getOrNull(): T? = when (this) {
|
|
is Result.Success -> data
|
|
else -> null
|
|
}
|
|
|
|
fun <T> Result<T>.getOrThrow(): T = when (this) {
|
|
is Result.Success -> data
|
|
is Result.Error -> throw exception
|
|
is Result.Loading -> error("Still loading")
|
|
}
|
|
|
|
fun <T, R> Result<T>.map(transform: (T) -> R): Result<R> = when (this) {
|
|
is Result.Success -> Result.Success(transform(data))
|
|
is Result.Error -> this
|
|
is Result.Loading -> Result.Loading
|
|
}
|
|
```
|
|
|
|
### Pattern: Tagged Union (Discriminated Union)
|
|
|
|
```kotlin
|
|
sealed class Command {
|
|
data class SendEvent(val event: Event) : Command()
|
|
data class Subscribe(val filters: List<Filter>) : Command()
|
|
data class Unsubscribe(val subId: String) : Command()
|
|
data object Close : Command()
|
|
|
|
fun toJson(): String = when (this) {
|
|
is SendEvent -> """["EVENT",${event.toJson()}]"""
|
|
is Subscribe -> """["REQ","sub",${filters.joinToString { it.toJson() }}]"""
|
|
is Unsubscribe -> """["CLOSE","$subId"]"""
|
|
is Close -> """["CLOSE"]"""
|
|
}
|
|
}
|
|
```
|
|
|
|
### Pattern: Nested Sealed Hierarchies
|
|
|
|
```kotlin
|
|
sealed interface UiState {
|
|
sealed interface Loading : UiState {
|
|
data object Initial : Loading
|
|
data class Refreshing(val currentData: List<Item>) : Loading
|
|
}
|
|
|
|
sealed interface Content : UiState {
|
|
data class Success(val data: List<Item>) : Content
|
|
data object Empty : Content
|
|
}
|
|
|
|
sealed interface Error : UiState {
|
|
data class Network(val message: String) : Error
|
|
data class Server(val code: Int, val message: String) : Error
|
|
}
|
|
}
|
|
|
|
// Usage
|
|
fun renderUi(state: UiState) {
|
|
when (state) {
|
|
is UiState.Loading.Initial -> showFullScreenLoader()
|
|
is UiState.Loading.Refreshing -> showRefreshIndicator(state.currentData)
|
|
is UiState.Content.Success -> showList(state.data)
|
|
is UiState.Content.Empty -> showEmptyState()
|
|
is UiState.Error.Network -> showNetworkError(state.message)
|
|
is UiState.Error.Server -> showServerError(state.code, state.message)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## All Sealed Types in Quartz
|
|
|
|
**Complete list of sealed types found in codebase:**
|
|
|
|
### Commons
|
|
- AccountState (class)
|
|
|
|
### Quartz
|
|
- BaseZapSplitSetup (class)
|
|
- MuteTag (class)
|
|
- BookmarkIdTag (class)
|
|
- SignerResult (interface)
|
|
- VerificationState (class)
|
|
- CacheResults (class)
|
|
- SignerExceptions (class)
|
|
- RelayUrlNormalizer.Result (interface)
|
|
|
|
**Total:** 8 sealed types (7 classes, 1 interface)
|
|
|
|
---
|
|
|
|
## Decision Tree
|
|
|
|
```
|
|
Need to represent variants of a concept?
|
|
YES → Use sealed type
|
|
NO → Regular class/interface
|
|
|
|
Variants have different data?
|
|
YES → sealed class or sealed interface
|
|
NO → enum (if simple constants)
|
|
|
|
Need generics with variance (out/in)?
|
|
YES → sealed interface
|
|
NO → sealed class (simpler)
|
|
|
|
Need common constructor/properties?
|
|
YES → sealed class
|
|
NO → sealed interface
|
|
|
|
Need multiple inheritance?
|
|
YES → sealed interface
|
|
NO → Either works
|
|
|
|
Representing state machine?
|
|
→ sealed class (state transitions)
|
|
|
|
Representing result/error types?
|
|
→ sealed interface (if generic, else class)
|
|
|
|
Representing tag/command variants?
|
|
→ sealed class (common structure)
|
|
```
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [Sealed Classes | Kotlin Docs](https://kotlinlang.org/docs/sealed-classes.html)
|
|
- [Effective Kotlin: Sealed Classes](https://kt.academy/article/ek-sealed-classes)
|
|
- [Complete Guide: Sealed Classes & Interfaces 2025](https://proandroiddev.com/complete-technical-guide-sealed-classes-sealed-interfaces-enums-in-kotlin-28ffc39116df)
|