v0.2.6-pre.003
This commit is contained in:
290
crates/ksp-config-lib/src/wallet.rs
Normal file
290
crates/ksp-config-lib/src/wallet.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
// file: crates/ksp-config-lib/src/wallet.rs
|
||||
// version: 1
|
||||
|
||||
/// 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<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
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<ResolvedWalletConfig> {
|
||||
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<ResolvedWalletConfig> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
/// 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 let std::result::Result::Err(_) = validation {
|
||||
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<ResolvedWalletConfig> {
|
||||
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::<EffectiveWalletSource>(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<std::path::PathBuf> {
|
||||
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<std::option::Option<std::path::PathBuf>> {
|
||||
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;
|
||||
Reference in New Issue
Block a user