Files
khadhroony-bot3/ks-pipeline-demo-scenarios/src/environment.rs
2026-08-11 22:22:40 +02:00

254 lines
10 KiB
Rust

// file: ks-pipeline-demo-scenarios/src/environment.rs
// version: 9
//! Environment initialization for opt-in demonstration scenarios.
/// Loads the workspace `.env` selected by `ks-config`.
pub fn initialize_demo_scenario_environment(
workspace_root: &std::path::Path,
) -> ks_core::Result<ks_config::EnvironmentLoadReport> {
return ks_config::load_workspace_environment(workspace_root);
}
/// Loads the shared default Solana configuration or one explicit scenario composition override.
///
/// Shared documents are the canonical default when `KS_DEVNET_CONFIG_PATH` is absent.
/// An explicit path may select a dedicated composition without making one mandatory for scenarios.
pub fn load_demo_scenario_config(
workspace_root: &std::path::Path,
) -> ks_core::Result<ks_config::AppConfig> {
match initialize_demo_scenario_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let configured_path = std::env::var("KS_DEVNET_CONFIG_PATH")
.ok()
.map(|value| return value.trim().to_string())
.filter(|value| return !value.is_empty());
if let std::option::Option::Some(value) = configured_path {
let path = std::path::PathBuf::from(value);
let resolved_path = if path.is_absolute() { path } else { workspace_root.join(path) };
let composed = match ks_config::read_composed_app_config_with_environment(
resolved_path.as_path(),
workspace_root,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(composed.app_config);
}
return ks_config::read_default_shared_app_config_with_environment(workspace_root);
}
/// Resolves one compatible Devnet profile for demo scenarios.
///
/// An explicit name has priority, followed by `KS_DEVNET_PROFILE`, then the
/// first compatible Devnet profile declared by the configuration.
pub fn resolve_demo_devnet_profile(
config: &ks_config::AppConfig,
requested_profile_name: std::option::Option<&str>,
) -> ks_core::Result<ks_config::ProfileConfig> {
let explicit = requested_profile_name
.map(str::trim)
.filter(|value| return !value.is_empty())
.map(std::string::ToString::to_string);
let selected_name = match explicit {
std::option::Option::Some(value) => std::option::Option::Some(value),
std::option::Option::None => std::env::var("KS_DEVNET_PROFILE")
.ok()
.map(|value| return value.trim().to_string())
.filter(|value| return !value.is_empty()),
};
for profile in &config.profiles {
let compatible = profile.wallet.cluster == "devnet"
&& profile.wallet.temporary_wallet_enabled
&& profile.wallet.temporary_wallet_persist;
if !compatible {
continue;
}
if let std::option::Option::Some(name) = selected_name.as_ref()
&& profile.name != *name
{
continue;
}
return std::result::Result::Ok(profile.clone());
}
return match selected_name {
std::option::Option::Some(name) => std::result::Result::Err(ks_core::Error::config(
format!("compatible Devnet profile '{name}' is unavailable"),
)),
std::option::Option::None => std::result::Result::Err(ks_core::Error::config(
"no compatible Devnet profile is configured",
)),
};
}
/// Backend-agnostic readiness report for one Devnet profile store.
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DevnetProfileStoreReadiness {
/// Selected Devnet profile.
pub profile_name: std::string::String,
/// Selected backend code.
pub backend: std::string::String,
/// Whether schema creation was allowed by configuration.
pub auto_initialize_schema: bool,
/// Number of known logical resources already present before preparation.
pub available_resources_before: usize,
/// Number of known logical resources created during preparation.
pub created_resources: usize,
/// Number of known logical resources present after preparation.
pub available_resources_after: usize,
/// Total number of logical resources expected by the current store contract.
pub expected_resources: usize,
}
/// Builds backend-agnostic store-open options from one resolved profile.
fn store_open_options_from_profile(
profile: &ks_config::ProfileConfig,
) -> ks_core::Result<ks_store::StoreOpenOptions> {
return ks_store::StoreOpenOptions::new(
profile.database.enabled,
profile.database.backend.clone(),
profile.database.backend_options.clone(),
);
}
/// Opens the backend-agnostic store selected by one resolved profile.
#[cfg(test)]
pub(crate) async fn open_profile_store(
profile: &ks_config::ProfileConfig,
) -> ks_core::Result<ks_store::Store> {
let options = match store_open_options_from_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return ks_store::Store::open(options).await;
}
/// Verifies or initializes the store selected by one Devnet profile.
pub async fn prepare_demo_devnet_profile_store(
profile: &ks_config::ProfileConfig,
) -> ks_core::Result<crate::DevnetProfileStoreReadiness> {
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(ks_core::Error::config(format!(
"profile '{}' is not configured for Devnet",
profile.name
)));
}
if !profile.database.enabled {
return std::result::Result::Err(ks_core::Error::config(format!(
"Devnet profile '{}' has database storage disabled",
profile.name
)));
}
let options = match store_open_options_from_profile(profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configuration = match options.configuration_summary() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match ks_store::Store::open(options).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let initialization = store.initialization_summary();
if initialization.status != ks_store::StoreInitializationStatus::Ready {
return std::result::Result::Err(ks_core::Error::config(format!(
"Devnet profile '{}' store model is incomplete and automatic initialization is {}",
profile.name,
if configuration.auto_initialize_schema { "enabled" } else { "disabled" }
)));
}
return std::result::Result::Ok(crate::DevnetProfileStoreReadiness {
profile_name: profile.name.clone(),
backend: configuration.backend_code,
auto_initialize_schema: configuration.auto_initialize_schema,
available_resources_before: initialization
.available_resource_count
.saturating_sub(initialization.created_resource_count)
as usize,
created_resources: initialization.created_resource_count as usize,
available_resources_after: initialization.available_resource_count as usize,
expected_resources: initialization.expected_resource_count as usize,
});
}
#[cfg(test)]
pub(crate) fn override_profile_store_url_for_test(
profile: &mut ks_config::ProfileConfig,
database_url: std::string::String,
) -> ks_core::Result<()> {
profile.database.enabled = true;
profile.database.backend = "postgres".to_string();
let backend_options = match profile.database.backend_options.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::config(
"test store backend options must be an object",
));
},
};
backend_options.insert("url".to_string(), serde_json::Value::String(database_url));
backend_options.insert("auto_initialize_schema".to_string(), serde_json::Value::Bool(true));
return std::result::Result::Ok(());
}
#[cfg(test)]
pub(crate) async fn open_test_store(
database_url: std::string::String,
) -> ks_core::Result<ks_store::Store> {
let options = match ks_store::StoreOpenOptions::new(
true,
"postgres",
serde_json::json!({
"url": database_url,
"max_connections": 5,
"connect_timeout_ms": 5_000,
"auto_initialize_schema": true
}),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return ks_store::Store::open(options).await;
}
#[cfg(test)]
mod tests {
fn example_config() -> ks_config::AppConfig {
return match ks_config::parse_config_json(include_str!(
"../../test-fixtures/config/resolved.app.config.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
};
}
#[test]
fn resolver_finds_a_compatible_devnet_profile_without_a_fixed_name() {
let config = example_config();
let profile = match super::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
assert_eq!(profile.wallet.cluster, "devnet");
assert!(profile.wallet.temporary_wallet_enabled);
assert!(profile.wallet.temporary_wallet_persist);
}
#[test]
fn resolver_rejects_an_explicit_non_devnet_profile() {
let config = example_config();
assert!(
super::resolve_demo_devnet_profile(
&config,
std::option::Option::Some("mainnet_research"),
)
.is_err()
);
}
}