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:
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 { .. }));