sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 8da75250caf068977b0bdb5986c35ef30cacf70b
parent 52dedb8a3211ba7374731f8fef14951017a4c384
Author: triesap <tyson@radroots.org>
Date:   Wed, 17 Jun 2026 17:45:51 -0700

sdk: harden runtime DTO posture

- make runtime request DTOs constructor-led and non-exhaustive
- add constructors for storage, integrity, and backup requests
- update tests to exercise request construction APIs
- document DTO stability classes for runtime consumers

Diffstat:
Mcrates/sdk/README | 17+++++++++++++++++
Mcrates/sdk/src/listings_runtime.rs | 2++
Mcrates/sdk/src/orders_runtime.rs | 1+
Mcrates/sdk/src/runtime.rs | 34+++++++++++++++++++++++++++++++++-
Mcrates/sdk/src/sync_runtime.rs | 2++
Mcrates/sdk/tests/runtime_foundation.rs | 14+++++---------
Mcrates/sdk/tests/sync_runtime.rs | 28++++++++--------------------
7 files changed, 68 insertions(+), 30 deletions(-)

diff --git a/crates/sdk/README b/crates/sdk/README @@ -100,3 +100,20 @@ dry run. Runtime errors use a non-exhaustive `RadrootsSdkError` enum. Public callers should branch on the stable method surface: `code`, `class`, `retryable`, `detail_json`, and `recovery_actions`. + +## Runtime DTO Stability + +Runtime request DTOs are constructor-led and marked non-exhaustive where they +carry public fields: `ListingPreparePublishRequest`, +`ListingEnqueuePublishRequest`, `OrderStatusRequest`, `StorageStatusRequest`, +`BackupRequest`, `IntegrityRequest`, `SyncStatusRequest`, and +`PushOutboxRequest`. Use `new`, `parse`, `default`, and `with_*` methods rather +than struct literals. + +Runtime enums that may gain variants are non-exhaustive. This includes storage, +clock, relay-target, mutation-state, order-status, sync-status, relay-auth, and +push-outbox state or outcome enums. + +Runtime receipts and status records expose stable serialized public fields for +CLI and local-runtime reporting. Future additive reporting should preserve +serde compatibility for existing fields. diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -34,6 +34,7 @@ const LISTING_PUBLISH_OPERATION_KIND: &str = "listing.publish.v1"; #[cfg(feature = "runtime")] #[derive(Clone, Debug)] +#[non_exhaustive] pub struct ListingPreparePublishRequest { pub actor: RadrootsActorContext, pub document: RadrootsListingDraftDocumentV1, @@ -83,6 +84,7 @@ impl ListingPreparePublishRequest { #[cfg(feature = "runtime")] #[derive(Clone, Debug)] +#[non_exhaustive] pub struct ListingEnqueuePublishRequest { pub actor: RadrootsActorContext, pub document: RadrootsListingDraftDocumentV1, diff --git a/crates/sdk/src/orders_runtime.rs b/crates/sdk/src/orders_runtime.rs @@ -21,6 +21,7 @@ pub const ORDER_STATUS_MAX_LIMIT: u32 = 1_000; #[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +#[non_exhaustive] pub struct OrderStatusRequest { pub order_id: RadrootsOrderId, pub limit: u32, diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs @@ -104,9 +104,17 @@ pub struct RadrootsSdkStoragePaths { #[cfg(feature = "runtime")] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)] +#[non_exhaustive] pub struct StorageStatusRequest {} #[cfg(feature = "runtime")] +impl StorageStatusRequest { + pub fn new() -> Self { + Self::default() + } +} + +#[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct StorageStatusReceipt { pub storage: SdkStorageKind, @@ -163,12 +171,28 @@ pub struct SdkOutboxStorageStatus { #[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +#[non_exhaustive] pub struct BackupRequest { pub destination: PathBuf, pub overwrite: bool, } #[cfg(feature = "runtime")] +impl BackupRequest { + pub fn new(destination: impl Into<PathBuf>) -> Self { + Self { + destination: destination.into(), + overwrite: false, + } + } + + pub fn with_overwrite(mut self, overwrite: bool) -> Self { + self.overwrite = overwrite; + self + } +} + +#[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct BackupReceipt { pub destination: PathBuf, @@ -212,9 +236,17 @@ pub struct SdkBackupVerification { #[cfg(feature = "runtime")] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)] +#[non_exhaustive] pub struct IntegrityRequest {} #[cfg(feature = "runtime")] +impl IntegrityRequest { + pub fn new() -> Self { + Self::default() + } +} + +#[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct IntegrityReceipt { pub checked_paths: Vec<PathBuf>, @@ -410,7 +442,7 @@ impl RadrootsSdk { outbox_path: request.destination.join(OUTBOX_BACKUP_FILE), }; let manifest_path = request.destination.join(BACKUP_MANIFEST_FILE); - let source_status = self.storage_status(StorageStatusRequest::default()).await?; + let source_status = self.storage_status(StorageStatusRequest::new()).await?; sqlite_vacuum_into( self._event_store.pool(), &backup_paths.event_store_path, diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -30,6 +30,7 @@ const CLAIM_OWNER: &str = "radroots_sdk.sync.push_outbox"; #[cfg(feature = "runtime")] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)] +#[non_exhaustive] pub struct SyncStatusRequest {} #[cfg(feature = "runtime")] @@ -135,6 +136,7 @@ impl Default for SdkRelayAuthPolicy { #[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] +#[non_exhaustive] pub struct PushOutboxRequest { pub limit: usize, pub republish_accepted_relays: bool, diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -156,7 +156,7 @@ async fn sdk_memory_storage_status_and_integrity_report_canonical_stores() { .expect("sdk"); let status = sdk - .storage_status(StorageStatusRequest::default()) + .storage_status(StorageStatusRequest::new()) .await .expect("status"); assert_eq!(status.storage, SdkStorageKind::Memory); @@ -172,7 +172,7 @@ async fn sdk_memory_storage_status_and_integrity_report_canonical_stores() { assert!(status.outbox.store.integrity_ok); let integrity = sdk - .integrity(IntegrityRequest::default()) + .integrity(IntegrityRequest::new()) .await .expect("integrity"); assert!(integrity.checked_paths.is_empty()); @@ -555,7 +555,7 @@ fn storage_backup_and_integrity_contract_dtos_serialize() { integrity_result: "ok".to_owned(), }; assert_eq!( - serde_json::to_value(StorageStatusRequest::default()).expect("status request"), + serde_json::to_value(StorageStatusRequest::new()).expect("status request"), serde_json::json!({}) ); assert_eq!( @@ -624,11 +624,7 @@ fn storage_backup_and_integrity_contract_dtos_serialize() { }) ); assert_eq!( - serde_json::to_value(BackupRequest { - destination: PathBuf::from("backup"), - overwrite: false, - }) - .expect("backup request"), + serde_json::to_value(BackupRequest::new("backup")).expect("backup request"), serde_json::json!({ "destination": "backup", "overwrite": false @@ -654,7 +650,7 @@ fn storage_backup_and_integrity_contract_dtos_serialize() { }) ); assert_eq!( - serde_json::to_value(IntegrityRequest::default()).expect("integrity request"), + serde_json::to_value(IntegrityRequest::new()).expect("integrity request"), serde_json::json!({}) ); } diff --git a/crates/sdk/tests/sync_runtime.rs b/crates/sdk/tests/sync_runtime.rs @@ -390,7 +390,7 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() { let outbox_event_id = enqueue_listing(&sdk, LISTING_A_D_TAG, "Backup Coffee", &[RELAY_A]).await; let status = sdk - .storage_status(StorageStatusRequest::default()) + .storage_status(StorageStatusRequest::new()) .await .expect("storage status"); let source_paths = sdk.storage_paths().expect("source paths"); @@ -404,7 +404,7 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() { assert_eq!(status.outbox.store.journal_mode, "wal"); let integrity = sdk - .integrity(IntegrityRequest::default()) + .integrity(IntegrityRequest::new()) .await .expect("integrity"); assert_eq!( @@ -419,10 +419,7 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() { let backup_destination = tempdir.path().join("backup"); let backup = sdk - .backup(BackupRequest { - destination: backup_destination.clone(), - overwrite: false, - }) + .backup(BackupRequest::new(backup_destination.clone())) .await .expect("backup"); let event_store_path = backup @@ -473,20 +470,14 @@ async fn sdk_directory_backup_creates_verified_canonical_store_copy() { ); let duplicate = sdk - .backup(BackupRequest { - destination: backup_destination.clone(), - overwrite: false, - }) + .backup(BackupRequest::new(backup_destination.clone())) .await .expect_err("duplicate backup"); assert!(matches!(duplicate, RadrootsSdkError::InvalidRequest { .. })); - sdk.backup(BackupRequest { - destination: backup_destination, - overwrite: true, - }) - .await - .expect("overwrite backup"); + sdk.backup(BackupRequest::new(backup_destination).with_overwrite(true)) + .await + .expect("overwrite backup"); } #[cfg(unix)] @@ -499,10 +490,7 @@ async fn sdk_backup_rejects_symlink_destination_even_with_overwrite() { std::os::unix::fs::symlink(&target, &destination).expect("symlink"); let error = sdk - .backup(BackupRequest { - destination, - overwrite: true, - }) + .backup(BackupRequest::new(destination).with_overwrite(true)) .await .expect_err("symlink destination"); assert!(matches!(error, RadrootsSdkError::InvalidRequest { .. }));