-
Notifications
You must be signed in to change notification settings - Fork 378
feat: [SDK-4988] add OneSignalResult, OneSignalError, and the per-method payloads #2710
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
abdulraqeeb33
wants to merge
1
commit into
ar/sdk-4986
Choose a base branch
from
ar/sdk-4988
base: ar/sdk-4986
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
160 changes: 160 additions & 0 deletions
160
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalError.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| package com.onesignal | ||
|
|
||
| /** Whether the SDK produced a failure locally or OneSignal's backend returned it. */ | ||
| enum class ErrorSource { | ||
| CLIENT, | ||
| BACKEND, | ||
| } | ||
|
|
||
| /** | ||
| * The catalog of failure codes shared by every OneSignal SDK. | ||
| * | ||
| * An enum rather than a sealed hierarchy so that Java callers get a native `switch` and the | ||
| * wrapper bridges get a trivial name-to-string marshal. The backend half of the catalog is | ||
| * deliberately *not* modelled here — see [BACKEND_ERROR]. | ||
| */ | ||
| enum class ErrorCode(val source: ErrorSource) { | ||
| /** [IOneSignal.initWithContextSuspend] has not been called. */ | ||
| NOT_INITIALIZED(ErrorSource.CLIENT), | ||
|
|
||
| /** | ||
| * Device storage was locked, so the SDK could not read or write its own preferences. | ||
| * Transient: the same call generally succeeds once the device is unlocked. | ||
| */ | ||
| STORAGE_LOCKED(ErrorSource.CLIENT), | ||
|
|
||
| /** A caller-supplied argument failed validation before any request was made. */ | ||
| INVALID_ARGUMENT(ErrorSource.CLIENT), | ||
|
|
||
| /** OneSignal rejected the request. The catalog code is on [OneSignalError.Detail.backendCode]. */ | ||
| BACKEND_ERROR(ErrorSource.BACKEND), | ||
|
|
||
| /** No more specific code applies. Callers should surface [OneSignalError.Detail.message]. */ | ||
| UNKNOWN(ErrorSource.CLIENT), | ||
| } | ||
|
|
||
| /** | ||
| * Describes why a OneSignal call failed. | ||
| * | ||
| * One request can fail for several reasons at once, so [error] is a list of [Detail]. Everything | ||
| * the SDK raises locally has exactly one reason, which [first] reads without the indexing | ||
| * ceremony. | ||
| * | ||
| * On the wire this is the list itself, sitting under the envelope's `error` key: | ||
| * | ||
| * ```json | ||
| * { "success": false, "data": null, | ||
| * "error": [ { "code": "STORAGE_LOCKED", "source": "CLIENT", "backendCode": null, "message": "..." } ] } | ||
| * ``` | ||
| */ | ||
| class OneSignalError internal constructor( | ||
| error: List<Detail>, | ||
| /** | ||
| * The throwable behind the failure, when there was one. | ||
| * | ||
| * Deliberately absent from [toList]: a stack trace cannot cross the wrapper bridges, and the | ||
| * wire schema has to stay identical across every SDK. This exists so that native Kotlin and | ||
| * Java callers do not lose the stack when the suspend APIs report a failure instead of | ||
| * throwing it. | ||
| */ | ||
| val cause: Throwable? = null, | ||
| ) { | ||
| /** | ||
| * Why the call failed. Never empty. | ||
| * | ||
| * Copied rather than aliased so that a caller holding the original list cannot empty it | ||
| * afterwards and leave [first] throwing. | ||
| */ | ||
| val error: List<Detail> = error.toList() | ||
|
|
||
| init { | ||
| // [first] is documented as always safe to read, and the wire projection of an empty error | ||
| // would claim failure while explaining nothing. Both factories guard this; the check is | ||
| // here so a future caller of the constructor cannot quietly break the invariant. | ||
| require(this.error.isNotEmpty()) { "OneSignalError requires at least one Detail." } | ||
| } | ||
|
|
||
| /** | ||
| * A single reason a call failed. | ||
| * | ||
| * Nested rather than top-level so the name cannot collide with `kotlin.Error`, which is | ||
| * auto-imported everywhere, or shadow `java.lang.Error` in a Java file that imports it. | ||
| */ | ||
| class Detail internal constructor( | ||
| /** A stable code, safe to branch on. Never localized. */ | ||
| val code: ErrorCode, | ||
| /** | ||
| * The backend's catalog code, present only when [code] is [ErrorCode.BACKEND_ERROR]. | ||
| * | ||
| * Left as a raw number on purpose: the backend adds codes on its own schedule, and an SDK | ||
| * release must not be the thing that unblocks recognizing one. | ||
| */ | ||
| val backendCode: Int? = null, | ||
| /** A human-readable description intended for logs and diagnostics, not for end users. */ | ||
| val message: String? = null, | ||
| ) { | ||
| /** Projects this reason onto the cross-SDK wire shape consumed by the wrapper bridges. */ | ||
| fun toMap(): Map<String, Any?> = | ||
| mapOf( | ||
| KEY_CODE to code.name, | ||
| KEY_SOURCE to code.source.name, | ||
| KEY_BACKEND_CODE to backendCode, | ||
| KEY_MESSAGE to message, | ||
| ) | ||
|
|
||
| override fun toString(): String = "Detail(code=$code, backendCode=$backendCode, message=$message)" | ||
|
|
||
| internal companion object { | ||
| // Private because `const val` in an internal companion still compiles to a public | ||
| // static field, which would leak the wire keys into the customer-facing API surface. | ||
| private const val KEY_CODE = "code" | ||
| private const val KEY_SOURCE = "source" | ||
| private const val KEY_BACKEND_CODE = "backendCode" | ||
| private const val KEY_MESSAGE = "message" | ||
|
|
||
| /** | ||
| * Rebuilds a reason from its wire shape. | ||
| * | ||
| * An unrecognized code degrades to [ErrorCode.UNKNOWN] rather than throwing, so a | ||
| * wrapper built against an older SDK survives a newer producer emitting a code it has | ||
| * never heard of. The original text is preserved on [message] either way. | ||
| */ | ||
| fun fromMap(map: Map<String, Any?>): Detail = | ||
| Detail( | ||
| code = codeOf(map[KEY_CODE] as? String), | ||
| backendCode = (map[KEY_BACKEND_CODE] as? Number)?.toInt(), | ||
| message = map[KEY_MESSAGE] as? String, | ||
| ) | ||
|
|
||
| private fun codeOf(name: String?): ErrorCode = ErrorCode.entries.firstOrNull { it.name == name } ?: ErrorCode.UNKNOWN | ||
| } | ||
| } | ||
|
|
||
| /** The first reason, which is the only one for every failure the SDK raises locally. */ | ||
| val first: Detail | ||
| get() = error.first() | ||
|
|
||
| /** Projects this error onto the cross-SDK wire shape consumed by the wrapper bridges. */ | ||
| fun toList(): List<Map<String, Any?>> = error.map { it.toMap() } | ||
|
|
||
| override fun toString(): String = "OneSignalError(error=$error)" | ||
|
|
||
| internal companion object { | ||
| /** Builds a single-reason error, which is the shape of everything the SDK raises locally. */ | ||
| fun of( | ||
| code: ErrorCode, | ||
| message: String? = null, | ||
| backendCode: Int? = null, | ||
| cause: Throwable? = null, | ||
| ): OneSignalError = OneSignalError(listOf(Detail(code, backendCode, message)), cause) | ||
|
|
||
| /** | ||
| * Rebuilds an error from its wire shape. A payload carrying no recognizable reason still | ||
| * yields a usable error rather than an empty list, so [first] is always safe. | ||
| */ | ||
| fun fromList(reasons: List<Map<String, Any?>>): OneSignalError = | ||
| OneSignalError( | ||
| reasons.map { Detail.fromMap(it) }.takeIf { it.isNotEmpty() } ?: listOf(Detail(ErrorCode.UNKNOWN)), | ||
| ) | ||
| } | ||
| } | ||
113 changes: 113 additions & 0 deletions
113
OneSignalSDK/onesignal/core/src/main/java/com/onesignal/OneSignalResult.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package com.onesignal | ||
|
|
||
| /** | ||
| * The outcome of an asynchronous OneSignal call: either a [data] payload or an [error], never both | ||
| * and never neither. | ||
| * | ||
| * Every SDK returns the same envelope, so a wrapper can handle results uniformly regardless of the | ||
| * platform underneath it: | ||
| * | ||
| * ```json | ||
| * { "success": true, "data": { }, "error": null } | ||
| * { "success": false, "data": null, "error": [ { "code": "STORAGE_LOCKED", ... } ] } | ||
| * ``` | ||
| * | ||
| * The presence of [error] is what defines the outcome; [isSuccess] and the wire-level `success` | ||
| * flag are both derived from it, so the two can never disagree. | ||
| * | ||
| * From Kotlin: | ||
| * ```kotlin | ||
| * val result = OneSignal.login("user-123") | ||
| * if (result.isSuccess) println(result.data?.onesignalId) else println(result.error?.first?.code) | ||
| * ``` | ||
| * | ||
| * From Java the generated accessors read naturally: | ||
| * ```java | ||
| * if (result.isSuccess()) { result.getData(); } else { result.getError(); } | ||
| * ``` | ||
| */ | ||
| class OneSignalResult<T : OneSignalResultData> internal constructor( | ||
| /** The payload on success, `null` on failure. */ | ||
| val data: T?, | ||
| /** The failure detail on failure, `null` on success. */ | ||
| val error: OneSignalError?, | ||
| ) { | ||
| /** `true` when the call completed successfully. Equivalent to `error == null`. */ | ||
| val isSuccess: Boolean | ||
| get() = error == null | ||
|
|
||
| /** Kotlin-idiomatic alias for [data]. */ | ||
| fun getOrNull(): T? = data | ||
|
|
||
| /** | ||
| * Returns the payload, or throws [OneSignalException] when the call failed. Use this only where | ||
| * a failure genuinely cannot be handled locally. | ||
| */ | ||
| fun getOrThrow(): T = data ?: throw OneSignalException(error ?: unexpectedMissingError()) | ||
|
|
||
| /** Projects the envelope onto the cross-SDK wire shape consumed by the wrapper bridges. */ | ||
| fun toMap(): Map<String, Any?> = | ||
| mapOf( | ||
| KEY_SUCCESS to isSuccess, | ||
| KEY_DATA to data?.toMap(), | ||
| KEY_ERROR to error?.toList(), | ||
| ) | ||
|
|
||
| override fun toString(): String = if (isSuccess) "OneSignalResult(success, data=$data)" else "OneSignalResult(failure, error=$error)" | ||
|
|
||
| private fun unexpectedMissingError() = OneSignalError.of(ErrorCode.UNKNOWN, "Result carried neither data nor error.") | ||
|
|
||
| internal companion object { | ||
| // Private because `const val` in an internal companion still compiles to a public static | ||
| // field, which would leak the wire keys into the customer-facing API surface. | ||
| private const val KEY_SUCCESS = "success" | ||
| private const val KEY_DATA = "data" | ||
| private const val KEY_ERROR = "error" | ||
|
|
||
| fun <T : OneSignalResultData> success(data: T): OneSignalResult<T> = OneSignalResult(data, null) | ||
|
|
||
| fun <T : OneSignalResultData> failure(error: OneSignalError): OneSignalResult<T> = OneSignalResult(null, error) | ||
|
|
||
| fun <T : OneSignalResultData> failure( | ||
| code: ErrorCode, | ||
| message: String? = null, | ||
| backendCode: Int? = null, | ||
| cause: Throwable? = null, | ||
| ): OneSignalResult<T> = failure(OneSignalError.of(code, message, backendCode, cause)) | ||
|
|
||
| /** | ||
| * Rebuilds an envelope from its wire shape, delegating payload parsing to [dataParser]. | ||
| * | ||
| * The incoming `success` flag is deliberately ignored: [error] is the single source of | ||
| * truth, which keeps a malformed producer from yielding a result that claims success while | ||
| * carrying an error. Unrecognized keys are ignored so a newer producer can add fields | ||
| * without breaking an older consumer. | ||
| */ | ||
| @Suppress("UNCHECKED_CAST") | ||
| fun <T : OneSignalResultData> fromMap( | ||
| map: Map<String, Any?>, | ||
| dataParser: (Map<String, Any?>) -> T, | ||
| ): OneSignalResult<T> { | ||
| val reasons = map[KEY_ERROR] as? List<Map<String, Any?>> | ||
| if (reasons != null) { | ||
| return failure(OneSignalError.fromList(reasons)) | ||
| } | ||
|
|
||
| val dataMap = map[KEY_DATA] as? Map<String, Any?> ?: emptyMap() | ||
| return success(dataParser(dataMap)) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Thrown by [OneSignalResult.getOrThrow] when the underlying call failed. */ | ||
| class OneSignalException internal constructor( | ||
| /** The failure detail that caused this exception. */ | ||
| val error: OneSignalError, | ||
| ) : Exception(describe(error), error.cause) | ||
|
|
||
| // A Detail carries no message when the code says everything, so appending a bare "null" to the | ||
| // exception text would only add noise to the stack trace. | ||
| private fun describe(error: OneSignalError): String = | ||
| error.error.joinToString("; ") { detail -> | ||
| if (detail.message == null) detail.code.name else "${detail.code}: ${detail.message}" | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i thought were avoiding error codes