From 7f165ebf6658f084bb9cd24088f74f915713e1ab Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Wed, 5 Aug 2026 16:39:19 +0200 Subject: [PATCH 1/2] feat: add categorizable code to Error.GenericError Add a machine-readable `code: Int` to `Error.GenericError` so callers can branch on the kind of error without matching the message string. Implemented as a body property with a public secondary constructor and a private setter, keeping the change additive: the primary constructor, copy(), componentN, equals/hashCode/toString are unchanged, so it stays binary-backward-compatible (API dumps regenerated, additions only). copyWithMessage() now preserves the code; the generated copy() does not (documented on the property). --- stream-result/api/android/stream-result.api | 7 +++++ stream-result/api/jvm/stream-result.api | 7 +++++ .../kotlin/io/getstream/result/Error.kt | 31 +++++++++++++++++-- 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/stream-result/api/android/stream-result.api b/stream-result/api/android/stream-result.api index 873e5cf..d1235ac 100644 --- a/stream-result/api/android/stream-result.api +++ b/stream-result/api/android/stream-result.api @@ -3,16 +3,23 @@ public abstract class io/getstream/result/Error { } public final class io/getstream/result/Error$GenericError : io/getstream/result/Error { + public static final field Companion Lio/getstream/result/Error$GenericError$Companion; + public static final field UNCATEGORIZED I public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;I)V public final fun component1 ()Ljava/lang/String; public final fun copy (Ljava/lang/String;)Lio/getstream/result/Error$GenericError; public static synthetic fun copy$default (Lio/getstream/result/Error$GenericError;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/Error$GenericError; public fun equals (Ljava/lang/Object;)Z + public final fun getCode ()I public fun getMessage ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } +public final class io/getstream/result/Error$GenericError$Companion { +} + public final class io/getstream/result/Error$NetworkError : io/getstream/result/Error { public static final field Companion Lio/getstream/result/Error$NetworkError$Companion; public static final field UNKNOWN_STATUS_CODE I diff --git a/stream-result/api/jvm/stream-result.api b/stream-result/api/jvm/stream-result.api index 873e5cf..d1235ac 100644 --- a/stream-result/api/jvm/stream-result.api +++ b/stream-result/api/jvm/stream-result.api @@ -3,16 +3,23 @@ public abstract class io/getstream/result/Error { } public final class io/getstream/result/Error$GenericError : io/getstream/result/Error { + public static final field Companion Lio/getstream/result/Error$GenericError$Companion; + public static final field UNCATEGORIZED I public fun (Ljava/lang/String;)V + public fun (Ljava/lang/String;I)V public final fun component1 ()Ljava/lang/String; public final fun copy (Ljava/lang/String;)Lio/getstream/result/Error$GenericError; public static synthetic fun copy$default (Lio/getstream/result/Error$GenericError;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/Error$GenericError; public fun equals (Ljava/lang/Object;)Z + public final fun getCode ()I public fun getMessage ()Ljava/lang/String; public fun hashCode ()I public fun toString ()Ljava/lang/String; } +public final class io/getstream/result/Error$GenericError$Companion { +} + public final class io/getstream/result/Error$NetworkError : io/getstream/result/Error { public static final field Companion Lio/getstream/result/Error$NetworkError$Companion; public static final field UNKNOWN_STATUS_CODE I diff --git a/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt b/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt index 3294ffb..98f2d54 100644 --- a/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt +++ b/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt @@ -30,7 +30,34 @@ public sealed class Error { * * @param message The message describing the error. */ - public data class GenericError(override val message: String) : Error() + public data class GenericError(override val message: String) : Error() { + + /** + * A stable, machine-readable code categorizing the error, or [UNCATEGORIZED] when it has not + * been categorized. Read it to branch on the kind of error without matching [message]. + * + * Populated by the SDK at construction via the secondary constructor; immutable afterwards. + * + * Note: [code] is not a primary-constructor property, so it does not take part in the + * generated [equals]/[hashCode]/[toString] and is NOT carried by the generated [copy] — a + * copied instance resets to [UNCATEGORIZED]. Use [copyWithMessage], which preserves it. + */ + public var code: Int = UNCATEGORIZED + private set + + /** + * @param message The message describing the error. + * @param code A stable, machine-readable [code] categorizing the error. + */ + public constructor(message: String, code: Int) : this(message) { + this.code = code + } + + public companion object { + /** Default [code] value, meaning the error has not been categorized. */ + public const val UNCATEGORIZED: Int = 0 + } + } /** * An error that contains a message and cause. @@ -124,7 +151,7 @@ public sealed class Error { */ public fun Error.copyWithMessage(message: String): Error { return when (this) { - is Error.GenericError -> this.copy(message = message) + is Error.GenericError -> Error.GenericError(message = message, code = code) is Error.NetworkError -> this.copy(message = message) is Error.ThrowableError -> this.copy(message = message) } From 5d515141e9d70711d7ac1016d29b06b0d43b5f7f Mon Sep 17 00:00:00 2001 From: Aleksandar Apostolov Date: Thu, 6 Aug 2026 13:21:49 +0200 Subject: [PATCH 2/2] feat: include code in GenericError equals/hashCode/toString MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Manually implement equals/hashCode/toString on GenericError so the body-property `code` is reflected in them (the generated versions ignore non-constructor properties). Matches the existing manual implementations on ThrowableError/NetworkError. No public API change (identical signatures). copy() still does not carry code — inherent to data classes. Addresses review feedback on #208. --- .../kotlin/io/getstream/result/Error.kt | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt b/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt index 98f2d54..7004c09 100644 --- a/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt +++ b/stream-result/src/commonMain/kotlin/io/getstream/result/Error.kt @@ -38,9 +38,10 @@ public sealed class Error { * * Populated by the SDK at construction via the secondary constructor; immutable afterwards. * - * Note: [code] is not a primary-constructor property, so it does not take part in the - * generated [equals]/[hashCode]/[toString] and is NOT carried by the generated [copy] — a - * copied instance resets to [UNCATEGORIZED]. Use [copyWithMessage], which preserves it. + * [code] is included in this type's [equals]/[hashCode]/[toString] (implemented manually, + * since it is not a primary-constructor property). It is still NOT carried by the generated + * [copy] — a copied instance resets to [UNCATEGORIZED]; use [copyWithMessage], which preserves + * it. */ public var code: Int = UNCATEGORIZED private set @@ -53,6 +54,30 @@ public sealed class Error { this.code = code } + @StreamHandsOff( + "'code' is declared in the class body, not the primary constructor, so the generated" + + " equals/hashCode/toString would ignore it; they are implemented manually to include it." + ) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as GenericError + return message == other.message && code == other.code + } + + @StreamHandsOff( + "'code' is declared in the class body, not the primary constructor, so the generated" + + " equals/hashCode/toString would ignore it; they are implemented manually to include it." + ) + override fun hashCode(): Int { + return 31 * message.hashCode() + code + } + + override fun toString(): String { + return "GenericError(message=$message, code=$code)" + } + public companion object { /** Default [code] value, meaning the error has not been categorized. */ public const val UNCATEGORIZED: Int = 0