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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ The components are in different stages of development, and some may not be fully
- 🧪 Testing: The component is being tested and may have known issues.
- ✅ Stable: The component is stable and ready for production use.

### [Core Components](components/core/README.md)

| Component | Status | Module | Maven Coordinate | Description |
|----------------------------------------------|-------------------|----------------------------|-------------------------------------------|-------------------------------------------------------------------------|
| [Outcome](components/core/outcome/README.md) | 🛠 In development | `:components:core:outcome` | `net.thunderbird.components.core:outcome` | Small result type with a flexible failure type, unlike Kotlin `Result`. |

## Contributing

We welcome contributions to this repository! If you would like to contribute, please read our
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package net.thunderbird.gradle.plugin.library.kmp

import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import net.thunderbird.gradle.plugin.ProjectConfig
import org.gradle.api.Action
import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.NamedDomainObjectProvider
import org.gradle.api.Project
import org.gradle.api.artifacts.ExternalModuleDependency
import org.gradle.api.artifacts.MinimalExternalModuleDependency
import org.gradle.api.plugins.ExtensionAware
Expand All @@ -20,6 +22,17 @@ fun KotlinMultiplatformExtension.android(configure: Action<KotlinMultiplatformAn
(this as ExtensionAware).extensions.configure("android", configure)
}

fun KotlinMultiplatformAndroidLibraryTarget.namespaceByPath(project: Project) {
val pathSegments = project.path.split(':')
.filter { it.isNotBlank() }
.flatMap { it.split('-') }
.filter { it.isNotBlank() }

namespace = listOf(ProjectConfig.group)
.plus(pathSegments)
.joinToString(separator = ".")
}

