app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 69534aab0780caa719e9a775dd4d08543d7b8b75
parent 72694b715000952c36c5b0e6feb1bcc6339fb9c7
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Mar 2026 22:09:43 +0000

desktop: add non-blocking offline geocoder init

- add the radroots geocoder dependency back for the desktop launcher slice only
- copy the optional app-owned geocoder asset next to the desktop binary at build time without making it a hard build requirement
- initialize the offline geocoder on a background thread and surface ready or unavailable runtime state without blocking app startup
- add focused desktop tests for staged geocoder paths and unavailable-state messaging

Diffstat:
MCargo.lock | 290++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
MCargo.toml | 1+
Mcrates/desktop/Cargo.toml | 1+
Mcrates/desktop/build.rs | 39+++++++++++++++++++++++++++++++++++++++
Mcrates/desktop/src/main.rs | 45++++++++++++++++++++++++++++++++++++++++++---
Acrates/desktop/src/offline_geocoder.rs | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 519 insertions(+), 4 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1047,6 +1047,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1225,11 +1237,24 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] name = "gl_generator" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1419,6 +1444,12 @@ dependencies = [ ] [[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1536,6 +1567,12 @@ dependencies = [ ] [[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] name = "idna" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1578,6 +1615,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -1751,6 +1790,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1795,6 +1840,17 @@ dependencies = [ ] [[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "linux-keyutils" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2710,6 +2766,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" [[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] name = "proc-macro-crate" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2770,6 +2836,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] name = "radroots-app-android" version = "0.1.0" dependencies = [ @@ -2815,6 +2887,7 @@ dependencies = [ "objc2-foundation 0.3.2", "radroots-app-apple-security", "radroots-app-core", + "radroots-geocoder", "radroots-identity", "radroots-nostr-accounts", "wgpu", @@ -2866,6 +2939,15 @@ dependencies = [ ] [[package]] +name = "radroots-geocoder" +version = "0.1.0-alpha.1" +dependencies = [ + "rusqlite", + "serde", + "thiserror 1.0.69", +] + +[[package]] name = "radroots-identity" version = "0.1.0-alpha.1" dependencies = [ @@ -2894,6 +2976,7 @@ version = "0.1.0-alpha.1" dependencies = [ "keyring", "radroots-identity", + "radroots-nostr-signer", "radroots-runtime", "serde", "serde_json", @@ -2902,6 +2985,33 @@ dependencies = [ ] [[package]] +name = "radroots-nostr-connect" +version = "0.1.0-alpha.1" +dependencies = [ + "nostr", + "serde", + "serde_json", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "radroots-nostr-signer" +version = "0.1.0-alpha.1" +dependencies = [ + "hex", + "nostr", + "radroots-identity", + "radroots-nostr-connect", + "radroots-runtime", + "serde", + "sha2", + "thiserror 1.0.69", + "url", + "uuid", +] + +[[package]] name = "radroots-runtime" version = "0.1.0-alpha.1" dependencies = [ @@ -3045,6 +3155,30 @@ dependencies = [ ] [[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] name = "rust-ini" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3458,6 +3592,18 @@ dependencies = [ ] [[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3891,6 +4037,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3920,6 +4072,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3963,6 +4126,15 @@ dependencies = [ ] [[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] name = "wasm-bindgen" version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4022,6 +4194,40 @@ dependencies = [ ] [[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] name = "wayland-backend" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4733,6 +4939,88 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" diff --git a/Cargo.toml b/Cargo.toml @@ -32,6 +32,7 @@ nostr = { version = "0.44.1", default-features = false, features = ["std"] } nostr-browser-signer = "0.44.1" objc2-foundation = { version = "0.3.2", default-features = false, features = ["std"] } radroots-app-apple-security = { path = "crates/apple/security" } +radroots-geocoder = { path = "../lib/crates/geocoder" } radroots-identity = { path = "../lib/crates/identity", default-features = false, features = ["std"] } radroots-nostr-accounts = { path = "../lib/crates/nostr-accounts", default-features = false, features = ["std", "file-store", "os-keyring"] } wasm-bindgen-futures = "0.4.50" diff --git a/crates/desktop/Cargo.toml b/crates/desktop/Cargo.toml @@ -20,6 +20,7 @@ eframe = { workspace = true, features = ["wgpu", "wayland", "x11"] } egui.workspace = true image.workspace = true radroots-app-core = { path = "../core" } +radroots-geocoder.workspace = true radroots-nostr-accounts.workspace = true zeroize.workspace = true diff --git a/crates/desktop/build.rs b/crates/desktop/build.rs @@ -4,6 +4,7 @@ use std::process::Command; fn main() { println!("cargo:rerun-if-changed=build.rs"); + sync_optional_geocoder_asset(); if env::var("CARGO_CFG_TARGET_OS").ok().as_deref() != Some("macos") { return; @@ -46,6 +47,44 @@ fn main() { ); } +fn sync_optional_geocoder_asset() { + let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").expect("manifest dir")); + let source_path = manifest_dir.join("../../assets/geocoder/geonames.db"); + println!("cargo:rerun-if-changed={}", source_path.display()); + + let profile_dir = target_profile_dir(); + let target_path = profile_dir.join("geonames.db"); + + if source_path.is_file() { + std::fs::copy(&source_path, &target_path).unwrap_or_else(|err| { + panic!( + "failed to copy optional desktop geocoder asset from {} to {}: {err}", + source_path.display(), + target_path.display() + ) + }); + return; + } + + if target_path.exists() { + std::fs::remove_file(&target_path).unwrap_or_else(|err| { + panic!( + "failed to remove stale desktop geocoder asset at {}: {err}", + target_path.display() + ) + }); + } +} + +fn target_profile_dir() -> PathBuf { + let out_dir = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR")); + out_dir + .ancestors() + .nth(3) + .unwrap_or_else(|| panic!("unexpected cargo OUT_DIR layout: {}", out_dir.display())) + .to_path_buf() +} + fn emit_rerun_paths(package_dir: &Path) { println!( "cargo:rerun-if-changed={}", diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -10,7 +10,8 @@ use radroots_app_apple_security::{ }; use radroots_app_core::{ APP_NAME, HomeActionKind, HomeActionResult, HomeActionState, IdentityGateState, - ImportActionState, RadrootsApp, RadrootsAppBackend, SetupActionState, + ImportActionState, RadrootsApp, RadrootsAppBackend, RadrootsOfflineGeocoderState, + SetupActionState, }; #[cfg(target_os = "macos")] use radroots_identity::RadrootsIdentity; @@ -24,6 +25,10 @@ use std::sync::Arc; #[cfg(target_os = "macos")] use zeroize::Zeroizing; +mod offline_geocoder; + +use offline_geocoder::DesktopOfflineGeocoder; + const RADROOTS_DESKTOP_ICON_BYTES: &[u8] = include_bytes!("../assets/icons/radroots-logo.ico"); #[cfg(target_os = "macos")] @@ -50,9 +55,35 @@ fn desktop_icon() -> Option<egui::IconData> { }) } -struct DesktopBackend; +struct DesktopBackend { + offline_geocoder: DesktopOfflineGeocoder, +} impl DesktopBackend { + fn new() -> Self { + #[cfg(target_os = "macos")] + let offline_geocoder = match Self::app_data_root() { + Ok(app_data_root) => DesktopOfflineGeocoder::start(app_data_root), + Err(debug_message) => { + DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::Unavailable { + user_message: "Offline geocoder could not be initialized on this device." + .to_owned(), + debug_message, + }) + } + }; + + #[cfg(not(target_os = "macos"))] + let offline_geocoder = + DesktopOfflineGeocoder::from_state(RadrootsOfflineGeocoderState::Unavailable { + user_message: "Offline geocoder is not available in this desktop build.".to_owned(), + debug_message: "desktop offline geocoder initialization is only wired for macos" + .to_owned(), + }); + + Self { offline_geocoder } + } + #[cfg(target_os = "macos")] fn radroots_root() -> Result<PathBuf, String> { let base_dirs = @@ -252,6 +283,14 @@ impl RadrootsAppBackend for DesktopBackend { } } + fn offline_geocoder_state(&self) -> Option<RadrootsOfflineGeocoderState> { + Some(self.offline_geocoder.current_state()) + } + + fn poll_offline_geocoder_state(&self) -> Result<Option<RadrootsOfflineGeocoderState>, String> { + Ok(self.offline_geocoder.take_update()) + } + fn setup_action_state(&self) -> SetupActionState { #[cfg(target_os = "macos")] { @@ -400,7 +439,7 @@ fn main() -> eframe::Result<()> { eframe::run_native( APP_NAME, options, - Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(DesktopBackend))))), + Box::new(|_cc| Ok(Box::new(RadrootsApp::new(Box::new(DesktopBackend::new()))))), ) } diff --git a/crates/desktop/src/offline_geocoder.rs b/crates/desktop/src/offline_geocoder.rs @@ -0,0 +1,147 @@ +use radroots_app_core::RadrootsOfflineGeocoderState; +use radroots_geocoder::Geocoder; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + +const GEOCODER_ASSET_FILENAME: &str = "geonames.db"; + +#[derive(Clone)] +pub(crate) struct DesktopOfflineGeocoder { + current: Arc<Mutex<RadrootsOfflineGeocoderState>>, + changed: Arc<AtomicBool>, +} + +impl DesktopOfflineGeocoder { + pub(crate) fn from_state(state: RadrootsOfflineGeocoderState) -> Self { + Self { + current: Arc::new(Mutex::new(state)), + changed: Arc::new(AtomicBool::new(false)), + } + } + + pub(crate) fn start(app_data_root: PathBuf) -> Self { + let tracker = Self::from_state(RadrootsOfflineGeocoderState::Initializing); + + let current = Arc::clone(&tracker.current); + let changed = Arc::clone(&tracker.changed); + std::thread::spawn(move || { + let state = initialize_offline_geocoder(app_data_root.as_path()); + if let Ok(mut slot) = current.lock() { + *slot = state; + changed.store(true, Ordering::Release); + } + }); + + tracker + } + + pub(crate) fn current_state(&self) -> RadrootsOfflineGeocoderState { + self.current + .lock() + .map(|state| state.clone()) + .unwrap_or_else(|_| RadrootsOfflineGeocoderState::Unavailable { + user_message: "Offline geocoder is unavailable on this device.".to_owned(), + debug_message: "desktop offline geocoder state lock poisoned".to_owned(), + }) + } + + pub(crate) fn take_update(&self) -> Option<RadrootsOfflineGeocoderState> { + if self.changed.swap(false, Ordering::AcqRel) { + Some(self.current_state()) + } else { + None + } + } +} + +fn initialize_offline_geocoder(app_data_root: &Path) -> RadrootsOfflineGeocoderState { + match initialize_offline_geocoder_inner(app_data_root) { + Ok(()) => RadrootsOfflineGeocoderState::Ready, + Err(debug_message) => classify_initialize_error(debug_message), + } +} + +fn initialize_offline_geocoder_inner(app_data_root: &Path) -> Result<(), String> { + let source_path = runtime_asset_path()?; + if !source_path.is_file() { + return Err(format!( + "desktop bundled geocoder asset missing at {}", + source_path.display() + )); + } + + let staged_path = staged_db_path(app_data_root); + stage_runtime_asset(source_path.as_path(), staged_path.as_path())?; + Geocoder::open_path(staged_path.as_path()) + .map(|_| ()) + .map_err(|source| format!("failed to open staged geocoder db: {source}")) +} + +fn runtime_asset_path() -> Result<PathBuf, String> { + let executable_path = std::env::current_exe() + .map_err(|source| format!("failed to resolve desktop executable path: {source}"))?; + let Some(parent) = executable_path.parent() else { + return Err("desktop executable path did not have a parent directory".to_owned()); + }; + Ok(parent.join(GEOCODER_ASSET_FILENAME)) +} + +fn staged_db_path(app_data_root: &Path) -> PathBuf { + app_data_root.join("geocoder").join(GEOCODER_ASSET_FILENAME) +} + +fn stage_runtime_asset(source_path: &Path, staged_path: &Path) -> Result<(), String> { + let Some(parent) = staged_path.parent() else { + return Err("staged desktop geocoder path did not have a parent directory".to_owned()); + }; + std::fs::create_dir_all(parent) + .map_err(|source| format!("failed to create desktop geocoder directory: {source}"))?; + std::fs::copy(source_path, staged_path) + .map_err(|source| format!("failed to stage desktop geocoder asset: {source}"))?; + Ok(()) +} + +fn classify_initialize_error(debug_message: String) -> RadrootsOfflineGeocoderState { + let user_message = if debug_message.contains("asset missing") { + "Offline geocoder is not available in this build.".to_owned() + } else { + "Offline geocoder could not be initialized on this device.".to_owned() + }; + + RadrootsOfflineGeocoderState::Unavailable { + user_message, + debug_message, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn staged_db_path_uses_app_geocoder_directory() { + let app_data_root = PathBuf::from("/Users/example/.radroots/app/desktop"); + + assert_eq!( + staged_db_path(app_data_root.as_path()), + PathBuf::from("/Users/example/.radroots/app/desktop/geocoder/geonames.db") + ); + } + + #[test] + fn missing_asset_maps_to_build_unavailable_message() { + let state = classify_initialize_error( + "desktop bundled geocoder asset missing at /tmp/geonames.db".to_owned(), + ); + + assert_eq!( + state, + RadrootsOfflineGeocoderState::Unavailable { + user_message: "Offline geocoder is not available in this build.".to_owned(), + debug_message: "desktop bundled geocoder asset missing at /tmp/geonames.db" + .to_owned(), + } + ); + } +}