Files
amethyst/.claude/skills/kotlin-expert/references/sealed-class-catalog.md
T
2025-12-30 15:18:48 +02:00

13 KiB

Sealed Class Catalog

Comprehensive list of sealed types in AmethystMultiplatform with usage patterns.

Table of Contents


State Management

AccountState (Sealed Class)

File: commons/src/jvmAndroid/kotlin/com/vitorpamplona/amethyst/commons/account/AccountManager.kt:36-46

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:

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

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

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:

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

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

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

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

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:

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 - 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

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

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)

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

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