lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit fa6802ef6800a3e6cddda266b71fa48598ca3e16
parent ded5fc00f3b24b232682e290c026fb0837360cd4
Author: triesap <tyson@radroots.org>
Date:   Fri, 26 Jun 2026 08:37:20 +0000

sdk: promote GeoNames client surface

- Add GeoNames builder configuration and client handle on RadrootsClient.
- Replace public GeoNames helper exports with the target product-client API.
- Map geocoder failures into structured SDK GeoNames error taxonomy.
- Cover the new client and error contract in SDK runtime tests.

Diffstat:
Mcrates/sdk/src/error.rs | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/sdk/src/geonames.rs | 162++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/sdk/src/lib.rs | 18+++++-------------
Mcrates/sdk/src/product_clients.rs | 13+++++++++++++
Mcrates/sdk/src/runtime.rs | 26++++++++++++++++++++++++--
Mcrates/sdk/tests/geonames.rs | 57+++++++++++++++++++++++++++++++++++++++++++++------------
Mcrates/sdk/tests/runtime_foundation.rs | 23+++++++++++++++++------
Mcrates/sdk/tests/unit/runtime_tests.rs | 2++
8 files changed, 315 insertions(+), 88 deletions(-)

diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs @@ -26,13 +26,16 @@ pub enum RadrootsSdkErrorClass { pub enum RadrootsSdkRecoveryAction { RetryOutboxEnqueue, InspectLocalStores, + InspectGeoNamesAsset, RetryOperationWithSameIdempotencyKey, ConfigureRelayTargets, + ConfigureGeoNamesCache, ConfigureSigner, FixRequest, SelectAuthorizedActor, CompleteSignerAuthentication, RetryAfterTransportFailure, + RetryGeoNamesDownload, EnableRequiredFeature, } @@ -40,6 +43,19 @@ pub enum RadrootsSdkRecoveryAction { #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] #[serde(rename_all = "snake_case")] #[non_exhaustive] +pub enum RadrootsSdkGeoNamesErrorKind { + Configuration, + Download, + Cache, + Integrity, + Schema, + Lookup, +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] pub enum RadrootsSdkPartialLocalMutationFailure { OutboxEnqueue, OutboxIdempotencyConflict, @@ -156,6 +172,10 @@ pub enum RadrootsSdkError { Outbox { message: String, }, + GeoNames { + kind: RadrootsSdkGeoNamesErrorKind, + message: String, + }, RelayTransport { message: String, }, @@ -195,6 +215,14 @@ impl RadrootsSdkError { Self::ListingDraft { .. } => "listing_draft", Self::ListingMutation { .. } => "listing_mutation", Self::Outbox { .. } => "outbox", + Self::GeoNames { kind, .. } => match kind { + RadrootsSdkGeoNamesErrorKind::Configuration => "geonames_configuration", + RadrootsSdkGeoNamesErrorKind::Download => "geonames_download", + RadrootsSdkGeoNamesErrorKind::Cache => "geonames_cache", + RadrootsSdkGeoNamesErrorKind::Integrity => "geonames_integrity", + RadrootsSdkGeoNamesErrorKind::Schema => "geonames_schema", + RadrootsSdkGeoNamesErrorKind::Lookup => "geonames_lookup", + }, Self::RelayTransport { .. } => "relay_transport", Self::Projection { .. } => "projection", Self::PartialLocalMutation(_) => "partial_local_mutation", @@ -207,6 +235,14 @@ impl RadrootsSdkError { | Self::EventStore { .. } | Self::Outbox { .. } | Self::Projection { .. } => RadrootsSdkErrorClass::Storage, + Self::GeoNames { kind, .. } => match kind { + RadrootsSdkGeoNamesErrorKind::Configuration => RadrootsSdkErrorClass::Configuration, + RadrootsSdkGeoNamesErrorKind::Download => RadrootsSdkErrorClass::Transport, + RadrootsSdkGeoNamesErrorKind::Cache + | RadrootsSdkGeoNamesErrorKind::Integrity + | RadrootsSdkGeoNamesErrorKind::Schema => RadrootsSdkErrorClass::Storage, + RadrootsSdkGeoNamesErrorKind::Lookup => RadrootsSdkErrorClass::Request, + }, Self::ClockBeforeUnixEpoch | Self::TimestampOutOfRange { .. } => { RadrootsSdkErrorClass::Clock } @@ -243,6 +279,11 @@ impl RadrootsSdkError { | Self::ProductSyncRelaySetupFailure { .. } | Self::EventStore { .. } | Self::Outbox { .. } + | Self::GeoNames { + kind: RadrootsSdkGeoNamesErrorKind::Cache + | RadrootsSdkGeoNamesErrorKind::Download, + .. + } | Self::RelayTransport { .. } | Self::SignerRequestTimedOut { .. } | Self::SignerTransport { .. } @@ -257,6 +298,20 @@ impl RadrootsSdkError { | Self::EventStore { .. } | Self::Outbox { .. } | Self::Projection { .. } => vec![RadrootsSdkRecoveryAction::InspectLocalStores], + Self::GeoNames { kind, .. } => match kind { + RadrootsSdkGeoNamesErrorKind::Configuration => { + vec![RadrootsSdkRecoveryAction::ConfigureGeoNamesCache] + } + RadrootsSdkGeoNamesErrorKind::Download => { + vec![RadrootsSdkRecoveryAction::RetryGeoNamesDownload] + } + RadrootsSdkGeoNamesErrorKind::Cache + | RadrootsSdkGeoNamesErrorKind::Integrity + | RadrootsSdkGeoNamesErrorKind::Schema => { + vec![RadrootsSdkRecoveryAction::InspectGeoNamesAsset] + } + RadrootsSdkGeoNamesErrorKind::Lookup => vec![RadrootsSdkRecoveryAction::FixRequest], + }, Self::UnauthorizedActor { .. } | Self::SignerPubkeyMismatch { .. } | Self::SignerRequestRejected { .. } @@ -362,6 +417,7 @@ impl RadrootsSdkError { | Self::Outbox { message } | Self::RelayTransport { message } | Self::Projection { message } => json!({ "message": message }), + Self::GeoNames { kind, message } => json!({ "kind": kind, "message": message }), Self::PartialLocalMutation(error) => json!(error), }; json!({ @@ -437,6 +493,13 @@ impl RadrootsSdkError { message: message.into(), } } + + pub(crate) fn missing_geonames_config() -> Self { + Self::GeoNames { + kind: RadrootsSdkGeoNamesErrorKind::Configuration, + message: "GeoNames cache root is not configured".to_owned(), + } + } } #[cfg(feature = "runtime")] @@ -537,6 +600,9 @@ impl fmt::Display for RadrootsSdkError { write!(f, "sdk listing mutation error: {message}") } Self::Outbox { message } => write!(f, "sdk outbox error: {message}"), + Self::GeoNames { kind, message } => { + write!(f, "sdk GeoNames {kind:?} error: {message}") + } Self::RelayTransport { message } => { write!(f, "sdk relay transport error: {message}") } @@ -609,6 +675,42 @@ impl From<radroots_event_store::RadrootsEventStoreError> for RadrootsSdkError { } #[cfg(feature = "runtime")] +impl From<radroots_geocoder::GeocoderError> for RadrootsSdkError { + fn from(error: radroots_geocoder::GeocoderError) -> Self { + let kind = match &error { + radroots_geocoder::GeocoderError::InvalidAssetUrl { .. } + | radroots_geocoder::GeocoderError::InvalidAssetHost { .. } => { + RadrootsSdkGeoNamesErrorKind::Configuration + } + radroots_geocoder::GeocoderError::AssetDownload { .. } => { + RadrootsSdkGeoNamesErrorKind::Download + } + radroots_geocoder::GeocoderError::Io(_) + | radroots_geocoder::GeocoderError::Sqlite(_) + | radroots_geocoder::GeocoderError::AssetLockUnavailable { .. } => { + RadrootsSdkGeoNamesErrorKind::Cache + } + radroots_geocoder::GeocoderError::InvalidAssetSchema { .. } => { + RadrootsSdkGeoNamesErrorKind::Schema + } + radroots_geocoder::GeocoderError::InvalidAssetLength { .. } + | radroots_geocoder::GeocoderError::InvalidAssetSha256 { .. } + | radroots_geocoder::GeocoderError::InvalidAssetSqlite { .. } + | radroots_geocoder::GeocoderError::InvalidAssetIntegrity { .. } => { + RadrootsSdkGeoNamesErrorKind::Integrity + } + radroots_geocoder::GeocoderError::CountryCenterNotFound { .. } => { + RadrootsSdkGeoNamesErrorKind::Lookup + } + }; + Self::GeoNames { + kind, + message: error.to_string(), + } + } +} + +#[cfg(feature = "runtime")] impl From<radroots_trade::listing::RadrootsListingDraftError> for RadrootsSdkError { fn from(error: radroots_trade::listing::RadrootsListingDraftError) -> Self { match error { diff --git a/crates/sdk/src/geonames.rs b/crates/sdk/src/geonames.rs @@ -2,6 +2,7 @@ use std::path::{Path, PathBuf}; +use crate::{GeoNamesClient, RadrootsSdkError}; pub use radroots_geocoder::{ GEONAMES_1_0_ASSET, GEONAMES_ASSET_BYTE_SIZE, GEONAMES_ASSET_FILE_NAME, GEONAMES_ASSET_HOST, GEONAMES_ASSET_SHA256, GEONAMES_ASSET_URL, GEONAMES_ASSET_VERSION, GeoNamesAssetFetcher, @@ -18,72 +19,123 @@ use radroots_runtime_paths::{ default_shared_geonames_root_from_cache_root, }; -pub fn radroots_sdk_geonames_database_path_from_cache_root( - cache_root: impl AsRef<Path>, -) -> PathBuf { - default_shared_geonames_database_path_from_cache_root(cache_root, GEONAMES_ASSET_VERSION) +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct RadrootsGeoNamesConfig { + pub cache_root: PathBuf, } -pub fn radroots_sdk_geonames_database_path_from_cache_root_for_version( - cache_root: impl AsRef<Path>, - version: &str, -) -> PathBuf { - default_shared_geonames_database_path_from_cache_root(cache_root, version) -} +impl RadrootsGeoNamesConfig { + pub fn new(cache_root: impl Into<PathBuf>) -> Self { + Self { + cache_root: cache_root.into(), + } + } -pub fn radroots_sdk_geonames_root_from_cache_root(cache_root: impl AsRef<Path>) -> PathBuf { - default_shared_geonames_root_from_cache_root(cache_root) -} + pub fn root_path(&self) -> PathBuf { + default_shared_geonames_root_from_cache_root(&self.cache_root) + } -pub fn radroots_sdk_inspect_geonames_database_in_cache_root( - cache_root: impl AsRef<Path>, -) -> Result<GeoNamesAssetStatus, GeocoderError> { - inspect_default_geonames_asset_in_cache_root(cache_root) -} + pub fn database_path(&self) -> PathBuf { + geonames_database_path_from_cache_root(&self.cache_root) + } -pub fn radroots_sdk_inspect_geonames_database_path_with_spec( - path: impl AsRef<Path>, - spec: &GeoNamesAssetSpec, -) -> Result<GeoNamesAssetStatus, GeocoderError> { - inspect_geonames_asset_path(path, spec) + pub fn database_path_for_version(&self, version: &str) -> PathBuf { + geonames_database_path_from_cache_root_for_version(&self.cache_root, version) + } } -pub fn radroots_sdk_ensure_geonames_database_in_cache_root( - cache_root: impl AsRef<Path>, -) -> Result<GeoNamesAssetStatus, GeocoderError> { - ensure_default_geonames_asset_in_cache_root(cache_root) -} +impl<'sdk> GeoNamesClient<'sdk> { + pub fn config(&self) -> Option<&RadrootsGeoNamesConfig> { + self.sdk.geonames_config() + } -pub fn radroots_sdk_ensure_geonames_database_in_cache_root_with_fetcher<F>( - cache_root: impl AsRef<Path>, - fetcher: &F, -) -> Result<GeoNamesAssetStatus, GeocoderError> -where - F: GeoNamesAssetFetcher, -{ - ensure_geonames_asset_in_cache_root_with_fetcher(cache_root, &GEONAMES_1_0_ASSET, fetcher) -} + pub fn root_path(&self) -> Result<PathBuf, RadrootsSdkError> { + Ok(self.required_config()?.root_path()) + } -pub fn radroots_sdk_ensure_geonames_database_in_cache_root_with_spec_and_fetcher<F>( - cache_root: impl AsRef<Path>, - spec: &GeoNamesAssetSpec, - fetcher: &F, -) -> Result<GeoNamesAssetStatus, GeocoderError> -where - F: GeoNamesAssetFetcher, -{ - ensure_geonames_asset_in_cache_root_with_fetcher(cache_root, spec, fetcher) + pub fn database_path(&self) -> Result<PathBuf, RadrootsSdkError> { + Ok(self.required_config()?.database_path()) + } + + pub fn database_path_for_version(&self, version: &str) -> Result<PathBuf, RadrootsSdkError> { + Ok(self.required_config()?.database_path_for_version(version)) + } + + pub fn inspect(&self) -> Result<GeoNamesAssetStatus, RadrootsSdkError> { + inspect_default_geonames_asset_in_cache_root(&self.required_config()?.cache_root) + .map_err(RadrootsSdkError::from) + } + + pub fn inspect_path_with_spec( + &self, + path: impl AsRef<Path>, + spec: &GeoNamesAssetSpec, + ) -> Result<GeoNamesAssetStatus, RadrootsSdkError> { + inspect_geonames_asset_path(path, spec).map_err(RadrootsSdkError::from) + } + + pub fn ensure(&self) -> Result<GeoNamesAssetStatus, RadrootsSdkError> { + ensure_default_geonames_asset_in_cache_root(&self.required_config()?.cache_root) + .map_err(RadrootsSdkError::from) + } + + pub fn ensure_with_fetcher<F>( + &self, + fetcher: &F, + ) -> Result<GeoNamesAssetStatus, RadrootsSdkError> + where + F: GeoNamesAssetFetcher, + { + ensure_geonames_asset_in_cache_root_with_fetcher( + &self.required_config()?.cache_root, + &GEONAMES_1_0_ASSET, + fetcher, + ) + .map_err(RadrootsSdkError::from) + } + + pub fn ensure_with_spec_and_fetcher<F>( + &self, + spec: &GeoNamesAssetSpec, + fetcher: &F, + ) -> Result<GeoNamesAssetStatus, RadrootsSdkError> + where + F: GeoNamesAssetFetcher, + { + ensure_geonames_asset_in_cache_root_with_fetcher( + &self.required_config()?.cache_root, + spec, + fetcher, + ) + .map_err(RadrootsSdkError::from) + } + + pub fn open_verified(&self) -> Result<Geocoder, RadrootsSdkError> { + Geocoder::open_verified_geonames_asset(self.database_path()?, &GEONAMES_1_0_ASSET) + .map_err(RadrootsSdkError::from) + } + + pub fn open_verified_path_with_spec( + &self, + path: impl AsRef<Path>, + spec: &GeoNamesAssetSpec, + ) -> Result<Geocoder, RadrootsSdkError> { + Geocoder::open_verified_geonames_asset(path, spec).map_err(RadrootsSdkError::from) + } + + fn required_config(&self) -> Result<&RadrootsGeoNamesConfig, RadrootsSdkError> { + self.config() + .ok_or_else(RadrootsSdkError::missing_geonames_config) + } } -pub fn radroots_sdk_open_verified_geonames_database( - path: impl AsRef<Path>, -) -> Result<Geocoder, GeocoderError> { - Geocoder::open_verified_geonames_asset(path, &GEONAMES_1_0_ASSET) +fn geonames_database_path_from_cache_root(cache_root: impl AsRef<Path>) -> PathBuf { + default_shared_geonames_database_path_from_cache_root(cache_root, GEONAMES_ASSET_VERSION) } -pub fn radroots_sdk_open_verified_geonames_database_with_spec( - path: impl AsRef<Path>, - spec: &GeoNamesAssetSpec, -) -> Result<Geocoder, GeocoderError> { - Geocoder::open_verified_geonames_asset(path, spec) +fn geonames_database_path_from_cache_root_for_version( + cache_root: impl AsRef<Path>, + version: &str, +) -> PathBuf { + default_shared_geonames_database_path_from_cache_root(cache_root, version) } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs @@ -47,8 +47,9 @@ mod workflow_runtime; #[cfg(feature = "runtime")] pub use crate::error::{ - RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkPartialLocalMutationError, - RadrootsSdkPartialLocalMutationFailure, RadrootsSdkRecoveryAction, + RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, + RadrootsSdkPartialLocalMutationError, RadrootsSdkPartialLocalMutationFailure, + RadrootsSdkRecoveryAction, }; #[cfg(feature = "runtime")] pub use crate::farms_runtime::{ @@ -61,16 +62,7 @@ pub use crate::geonames::{ GEONAMES_ASSET_SHA256, GEONAMES_ASSET_URL, GEONAMES_ASSET_VERSION, GeoNamesAssetFetcher, GeoNamesAssetSpec, GeoNamesAssetState, GeoNamesAssetStatus, GeoNamesBlockingHttpFetcher, Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions, - GeocoderReverseResult, radroots_sdk_ensure_geonames_database_in_cache_root, - radroots_sdk_ensure_geonames_database_in_cache_root_with_fetcher, - radroots_sdk_ensure_geonames_database_in_cache_root_with_spec_and_fetcher, - radroots_sdk_geonames_database_path_from_cache_root, - radroots_sdk_geonames_database_path_from_cache_root_for_version, - radroots_sdk_geonames_root_from_cache_root, - radroots_sdk_inspect_geonames_database_in_cache_root, - radroots_sdk_inspect_geonames_database_path_with_spec, - radroots_sdk_open_verified_geonames_database, - radroots_sdk_open_verified_geonames_database_with_spec, + GeocoderReverseResult, RadrootsGeoNamesConfig, }; #[cfg(feature = "runtime")] pub use crate::idempotency::{SDK_IDEMPOTENCY_KEY_MAX_LEN, SdkIdempotencyKey}; @@ -100,7 +92,7 @@ pub use crate::orders_runtime::{ }; #[cfg(feature = "runtime")] pub use crate::product_clients::{ - DvmClient, FarmsClient, ListingsClient, MarketClient, SyncClient, TradesClient, + DvmClient, FarmsClient, GeoNamesClient, ListingsClient, MarketClient, SyncClient, TradesClient, }; #[cfg(feature = "runtime")] pub use crate::relay_targets::{ diff --git a/crates/sdk/src/product_clients.rs b/crates/sdk/src/product_clients.rs @@ -42,6 +42,19 @@ impl<'client> MarketClient<'client> { #[cfg(feature = "runtime")] #[derive(Clone, Copy)] +pub struct GeoNamesClient<'client> { + pub(crate) sdk: &'client RadrootsClient, +} + +#[cfg(feature = "runtime")] +impl<'client> GeoNamesClient<'client> { + pub(crate) fn new(sdk: &'client RadrootsClient) -> Self { + Self { sdk } + } +} + +#[cfg(feature = "runtime")] +#[derive(Clone, Copy)] pub struct TradesClient<'client> { pub(crate) sdk: &'client RadrootsClient, } diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs @@ -1,7 +1,7 @@ #[cfg(feature = "runtime")] use crate::{ - DvmClient, FarmsClient, ListingsClient, MarketClient, RadrootsSdkError, SdkRelayTargetSet, - SdkRelayUrlPolicy, SyncClient, TradesClient, + DvmClient, FarmsClient, GeoNamesClient, ListingsClient, MarketClient, RadrootsGeoNamesConfig, + RadrootsSdkError, SdkRelayTargetSet, SdkRelayUrlPolicy, SyncClient, TradesClient, }; #[cfg(all(feature = "runtime", feature = "signer-adapters"))] use crate::{ @@ -392,6 +392,7 @@ pub struct RestoreReceipt { #[derive(Clone)] pub struct RadrootsClientBuilder { storage: RadrootsSdkStorageConfig, + geonames: Option<RadrootsGeoNamesConfig>, clock: RadrootsSdkClock, relay_urls: Vec<String>, relay_url_policy: SdkRelayUrlPolicy, @@ -405,6 +406,7 @@ impl Default for RadrootsClientBuilder { fn default() -> Self { Self { storage: RadrootsSdkStorageConfig::Memory, + geonames: None, clock: RadrootsSdkClock::System, relay_urls: Vec::new(), relay_url_policy: SdkRelayUrlPolicy::Public, @@ -427,6 +429,16 @@ impl RadrootsClientBuilder { self } + pub fn geonames_config(mut self, geonames: RadrootsGeoNamesConfig) -> Self { + self.geonames = Some(geonames); + self + } + + pub fn geonames_cache_root(mut self, cache_root: impl Into<PathBuf>) -> Self { + self.geonames = Some(RadrootsGeoNamesConfig::new(cache_root)); + self + } + pub fn clock(mut self, clock: RadrootsSdkClock) -> Self { self.clock = clock; self @@ -466,6 +478,7 @@ impl RadrootsClientBuilder { _event_store: storage.event_store, _outbox: storage.outbox, storage_paths: storage.paths, + geonames: self.geonames, clock: self.clock, relay_urls, publish_transport: self.publish_transport, @@ -481,6 +494,7 @@ pub struct RadrootsClient { pub(crate) _event_store: RadrootsEventStore, pub(crate) _outbox: RadrootsOutbox, storage_paths: Option<RadrootsSdkStoragePaths>, + geonames: Option<RadrootsGeoNamesConfig>, clock: RadrootsSdkClock, relay_urls: Vec<String>, publish_transport: SdkPublishTransport, @@ -506,6 +520,10 @@ impl RadrootsClient { MarketClient::new(self) } + pub fn geonames(&self) -> GeoNamesClient<'_> { + GeoNamesClient::new(self) + } + pub fn trades(&self) -> TradesClient<'_> { TradesClient::new(self) } @@ -561,6 +579,10 @@ impl RadrootsClient { self.storage_paths.as_ref() } + pub fn geonames_config(&self) -> Option<&RadrootsGeoNamesConfig> { + self.geonames.as_ref() + } + pub async fn storage_status( &self, _request: StorageStatusRequest, diff --git a/crates/sdk/tests/geonames.rs b/crates/sdk/tests/geonames.rs @@ -1,41 +1,50 @@ #![cfg(feature = "runtime")] use radroots_sdk::{ - GEONAMES_ASSET_HOST, GEONAMES_ASSET_VERSION, GeoNamesAssetState, - radroots_sdk_geonames_database_path_from_cache_root, - radroots_sdk_geonames_database_path_from_cache_root_for_version, - radroots_sdk_geonames_root_from_cache_root, - radroots_sdk_inspect_geonames_database_in_cache_root, + GEONAMES_ASSET_HOST, GEONAMES_ASSET_VERSION, GeoNamesAssetState, RadrootsClient, + RadrootsGeoNamesConfig, RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, + RadrootsSdkRecoveryAction, }; -#[test] -fn sdk_geonames_facade_resolves_shared_cache_paths_and_reports_missing_state() { +#[tokio::test] +async fn sdk_geonames_client_resolves_shared_cache_paths_and_reports_missing_state() { let tempdir = tempfile::tempdir().expect("tempdir"); let cache_root = tempdir.path().join("cache"); + let sdk = RadrootsClient::builder() + .geonames_cache_root(cache_root.clone()) + .build() + .await + .expect("sdk"); + let geonames = sdk.geonames(); assert_eq!(GEONAMES_ASSET_HOST, "assets.radroots.io"); assert_eq!(GEONAMES_ASSET_VERSION, "1.0"); assert_eq!( - radroots_sdk_geonames_root_from_cache_root(&cache_root), + sdk.geonames_config(), + Some(&RadrootsGeoNamesConfig::new(cache_root.clone())) + ); + assert_eq!( + geonames.root_path().expect("geonames root"), cache_root.join("shared").join("geonames") ); assert_eq!( - radroots_sdk_geonames_database_path_from_cache_root(&cache_root), + geonames.database_path().expect("geonames database path"), cache_root .join("shared") .join("geonames") .join("geonames-1.0.db") ); assert_eq!( - radroots_sdk_geonames_database_path_from_cache_root_for_version(&cache_root, "1.1"), + geonames + .database_path_for_version("1.1") + .expect("geonames version path"), cache_root .join("shared") .join("geonames") .join("geonames-1.1.db") ); - let status = - radroots_sdk_inspect_geonames_database_in_cache_root(&cache_root).expect("inspection"); + let status = geonames.inspect().expect("inspection"); assert_eq!(status.state, GeoNamesAssetState::Missing); assert_eq!(status.version, "1.0"); assert_eq!( @@ -46,3 +55,27 @@ fn sdk_geonames_facade_resolves_shared_cache_paths_and_reports_missing_state() { .join("geonames-1.0.db") ); } + +#[tokio::test] +async fn sdk_geonames_client_reports_missing_config_as_structured_error() { + let sdk = RadrootsClient::builder().build().await.expect("sdk"); + let error = sdk + .geonames() + .database_path() + .expect_err("missing geonames config"); + + match &error { + RadrootsSdkError::GeoNames { kind, .. } => { + assert_eq!(*kind, RadrootsSdkGeoNamesErrorKind::Configuration); + } + other => panic!("expected geonames config error, got {other}"), + } + assert_eq!(error.code(), "geonames_configuration"); + assert_eq!(error.class(), RadrootsSdkErrorClass::Configuration); + assert!(!error.retryable()); + assert_eq!( + error.recovery_actions(), + vec![RadrootsSdkRecoveryAction::ConfigureGeoNamesCache] + ); + assert_eq!(error.detail_json()["detail"]["kind"], "configuration"); +} diff --git a/crates/sdk/tests/runtime_foundation.rs b/crates/sdk/tests/runtime_foundation.rs @@ -2,12 +2,12 @@ use radroots_sdk::{ BackupRequest, IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, RadrootsClient, - RadrootsSdkClock, RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkRecoveryAction, - RadrootsSdkStorageConfig, RadrootsSdkTimestamp, RestoreRequest, SDK_IDEMPOTENCY_KEY_MAX_LEN, - SDK_RELAY_TARGET_MAX_COUNT, SdkBackupState, SdkBackupVerification, SdkEventStoreStorageStatus, - SdkIdempotencyKey, SdkOutboxStorageStatus, SdkRelayTargetPolicy, SdkRelayTargetSet, - SdkRelayUrlPolicy, SdkRestoreState, SdkSqliteStoreStatus, SdkStorageKind, StorageStatusReceipt, - StorageStatusRequest, + RadrootsSdkClock, RadrootsSdkError, RadrootsSdkErrorClass, RadrootsSdkGeoNamesErrorKind, + RadrootsSdkRecoveryAction, RadrootsSdkStorageConfig, RadrootsSdkTimestamp, RestoreRequest, + SDK_IDEMPOTENCY_KEY_MAX_LEN, SDK_RELAY_TARGET_MAX_COUNT, SdkBackupState, SdkBackupVerification, + SdkEventStoreStorageStatus, SdkIdempotencyKey, SdkOutboxStorageStatus, SdkRelayTargetPolicy, + SdkRelayTargetSet, SdkRelayUrlPolicy, SdkRestoreState, SdkSqliteStoreStatus, SdkStorageKind, + StorageStatusReceipt, StorageStatusRequest, }; use std::path::PathBuf; @@ -19,6 +19,7 @@ async fn sdk_builder_defaults_to_memory_storage_and_no_relays() { assert!(sdk.storage_paths().is_none()); let _listings = sdk.listings(); let _market = sdk.market(); + let _geonames = sdk.geonames(); let _trades = sdk.trades(); let _sync = sdk.sync(); let _dvm = sdk.dvm(); @@ -471,6 +472,16 @@ fn sdk_error_contract_methods_cover_all_variants() { vec![RadrootsSdkRecoveryAction::InspectLocalStores], ), ( + RadrootsSdkError::GeoNames { + kind: RadrootsSdkGeoNamesErrorKind::Download, + message: "download".to_owned(), + }, + "geonames_download", + RadrootsSdkErrorClass::Transport, + true, + vec![RadrootsSdkRecoveryAction::RetryGeoNamesDownload], + ), + ( RadrootsSdkError::RelayTransport { message: "relay".to_owned(), }, diff --git a/crates/sdk/tests/unit/runtime_tests.rs b/crates/sdk/tests/unit/runtime_tests.rs @@ -128,6 +128,7 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures( _event_store: memory.event_store, _outbox: memory.outbox, storage_paths: None, + geonames: None, clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)), relay_urls: Vec::new(), publish_transport: SdkPublishTransport::DirectNostrRelay, @@ -148,6 +149,7 @@ async fn open_storage_and_storage_kind_cover_memory_directory_and_file_failures( _event_store: directory_storage.event_store, _outbox: directory_storage.outbox, storage_paths: Some(directory_paths), + geonames: None, clock: RadrootsSdkClock::Fixed(RadrootsSdkTimestamp::from_unix_seconds(1)), relay_urls: Vec::new(), publish_transport: SdkPublishTransport::DirectNostrRelay,