// 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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() ); } }