/**
* Creates a dependency with the given configuration.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class LibraryKmpPlugin : Plugin<Project> {
}

android {
namespaceByPath(target)
minSdk = ProjectConfig.Android.sdkMin
compileSdk = ProjectConfig.Android.sdkCompile

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.thunderbird.gradle.plugin.library.kmp.compose
import net.thunderbird.gradle.plugin.ProjectConfig
import net.thunderbird.gradle.plugin.library.kmp.android
import net.thunderbird.gradle.plugin.library.kmp.androidHostTest
import net.thunderbird.gradle.plugin.library.kmp.namespaceByPath
import net.thunderbird.gradle.plugin.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
Expand Down Expand Up @@ -48,6 +49,7 @@ class LibraryKmpComposePlugin : Plugin<Project> {
}

android {
namespaceByPath(target)
minSdk = ProjectConfig.Android.sdkMin
compileSdk = ProjectConfig.Android.sdkCompile

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.assign
import org.gradle.kotlin.dsl.dependencies
import org.gradle.kotlin.dsl.named
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.withType

/**
Expand All @@ -19,6 +21,7 @@ import org.gradle.kotlin.dsl.withType
class DetektPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("base")
pluginManager.apply("dev.detekt")

dependencies {
Expand Down Expand Up @@ -56,8 +59,6 @@ class DetektPlugin : Plugin<Project> {
sarif.required.set(true)
markdown.required.set(true)
}

tasks.getByName("build").dependsOn(this)
}

withType<DetektCreateBaselineTask>().configureEach {
Expand All @@ -70,12 +71,16 @@ class DetektPlugin : Plugin<Project> {
exclude(defaultExcludes)
}

register("detektAll") {
val detektAll = register("detektAll") {
group = "verification"
description = "Runs detekt on this project"

dependsOn(tasks.withType<Detekt>())
}

named("check") {
dependsOn(detektAll)
}
}
}
}
Expand Down
3 changes: 1 addition & 2 deletions components/bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ plugins {

dependencies {
constraints {
// Add constraints for published components here, e.g.:
// api("net.thunderbird:some-component:${project.version}")
api(projects.components.core.outcome)
}
}
10 changes: 10 additions & 0 deletions components/core/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Thunderbird Core Components

Core components provide small, foundational building blocks that can be reused by higher-level
components and applications.

## Components

| Component | Module | Maven Coordinate | Description |
|-----------|----------------------|-------------------------------------------|----------------------------------------------------------------------------|
| [Outcome](outcome/README.md) | `:components:core:outcome` | `net.thunderbird.components.core:outcome` | Small result type with a flexible failure type, unlike Kotlin `Result`. |
4 changes: 4 additions & 0 deletions components/core/outcome/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Changelog

## Unreleased

62 changes: 62 additions & 0 deletions components/core/outcome/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Thunderbird Outcome Component

Outcome provides a small result type for operations that need a domain-specific failure type.

Use it when Kotlin `Result` is too restrictive because callers need a typed failure value instead
of only a `Throwable`.

## Dependency

```kotlin
dependencies {
implementation("net.thunderbird.components.core:outcome:<version>")
}
```

## Usage

```kotlin
import net.thunderbird.components.core.outcome.Outcome
import net.thunderbird.components.core.outcome.flatMapSuccess
import net.thunderbird.components.core.outcome.fold
import net.thunderbird.components.core.outcome.mapFailure
import net.thunderbird.components.core.outcome.mapSuccess

sealed interface ReadError {
data object Missing : ReadError
data object Invalid : ReadError
}

fun readValue(): Outcome<String, ReadError> {
return Outcome.success("42")
}

val result = readValue()
.mapSuccess { it.toInt() }
.mapFailure { error, _ -> error }
.flatMapSuccess { value ->
if (value > 0) {
Outcome.success(value)
} else {
Outcome.failure(ReadError.Invalid)
}
}
.fold(
onSuccess = { value -> "Read value: $value" },
onFailure = { error -> "Could not read value: $error" },
)
```

## Error Handling

Prefer domain-specific failure types over plain strings or exceptions:

```kotlin
sealed interface FileReadError {
data object NotFound : FileReadError
data object PermissionDenied : FileReadError
data class Unknown(val message: String) : FileReadError
}
```

This keeps callers exhaustive and makes expected failure states visible at the API boundary.
55 changes: 55 additions & 0 deletions components/core/outcome/api/jvm/outcome.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
public abstract interface class net/thunderbird/components/core/outcome/Outcome {
public static final field Companion Lnet/thunderbird/components/core/outcome/Outcome$Companion;
public fun isFailure ()Z
public fun isSuccess ()Z
}

public final class net/thunderbird/components/core/outcome/Outcome$Companion {
public final fun failure (Ljava/lang/Object;)Lnet/thunderbird/components/core/outcome/Outcome;
public final fun success (Ljava/lang/Object;)Lnet/thunderbird/components/core/outcome/Outcome;
}

public final class net/thunderbird/components/core/outcome/Outcome$DefaultImpls {
public static fun isFailure (Lnet/thunderbird/components/core/outcome/Outcome;)Z
public static fun isSuccess (Lnet/thunderbird/components/core/outcome/Outcome;)Z
}

public final class net/thunderbird/components/core/outcome/Outcome$Failure : net/thunderbird/components/core/outcome/Outcome {
public fun <init> (Ljava/lang/Object;Ljava/lang/Object;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Object;
public final fun component2 ()Ljava/lang/Object;
public final fun copy (Ljava/lang/Object;Ljava/lang/Object;)Lnet/thunderbird/components/core/outcome/Outcome$Failure;
public static synthetic fun copy$default (Lnet/thunderbird/components/core/outcome/Outcome$Failure;Ljava/lang/Object;Ljava/lang/Object;ILjava/lang/Object;)Lnet/thunderbird/components/core/outcome/Outcome$Failure;
public fun equals (Ljava/lang/Object;)Z
public final fun getCause ()Ljava/lang/Object;
public final fun getError ()Ljava/lang/Object;
public fun hashCode ()I
public fun isFailure ()Z
public fun isSuccess ()Z
public fun toString ()Ljava/lang/String;
}

public final class net/thunderbird/components/core/outcome/Outcome$Success : net/thunderbird/components/core/outcome/Outcome {
public fun <init> (Ljava/lang/Object;)V
public final fun component1 ()Ljava/lang/Object;
public final fun copy (Ljava/lang/Object;)Lnet/thunderbird/components/core/outcome/Outcome$Success;
public static synthetic fun copy$default (Lnet/thunderbird/components/core/outcome/Outcome$Success;Ljava/lang/Object;ILjava/lang/Object;)Lnet/thunderbird/components/core/outcome/Outcome$Success;
public fun equals (Ljava/lang/Object;)Z
public final fun getData ()Ljava/lang/Object;
public fun hashCode ()I
public fun isFailure ()Z
public fun isSuccess ()Z
public fun toString ()Ljava/lang/String;
}

public final class net/thunderbird/components/core/outcome/OutcomeKt {
public static final fun flatMapSuccess (Lnet/thunderbird/components/core/outcome/Outcome;Lkotlin/jvm/functions/Function1;)Lnet/thunderbird/components/core/outcome/Outcome;
public static final fun fold (Lnet/thunderbird/components/core/outcome/Outcome;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
public static final fun handle (Lnet/thunderbird/components/core/outcome/Outcome;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V
public static final fun handleAsync (Lnet/thunderbird/components/core/outcome/Outcome;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun map (Lnet/thunderbird/components/core/outcome/Outcome;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lnet/thunderbird/components/core/outcome/Outcome;
public static final fun mapFailure (Lnet/thunderbird/components/core/outcome/Outcome;Lkotlin/jvm/functions/Function2;)Lnet/thunderbird/components/core/outcome/Outcome;
public static final fun mapSuccess (Lnet/thunderbird/components/core/outcome/Outcome;Lkotlin/jvm/functions/Function1;)Lnet/thunderbird/components/core/outcome/Outcome;
}

56 changes: 56 additions & 0 deletions components/core/outcome/api/outcome.klib.api
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Klib ABI Dump
// Targets: [iosArm64, iosSimulatorArm64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
// - Show declarations: true

// Library unique name: <net.thunderbird.components.core:outcome>
sealed interface <#A: out kotlin/Any?, #B: out kotlin/Any?> net.thunderbird.components.core.outcome/Outcome { // net.thunderbird.components.core.outcome/Outcome|null[0]
open val isFailure // net.thunderbird.components.core.outcome/Outcome.isFailure|{}isFailure[0]
open fun <get-isFailure>(): kotlin/Boolean // net.thunderbird.components.core.outcome/Outcome.isFailure.<get-isFailure>|<get-isFailure>(){}[0]
open val isSuccess // net.thunderbird.components.core.outcome/Outcome.isSuccess|{}isSuccess[0]
open fun <get-isSuccess>(): kotlin/Boolean // net.thunderbird.components.core.outcome/Outcome.isSuccess.<get-isSuccess>|<get-isSuccess>(){}[0]

final class <#A1: out kotlin/Any?> Failure : net.thunderbird.components.core.outcome/Outcome<kotlin/Nothing, #A1> { // net.thunderbird.components.core.outcome/Outcome.Failure|null[0]
constructor <init>(#A1, kotlin/Any? = ...) // net.thunderbird.components.core.outcome/Outcome.Failure.<init>|<init>(1:0;kotlin.Any?){}[0]

final val cause // net.thunderbird.components.core.outcome/Outcome.Failure.cause|{}cause[0]
final fun <get-cause>(): kotlin/Any? // net.thunderbird.components.core.outcome/Outcome.Failure.cause.<get-cause>|<get-cause>(){}[0]
final val error // net.thunderbird.components.core.outcome/Outcome.Failure.error|{}error[0]
final fun <get-error>(): #A1 // net.thunderbird.components.core.outcome/Outcome.Failure.error.<get-error>|<get-error>(){}[0]

final fun component1(): #A1 // net.thunderbird.components.core.outcome/Outcome.Failure.component1|component1(){}[0]
final fun component2(): kotlin/Any? // net.thunderbird.components.core.outcome/Outcome.Failure.component2|component2(){}[0]
final fun copy(#A1 = ..., kotlin/Any? = ...): net.thunderbird.components.core.outcome/Outcome.Failure<#A1> // net.thunderbird.components.core.outcome/Outcome.Failure.copy|copy(1:0;kotlin.Any?){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // net.thunderbird.components.core.outcome/Outcome.Failure.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // net.thunderbird.components.core.outcome/Outcome.Failure.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // net.thunderbird.components.core.outcome/Outcome.Failure.toString|toString(){}[0]
}

final class <#A1: out kotlin/Any?> Success : net.thunderbird.components.core.outcome/Outcome<#A1, kotlin/Nothing> { // net.thunderbird.components.core.outcome/Outcome.Success|null[0]
constructor <init>(#A1) // net.thunderbird.components.core.outcome/Outcome.Success.<init>|<init>(1:0){}[0]

final val data // net.thunderbird.components.core.outcome/Outcome.Success.data|{}data[0]
final fun <get-data>(): #A1 // net.thunderbird.components.core.outcome/Outcome.Success.data.<get-data>|<get-data>(){}[0]

final fun component1(): #A1 // net.thunderbird.components.core.outcome/Outcome.Success.component1|component1(){}[0]
final fun copy(#A1 = ...): net.thunderbird.components.core.outcome/Outcome.Success<#A1> // net.thunderbird.components.core.outcome/Outcome.Success.copy|copy(1:0){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // net.thunderbird.components.core.outcome/Outcome.Success.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // net.thunderbird.components.core.outcome/Outcome.Success.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // net.thunderbird.components.core.outcome/Outcome.Success.toString|toString(){}[0]
}

final object Companion { // net.thunderbird.components.core.outcome/Outcome.Companion|null[0]
final fun <#A2: kotlin/Any?> failure(#A2): net.thunderbird.components.core.outcome/Outcome<kotlin/Nothing, #A2> // net.thunderbird.components.core.outcome/Outcome.Companion.failure|failure(0:0){0§<kotlin.Any?>}[0]
final fun <#A2: kotlin/Any?> success(#A2): net.thunderbird.components.core.outcome/Outcome<#A2, kotlin/Nothing> // net.thunderbird.components.core.outcome/Outcome.Companion.success|success(0:0){0§<kotlin.Any?>}[0]
}
}

final fun <#A: kotlin/Any?, #B: kotlin/Any?> (net.thunderbird.components.core.outcome/Outcome<#A, #B>).net.thunderbird.components.core.outcome/handle(kotlin/Function1<#A, kotlin/Unit>, kotlin/Function1<#B, kotlin/Unit>) // net.thunderbird.components.core.outcome/handle|handle@net.thunderbird.components.core.outcome.Outcome<0:0,0:1>(kotlin.Function1<0:0,kotlin.Unit>;kotlin.Function1<0:1,kotlin.Unit>){0§<kotlin.Any?>;1§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?, #D: kotlin/Any?> (net.thunderbird.components.core.outcome/Outcome<#A, #B>).net.thunderbird.components.core.outcome/map(kotlin/Function1<#A, #C>, kotlin/Function2<#B, kotlin/Any?, #D>): net.thunderbird.components.core.outcome/Outcome<#C, #D> // net.thunderbird.components.core.outcome/map|map@net.thunderbird.components.core.outcome.Outcome<0:0,0:1>(kotlin.Function1<0:0,0:2>;kotlin.Function2<0:1,kotlin.Any?,0:3>){0§<kotlin.Any?>;1§<kotlin.Any?>;2§<kotlin.Any?>;3§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (net.thunderbird.components.core.outcome/Outcome<#A, #B>).net.thunderbird.components.core.outcome/flatMapSuccess(kotlin/Function1<#A, net.thunderbird.components.core.outcome/Outcome<#C, #B>>): net.thunderbird.components.core.outcome/Outcome<#C, #B> // net.thunderbird.components.core.outcome/flatMapSuccess|flatMapSuccess@net.thunderbird.components.core.outcome.Outcome<0:0,0:1>(kotlin.Function1<0:0,net.thunderbird.components.core.outcome.Outcome<0:2,0:1>>){0§<kotlin.Any?>;1§<kotlin.Any?>;2§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (net.thunderbird.components.core.outcome/Outcome<#A, #B>).net.thunderbird.components.core.outcome/fold(kotlin/Function1<#A, #C>, kotlin/Function1<#B, #C>): #C // net.thunderbird.components.core.outcome/fold|fold@net.thunderbird.components.core.outcome.Outcome<0:0,0:1>(kotlin.Function1<0:0,0:2>;kotlin.Function1<0:1,0:2>){0§<kotlin.Any?>;1§<kotlin.Any?>;2§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (net.thunderbird.components.core.outcome/Outcome<#A, #B>).net.thunderbird.components.core.outcome/mapFailure(kotlin/Function2<#B, kotlin/Any?, #C>): net.thunderbird.components.core.outcome/Outcome<#A, #C> // net.thunderbird.components.core.outcome/mapFailure|mapFailure@net.thunderbird.components.core.outcome.Outcome<0:0,0:1>(kotlin.Function2<0:1,kotlin.Any?,0:2>){0§<kotlin.Any?>;1§<kotlin.Any?>;2§<kotlin.Any?>}[0]
final inline fun <#A: kotlin/Any?, #B: kotlin/Any?, #C: kotlin/Any?> (net.thunderbird.components.core.outcome/Outcome<#A, #C>).net.thunderbird.components.core.outcome/mapSuccess(kotlin/Function1<#A, #B>): net.thunderbird.components.core.outcome/Outcome<#B, #C> // net.thunderbird.components.core.outcome/mapSuccess|mapSuccess@net.thunderbird.components.core.outcome.Outcome<0:0,0:2>(kotlin.Function1<0:0,0:1>){0§<kotlin.Any?>;1§<kotlin.Any?>;2§<kotlin.Any?>}[0]
final suspend fun <#A: kotlin/Any?, #B: kotlin/Any?> (net.thunderbird.components.core.outcome/Outcome<#A, #B>).net.thunderbird.components.core.outcome/handleAsync(kotlin.coroutines/SuspendFunction1<#A, kotlin/Unit>, kotlin.coroutines/SuspendFunction1<#B, kotlin/Unit>) // net.thunderbird.components.core.outcome/handleAsync|handleAsync@net.thunderbird.components.core.outcome.Outcome<0:0,0:1>(kotlin.coroutines.SuspendFunction1<0:0,kotlin.Unit>;kotlin.coroutines.SuspendFunction1<0:1,kotlin.Unit>){0§<kotlin.Any?>;1§<kotlin.Any?>}[0]
4 changes: 4 additions & 0 deletions components/core/outcome/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
plugins {
alias(libs.plugins.tb.library.kmp)
alias(libs.plugins.testBalloon)
}
Loading