commit b65d2bcad55bf3c7c55037f2bc1bbe2a6c6a6f61
parent cb1de9a3294b4cdf2f25bd9e8e199051344e943f
Author: triesap <tyson@radroots.org>
Date: Thu, 6 Aug 2026 06:57:07 +0000
mobile: replace debugderived wire labels
- define exhaustive protocol class labels independent of Rust variant names
- define exhaustive recovery action labels from the protocol authority
- remove generic Debug case conversion from mobile error records
- preserve the versioned secret-safe error wire contract with tests
Diffstat:
2 files changed, 69 insertions(+), 27 deletions(-)
diff --git a/crates/mobile_core/src/error.rs b/crates/mobile_core/src/error.rs
@@ -34,12 +34,12 @@ impl RadrootsAppError {
report: SdkErrorRecord {
schema_version: report.schema_version(),
code: report.code().as_str().to_owned(),
- class: debug_label(report.class()),
+ class: report.class().as_str().to_owned(),
retryable: report.retryable(),
recovery_actions: report
.recovery_actions()
.iter()
- .map(|action| debug_label(*action))
+ .map(|action| action.as_str().to_owned())
.collect(),
operation_id: report.operation_id().map(|id| id.as_str().to_owned()),
capability_id: report.capability_id().map(|id| id.as_str().to_owned()),
@@ -65,21 +65,10 @@ impl RadrootsAppError {
}
}
-fn debug_label(value: impl std::fmt::Debug) -> String {
- let mut label = String::new();
- for (index, character) in format!("{value:?}").chars().enumerate() {
- if character.is_ascii_uppercase() && index != 0 {
- label.push('_');
- }
- label.push(character.to_ascii_lowercase());
- }
- label
-}
-
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
- use super::{RadrootsAppError, SdkErrorRecord, debug_label};
+ use super::{RadrootsAppError, SdkErrorRecord};
#[test]
fn sdk_error_records_are_versioned_stable_and_secret_safe() {
@@ -106,14 +95,6 @@ mod tests {
}
#[test]
- fn debug_labels_use_stable_mobile_case() {
- assert_eq!(
- debug_label(SampleLabel::RetryAfterClose),
- "retry_after_close"
- );
- }
-
- #[test]
fn public_error_constructors_preserve_typed_variants() {
assert!(matches!(
RadrootsAppError::initialization("init"),
@@ -132,9 +113,4 @@ mod tests {
RadrootsAppError::Internal(message) if message == "internal"
));
}
-
- #[derive(Debug)]
- enum SampleLabel {
- RetryAfterClose,
- }
}
diff --git a/crates/protocol/src/error/v1.rs b/crates/protocol/src/error/v1.rs
@@ -53,6 +53,33 @@ pub enum Class {
Unknown,
}
+impl Class {
+ /// Returns the stable wire identity for this class.
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::Validation => "validation",
+ Self::Contract => "contract",
+ Self::Storage => "storage",
+ Self::Resource => "resource",
+ Self::Conflict => "conflict",
+ Self::Operation => "operation",
+ Self::Authorization => "authorization",
+ Self::Signer => "signer",
+ Self::Network => "network",
+ Self::Sync => "sync",
+ Self::Runtime => "runtime",
+ Self::Projection => "projection",
+ Self::Query => "query",
+ Self::Capability => "capability",
+ Self::Privacy => "privacy",
+ Self::Security => "security",
+ Self::Maintenance => "maintenance",
+ Self::Internal => "internal",
+ Self::Unknown => "unknown",
+ }
+ }
+}
+
/// Stable recovery action vocabulary established by the SDK surface.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
@@ -74,6 +101,30 @@ pub enum RecoveryAction {
RecreateClient,
}
+impl RecoveryAction {
+ /// Returns the stable wire identity for this recovery action.
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ Self::InspectLocalStores => "inspect_local_stores",
+ Self::ConfigureStorage => "configure_storage",
+ Self::InspectGeoNamesAsset => "inspect_geo_names_asset",
+ Self::RetryOperationWithSameIdempotencyKey => {
+ "retry_operation_with_same_idempotency_key"
+ }
+ Self::ConfigureTransportTargets => "configure_transport_targets",
+ Self::ConfigureGeoNamesCache => "configure_geo_names_cache",
+ Self::ConfigureSigner => "configure_signer",
+ Self::FixRequest => "fix_request",
+ Self::SelectAuthorizedActor => "select_authorized_actor",
+ Self::CompleteSignerAuthentication => "complete_signer_authentication",
+ Self::RetryAfterTransportFailure => "retry_after_transport_failure",
+ Self::RetryGeoNamesDownload => "retry_geonames_download",
+ Self::EnableRequiredFeature => "enable_required_feature",
+ Self::RecreateClient => "recreate_client",
+ }
+ }
+}
+
/// One generated catalog descriptor.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Descriptor {
@@ -770,6 +821,21 @@ mod tests {
use super::*;
#[test]
+ fn class_and_recovery_labels_are_explicit_and_stable() {
+ assert_eq!(Class::Validation.as_str(), "validation");
+ assert_eq!(Class::Unknown.as_str(), "unknown");
+ assert_eq!(
+ RecoveryAction::InspectLocalStores.as_str(),
+ "inspect_local_stores"
+ );
+ assert_eq!(RecoveryAction::RecreateClient.as_str(), "recreate_client");
+ assert_eq!(
+ RecoveryAction::RetryOperationWithSameIdempotencyKey.as_str(),
+ "retry_operation_with_same_idempotency_key"
+ );
+ }
+
+ #[test]
fn generated_catalog_is_complete_unique_and_self_consistent() {
assert_eq!(CATALOG.len(), 63);
assert_eq!(KnownCode::ALL.len(), CATALOG.len());