254 lines
10 KiB
Rust
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()
|
|
);
|
|
}
|
|
}
|