sdk

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

commit d62f884833ebf2d27b818e73e227b6c9a9a49af6
parent df4be6f48cefbb09db3f8242afaf40cef6039a2f
Author: triesap <tyson@radroots.org>
Date:   Mon,  3 Aug 2026 10:50:41 +0000

sdk: replace SDK storage implementation with storage SPIs

- compose memory and SQLite through canonical storage traits
- expose native status and passive integrity reports
- keep SQLite configuration free of backend handles
- verify memory and SQLite lifecycle behavior

Diffstat:
MCargo.lock | 2++
Mcrates/sdk/Cargo.toml | 4++++
Mcrates/sdk/src/client.rs | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/error.rs | 29+++++++++++++++++++++++++++++
Mcrates/sdk/src/storage.rs | 20+++++++++++++++++++-
5 files changed, 146 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1887,6 +1887,8 @@ dependencies = [ "radroots_transport", "radroots_transport_nostr", "reqwest", + "tempfile", + "tokio", ] [[package]] diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml @@ -77,6 +77,10 @@ reqwest = { workspace = true, optional = true, default-features = false, feature "rustls-tls", ] } +[dev-dependencies] +tempfile = { workspace = true } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + [[test]] name = "package_boundary" path = "tests/package_boundary.rs" diff --git a/crates/sdk/src/client.rs b/crates/sdk/src/client.rs @@ -70,6 +70,18 @@ impl ClientBuilder { Self::new().storage(Arc::new(MemoryStorage::new(generation))) } + /// Explicitly opens canonical SQLite storage from validated host-owned + /// configuration and returns a builder containing only the storage SPI. + #[cfg(feature = "sqlite")] + pub async fn sqlite(options: crate::storage::SqliteOptions) -> Result<Self> { + let storage = radroots_storage_sqlite::SqliteStorage::open(options) + .await + .map_err(Error::storage_open_failed)?; + Ok(Self::new() + .storage(Arc::new(storage)) + .capability_availability(CapabilityId::PERSISTENT_STORAGE, Availability::Available)) + } + /// Injects the canonical storage capability. #[must_use] pub fn storage(mut self, storage: Arc<dyn Storage>) -> Self { @@ -162,6 +174,22 @@ impl Client { }) } + /// Returns canonical backend status without exposing a backend handle. + pub async fn storage_status(&self) -> Result<crate::storage::Status> { + let storage = self.storage()?; + radroots_storage::BackupSource::status(storage) + .await + .map_err(Error::storage_inspection_failed) + } + + /// Runs canonical integrity inspection without exposing backend internals. + pub async fn storage_integrity(&self) -> Result<crate::storage::IntegrityStatus> { + let storage = self.storage()?; + radroots_storage::BackupSource::integrity(storage) + .await + .map_err(Error::storage_inspection_failed) + } + /// Returns the injected canonical storage capability. pub fn storage(&self) -> Result<&dyn Storage> { self.require_open()?; @@ -514,6 +542,70 @@ mod tests { ); } + #[test] + fn memory_storage_status_integrity_and_lifecycle_use_native_contracts() { + use radroots_storage::status::{ + IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy, + }; + + let client = ClientBuilder::memory(generation()).build().expect("client"); + let status = block_on(client.storage_status()).expect("status"); + assert_eq!(status.backend(), StorageBackend::Memory); + assert_eq!(status.open_mode(), StorageOpenMode::Create); + assert_eq!(status.writer_policy(), WriterPolicy::NoWriter); + assert_eq!(status.shutdown(), ShutdownState::Open); + assert_eq!( + block_on(client.storage_integrity()) + .expect("integrity") + .health(), + IntegrityHealth::Healthy + ); + block_on(client.close()).expect("close"); + assert_eq!( + block_on(client.storage_status()) + .expect_err("closed") + .kind(), + crate::error::ErrorKind::ClientClosed + ); + } + + #[cfg(feature = "sqlite")] + #[tokio::test] + async fn sqlite_builder_exposes_native_status_integrity_and_lifecycle() { + use radroots_storage::status::{ + IntegrityHealth, ShutdownState, StorageBackend, StorageOpenMode, WriterPolicy, + }; + + let directory = tempfile::tempdir().expect("temporary directory"); + let paths = crate::storage::SqlitePaths::from_directory(directory.path()).expect("paths"); + let options = + crate::storage::SqliteOptions::new(paths, crate::storage::SqliteOpenMode::Create) + .with_source_generation(generation(), 1) + .expect("source generation"); + let client = ClientBuilder::sqlite(options) + .await + .expect("open builder") + .build() + .expect("client"); + let status = client.storage_status().await.expect("status"); + assert_eq!(status.backend(), StorageBackend::Sqlite); + assert_eq!(status.open_mode(), StorageOpenMode::Create); + assert_eq!(status.writer_policy(), WriterPolicy::AdvisoryProcessLock); + assert_eq!(status.shutdown(), ShutdownState::Open); + assert!(status.wal_enabled()); + assert_ne!(status.busy_timeout_ms(), 0); + assert_eq!( + client + .storage_integrity() + .await + .expect("integrity") + .health(), + IntegrityHealth::Unknown + ); + client.close().await.expect("close"); + assert!(client.is_closed()); + } + struct ThreadWaker; impl Wake for ThreadWaker { diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -106,6 +106,20 @@ error_catalog! { message: "SDK storage close failed", safe_detail_keys: [] }, + StorageOpenFailed => { + code: InternalError, + operation: None, + capability: Some(CapabilityId::PERSISTENT_STORAGE), + message: "SDK persistent storage open failed", + safe_detail_keys: [] + }, + StorageInspectionFailed => { + code: StorageIntegrityFailed, + operation: None, + capability: Some(CapabilityId::CANONICAL_STORAGE), + message: "SDK storage inspection failed", + safe_detail_keys: [] + }, } /// Stable metadata for one native SDK failure. @@ -209,6 +223,21 @@ impl Error { } } + #[cfg(feature = "sqlite")] + pub(crate) fn storage_open_failed(source: radroots_storage_sqlite::Error) -> Self { + Self { + kind: ErrorKind::StorageOpenFailed, + source: Some(Box::new(source)), + } + } + + pub(crate) fn storage_inspection_failed(source: radroots_storage::Error) -> Self { + Self { + kind: ErrorKind::StorageInspectionFailed, + source: Some(Box::new(source)), + } + } + fn without_source(kind: ErrorKind) -> Self { Self { kind, source: None } } diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage.rs @@ -1 +1,19 @@ -//! Storage capability composition. +//! Canonical storage capability composition. + +/// Backend-neutral storage status owned by `radroots_storage`. +pub type Status = radroots_storage::StorageStatus; + +/// Backend-neutral integrity status owned by `radroots_storage`. +pub type IntegrityStatus = radroots_storage::status::IntegrityStatus; + +/// Validated SQLite open configuration; this contains no connection or pool. +#[cfg(feature = "sqlite")] +pub type SqliteOptions = radroots_storage_sqlite::OpenOptions; + +/// Explicit SQLite lifecycle mode. +#[cfg(feature = "sqlite")] +pub type SqliteOpenMode = radroots_storage_sqlite::OpenMode; + +/// Validated SQLite-owned paths; this contains no backend handle. +#[cfg(feature = "sqlite")] +pub type SqlitePaths = radroots_storage_sqlite::Paths;