// file: crates/ksp-config-lib/src/wallet.rs // version: 2 /// Effective standard Wallet configuration resolved from Config. #[derive(Clone, Eq, PartialEq)] pub struct ResolvedWalletConfig { effective: crate::ResolvedConfigJson, effective_wallets_directory: std::path::PathBuf, file_id: crate::ConfigFileId, profile_id: String, selection_source: crate::ConfigProfileSelectionSource, source_path: std::path::PathBuf, wallets_directory: std::path::PathBuf, wallets_subdirectory: std::option::Option, } impl ResolvedWalletConfig { /// Returns the detailed environment-resolved effective Config view. #[must_use] pub const fn effective(&self) -> &crate::ResolvedConfigJson { return &self.effective; } /// Returns the effective Wallet directory after applying the optional profile subdirectory. #[must_use] pub fn effective_wallets_directory(&self) -> &std::path::Path { return self.effective_wallets_directory.as_path(); } /// Returns the logical Config document identifier used by this runtime configuration. #[must_use] pub const fn file_id(&self) -> &crate::ConfigFileId { return &self.file_id; } /// Returns the selected standard Wallet profile identifier. #[must_use] pub fn profile_id(&self) -> &str { return self.profile_id.as_str(); } /// Returns the source that selected the standard Wallet profile. #[must_use] pub const fn selection_source(&self) -> crate::ConfigProfileSelectionSource { return self.selection_source; } /// Returns the physical source Config document path. #[must_use] pub fn source_path(&self) -> &std::path::Path { return self.source_path.as_path(); } /// Returns the resolved global Wallet root directory. #[must_use] pub fn wallets_directory(&self) -> &std::path::Path { return self.wallets_directory.as_path(); } /// Returns the validated optional relative Wallet subdirectory selected by the profile. #[must_use] pub fn wallets_subdirectory(&self) -> std::option::Option<&std::path::Path> { return self.wallets_subdirectory.as_deref(); } } impl std::fmt::Debug for ResolvedWalletConfig { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { return formatter .debug_struct("ResolvedWalletConfig") .field("effective", &self.effective) .field("effective_wallets_directory", &self.effective_wallets_directory) .field("file_id", &self.file_id) .field("profile_id", &self.profile_id) .field("selection_source", &self.selection_source) .field("source_path", &self.source_path) .field("wallets_directory", &self.wallets_directory) .field("wallets_subdirectory", &self.wallets_subdirectory) .finish(); } } impl crate::ConfigDocumentEngine { /// Loads the standard Wallet document, selects a profile, resolves environment placeholders and validates the effective directory contract. /// /// `requested_profile = None` uses the document `default_profile`; `Some(profile_id)` requests an explicit profile. `wallets_directory` may be absolute /// or relative to the process current working directory. `wallets_subdirectory` is always relative and may contain nested normal path components only. pub fn load_resolved_wallet_config( &self, requested_profile: std::option::Option<&str>, environment: &crate::ConfigEnvironment, ) -> ksp_core_lib::Result { let file_id = crate::ConfigFileId::new(crate::FILE_ID_STD_WALLET); let file_id = match file_id { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let profile = self.load_resolved_profile(&file_id, requested_profile); let profile = match profile { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return resolve_wallet_profile(&profile, environment); } /// Maps an already resolved standard Wallet profile to the runtime Wallet Config adapter. /// /// This entry point is intended for profiles selected by a composite and preserves their original selection source instead of reclassifying the profile as /// an explicit selection. The profile must reference `cfg.std.wallet`. pub fn resolve_wallet_config_profile( &self, profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment, ) -> ksp_core_lib::Result { if profile.file_id().as_str() != crate::FILE_ID_STD_WALLET { return std::result::Result::Err(effective_error(profile, "resolved Config profile does not reference the standard Wallet document")); } let descriptor = self.registry().descriptor(profile.file_id()); if let std::result::Result::Err(error) = descriptor { return std::result::Result::Err(error); } return resolve_wallet_profile(profile, environment); } } #[derive(serde::Deserialize)] #[serde(deny_unknown_fields)] struct EffectiveWalletSource { format_version: u32, profile_id: String, wallets_directory: String, wallets_subdirectory: std::option::Option, } /// Validates standard Wallet source path invariants that are meaningful before environment resolution. pub(crate) fn validate_wallet_document_contract(document: &crate::ConfigJsonDocument) -> ksp_core_lib::Result<()> { if document.file_id().as_str() != crate::FILE_ID_STD_WALLET { return std::result::Result::Ok(()); } let profiles = document.value().get("profiles").and_then(serde_json::Value::as_array); let profiles = match profiles { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok(()), }; for (profile_index, profile) in profiles.iter().enumerate() { let subdirectory = profile.get("wallets_subdirectory").and_then(serde_json::Value::as_str); if let std::option::Option::Some(value) = subdirectory { let validation = validate_relative_subdirectory(value); if validation.is_err() { return std::result::Result::Err( wallet_document_error(document, "wallets_subdirectory must contain only relative normal path components") .with_context("profile_index", profile_index.to_string()), ); } } } return std::result::Result::Ok(()); } fn resolve_wallet_profile(profile: &crate::ResolvedConfigProfile, environment: &crate::ConfigEnvironment) -> ksp_core_lib::Result { let effective = profile.resolve_effective_environment_detailed(environment); let effective = match effective { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if effective.sensitivity().is_secret() { return std::result::Result::Err(effective_error(profile, "standard Wallet configuration must not consume Secret environment values")); } let source = serde_json::from_value::(effective.value().clone()); let source = match source { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err( effective_error(profile, "effective Wallet Config cannot be decoded into the runtime adapter contract").with_source(error), ); }, }; if source.format_version != 1 { return std::result::Result::Err(effective_error(profile, "effective Wallet format_version is unsupported")); } if source.profile_id != profile.profile_id() { return std::result::Result::Err(effective_error(profile, "effective Wallet profile_id does not match the selected profile")); } let wallets_directory = resolve_wallets_directory(source.wallets_directory.as_str(), profile); let wallets_directory = match wallets_directory { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let wallets_subdirectory = resolve_wallets_subdirectory(source.wallets_subdirectory.as_deref(), profile); let wallets_subdirectory = match wallets_subdirectory { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let effective_wallets_directory = match &wallets_subdirectory { std::option::Option::Some(value) => wallets_directory.join(value), std::option::Option::None => wallets_directory.clone(), }; return std::result::Result::Ok(ResolvedWalletConfig { effective, effective_wallets_directory, file_id: profile.file_id().clone(), profile_id: profile.profile_id().to_owned(), selection_source: profile.selection_source(), source_path: profile.path().to_path_buf(), wallets_directory, wallets_subdirectory, }); } fn resolve_wallets_directory(value: &str, profile: &crate::ResolvedConfigProfile) -> ksp_core_lib::Result { if value.trim().is_empty() { return std::result::Result::Err(effective_field_error(profile, "wallets_directory", "effective Wallet wallets_directory must not be empty")); } let path = std::path::PathBuf::from(value); if path.is_absolute() { return std::result::Result::Ok(path); } let current_directory = std::env::current_dir(); return match current_directory { std::result::Result::Ok(current_directory) => std::result::Result::Ok(current_directory.join(path)), std::result::Result::Err(error) => std::result::Result::Err( effective_field_error(profile, "wallets_directory", "process current working directory cannot be resolved").with_source(error), ), }; } fn resolve_wallets_subdirectory( value: std::option::Option<&str>, profile: &crate::ResolvedConfigProfile, ) -> ksp_core_lib::Result> { let value = match value { std::option::Option::Some(value) => value, std::option::Option::None => return std::result::Result::Ok(std::option::Option::None), }; let validation = validate_relative_subdirectory(value); if let std::result::Result::Err(()) = validation { return std::result::Result::Err(effective_field_error( profile, "wallets_subdirectory", "effective Wallet wallets_subdirectory must contain only relative normal path components", )); } return std::result::Result::Ok(std::option::Option::Some(std::path::PathBuf::from(value))); } fn validate_relative_subdirectory(value: &str) -> std::result::Result<(), ()> { if value.trim().is_empty() { return std::result::Result::Err(()); } let path = std::path::Path::new(value); if path.is_absolute() { return std::result::Result::Err(()); } let mut component_count: usize = 0; for component in path.components() { match component { std::path::Component::Normal(_) => component_count += 1, std::path::Component::CurDir | std::path::Component::ParentDir | std::path::Component::RootDir | std::path::Component::Prefix(_) => { return std::result::Result::Err(()); }, } } if component_count == 0 { return std::result::Result::Err(()); } return std::result::Result::Ok(()); } fn wallet_document_error(document: &crate::ConfigJsonDocument, reason: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_DOCUMENT_SEMANTIC_INVALID, "standard Wallet Config violates KSP semantic invariants") .with_context("file_id", document.file_id().as_str()) .with_context("path", document.path().to_string_lossy().into_owned()) .with_context("reason", reason); } fn effective_field_error(profile: &crate::ResolvedConfigProfile, field: &'static str, reason: &'static str) -> ksp_core_lib::Error { return effective_error(profile, reason).with_context("field", field); } fn effective_error(profile: &crate::ResolvedConfigProfile, reason: &'static str) -> ksp_core_lib::Error { return ksp_core_lib::Error::new(crate::ERROR_CODE_EFFECTIVE_CONFIG_INVALID, "effective Config cannot be mapped to the requested runtime contract") .with_context("file_id", profile.file_id().as_str()) .with_context("path", profile.path().to_string_lossy().into_owned()) .with_context("profile_id", profile.profile_id()) .with_context("reason", reason); } #[cfg(test)] #[path = "../unit_tests/wallet.rs"] mod tests;