0.5.1-pre.007

This commit is contained in:
2026-08-10 09:28:57 +02:00
parent 34a670eaec
commit a2820062eb
81 changed files with 3357 additions and 2381 deletions

View File

@@ -1,10 +1,16 @@
// file: ks-config/src/composition.rs
// version: 1
// version: 2
//! Binary composition configuration and resolution into the shared runtime profile contract.
const COMPOSITION_JSON_SCHEMA: &str =
include_str!("../../config/schemas/composition.config.schema.json");
const DEFAULT_LOGGING_CONFIG_PATH: &str = "config/logging.config.json";
const DEFAULT_TRANSPORT_CONFIG_PATH: &str = "config/transport.config.json";
const DEFAULT_LISTENERS_CONFIG_PATH: &str = "config/listeners.config.json";
const DEFAULT_STORE_CONFIG_PATH: &str = "config/store.config.json";
const DEFAULT_WALLET_CONFIG_PATH: &str = "config/wallet.config.json";
const DEFAULT_EXECUTION_CONFIG_PATH: &str = "config/execution.config.json";
/// Root composition document used by a binary to select shared configuration profiles.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
@@ -26,33 +32,58 @@ pub struct CompositionConfigSources {
pub transport: std::string::String,
/// Listener configuration document path relative to the workspace root unless absolute.
pub listeners: std::string::String,
/// Store configuration document path relative to the workspace root unless absolute.
pub store: std::string::String,
/// Wallet configuration document path relative to the workspace root unless absolute.
pub wallet: std::string::String,
/// Execution configuration document path relative to the workspace root unless absolute.
pub execution: std::string::String,
}
/// Named composition profile referencing shared configuration profiles.
/// Named composition profile with optional overrides of each shared document default.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct CompositionProfileConfig {
/// Composition profile code.
pub name: std::string::String,
/// Application metadata retained until the remaining configuration split is completed.
/// Application metadata retained until the application-specific split is completed.
pub app: crate::AppSectionConfig,
/// Logging profile selected from the referenced logging document.
pub logging_profile: std::string::String,
/// Transport profile selected from the referenced transport document.
pub transport_profile: std::string::String,
/// Listener profile selected from the referenced listener document.
pub listeners_profile: std::string::String,
/// Database configuration retained until the store split prerelease.
pub database: crate::DatabaseConfig,
/// Local data directories retained until their owning configuration files are split.
pub data: crate::DataConfig,
/// Wallet configuration retained until the wallet split prerelease.
pub wallet: crate::WalletConfig,
/// Execution policy retained until the execution split prerelease.
pub execution: crate::ExecutionConfig,
/// Desktop demo flags retained until the binary-specific split prerelease.
/// Optional logging profile override.
pub logging_profile: std::option::Option<std::string::String>,
/// Optional transport profile override.
pub transport_profile: std::option::Option<std::string::String>,
/// Optional listener profile override.
pub listeners_profile: std::option::Option<std::string::String>,
/// Optional store profile override.
pub store_profile: std::option::Option<std::string::String>,
/// Optional wallet profile override.
pub wallet_profile: std::option::Option<std::string::String>,
/// Optional execution profile override.
pub execution_profile: std::option::Option<std::string::String>,
/// Desktop demo flags retained until the application-specific split is completed.
pub demo: crate::DemoConfig,
}
/// Resolved application configuration and the source documents needed by a binary.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComposedAppConfig {
/// Parsed binary composition document.
pub composition: CompositionConfigDocument,
/// Resolved runtime application configuration.
pub app_config: crate::AppConfig,
/// Resolved logging source path.
pub logging_path: std::path::PathBuf,
/// Resolved transport source path.
pub transport_path: std::path::PathBuf,
/// Resolved listener source path.
pub listeners_path: std::path::PathBuf,
/// Resolved store source path.
pub store_path: std::path::PathBuf,
/// Resolved wallet source path.
pub wallet_path: std::path::PathBuf,
/// Resolved execution source path.
pub execution_path: std::path::PathBuf,
}
/// Returns the embedded binary composition JSON Schema text.
pub fn composition_json_schema_text() -> &'static str {
return COMPOSITION_JSON_SCHEMA;
@@ -60,9 +91,9 @@ pub fn composition_json_schema_text() -> &'static str {
/// Parses the embedded binary composition JSON Schema into a JSON value.
pub fn composition_json_schema_value() -> ks_core::Result<serde_json::Value> {
let schema_result = serde_json::from_str::<serde_json::Value>(COMPOSITION_JSON_SCHEMA);
return match schema_result {
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
let result = serde_json::from_str::<serde_json::Value>(COMPOSITION_JSON_SCHEMA);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"composition_config_schema_parse_failed",
error.to_string(),
@@ -73,11 +104,11 @@ pub fn composition_json_schema_value() -> ks_core::Result<serde_json::Value> {
/// Validates raw binary composition JSON against its embedded schema.
pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> {
let schema = match composition_json_schema_value() {
std::result::Result::Ok(schema) => schema,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
std::result::Result::Ok(instance) => instance,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"composition_config_json_parse_failed",
@@ -86,7 +117,7 @@ pub fn validate_composition_json_schema(raw_json: &str) -> ks_core::Result<()> {
},
};
let validator = match jsonschema::validator_for(&schema) {
std::result::Result::Ok(validator) => validator,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"composition_config_schema_compile_failed",
@@ -110,7 +141,7 @@ pub fn parse_composition_json(raw_json: &str) -> ks_core::Result<CompositionConf
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let document = match serde_json::from_str::<CompositionConfigDocument>(raw_json) {
std::result::Result::Ok(document) => document,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"composition_config_json_decode_failed",
@@ -129,7 +160,7 @@ pub fn read_composition_json_file(
path: &std::path::Path,
) -> ks_core::Result<CompositionConfigDocument> {
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(content) => content,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"composition_config_file_read_failed",
@@ -150,7 +181,7 @@ pub fn read_composition_json_file_with_environment(
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(content) => content,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"composition_config_file_read_failed",
@@ -158,8 +189,7 @@ pub fn read_composition_json_file_with_environment(
));
},
};
let resolved = crate::resolve_environment_placeholders(&raw_json);
return parse_composition_json(&resolved);
return parse_composition_json(&crate::resolve_environment_placeholders(&raw_json));
}
/// Returns one named binary composition profile.
@@ -197,36 +227,79 @@ pub fn resolve_composition_source_path(
return workspace_root.join(path);
}
/// Resolves all shared transport/listener selections into the existing runtime application contract.
/// Resolves one optional composition override against a shared document default profile.
pub fn resolve_profile_selection<'a>(
override_name: std::option::Option<&'a str>,
default_profile: &'a str,
) -> &'a str {
return match override_name {
std::option::Option::Some(value) if !value.trim().is_empty() => value,
_ => default_profile,
};
}
/// Resolves all shared source selections into the runtime application contract.
pub fn compose_app_config(
document: &CompositionConfigDocument,
transport: &crate::TransportConfigDocument,
listeners: &crate::ListenersConfigDocument,
store: &crate::StoreConfigDocument,
wallet: &crate::WalletConfigDocument,
execution: &crate::ExecutionConfigDocument,
) -> ks_core::Result<crate::AppConfig> {
let mut profiles = std::vec::Vec::<crate::ProfileConfig>::new();
for profile in &document.profiles {
let transport_profile =
match crate::transport_profile(transport, profile.transport_profile.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let listeners_profile =
match crate::listeners_profile(listeners, profile.listeners_profile.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let transport_name = resolve_profile_selection(
profile.transport_profile.as_deref(),
transport.default_profile.as_str(),
);
let listeners_name = resolve_profile_selection(
profile.listeners_profile.as_deref(),
listeners.default_profile.as_str(),
);
let store_name = resolve_profile_selection(
profile.store_profile.as_deref(),
store.default_profile.as_str(),
);
let wallet_name = resolve_profile_selection(
profile.wallet_profile.as_deref(),
wallet.default_profile.as_str(),
);
let execution_name = resolve_profile_selection(
profile.execution_profile.as_deref(),
execution.default_profile.as_str(),
);
let transport_profile = match crate::transport_profile(transport, transport_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let listeners_profile = match crate::listeners_profile(listeners, listeners_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store_profile = match crate::store_profile(store, store_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet_profile = match crate::resolved_wallet_profile(wallet, wallet_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let execution_profile = match crate::execution_profile(execution, execution_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
profiles.push(crate::ProfileConfig {
name: profile.name.clone(),
app: profile.app.clone(),
database: profile.database.clone(),
data: profile.data.clone(),
database: store_profile.database.clone(),
solana: crate::SolanaConfig {
http_endpoints: transport_profile.http_endpoints.clone(),
ws_endpoints: transport_profile.ws_endpoints.clone(),
http_endpoints: transport_profile.http_endpoints,
ws_endpoints: transport_profile.ws_endpoints,
listeners: crate::resolved_listener_config(listeners_profile),
},
wallet: profile.wallet.clone(),
execution: profile.execution.clone(),
wallet: wallet_profile,
execution: execution_profile.config.clone(),
demo: profile.demo.clone(),
});
}
@@ -240,20 +313,28 @@ pub fn compose_app_config(
};
}
/// Reads a binary composition and its referenced transport/listener documents into the resolved runtime contract.
/// Reads a binary composition and all referenced shared documents into the runtime contract.
pub fn read_composed_app_config_with_environment(
composition_path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<crate::ComposedAppConfig> {
) -> ks_core::Result<ComposedAppConfig> {
let composition =
match read_composition_json_file_with_environment(composition_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let logging_path =
resolve_composition_source_path(workspace_root, composition.sources.logging.as_str());
let transport_path =
resolve_composition_source_path(workspace_root, composition.sources.transport.as_str());
let listeners_path =
resolve_composition_source_path(workspace_root, composition.sources.listeners.as_str());
let store_path =
resolve_composition_source_path(workspace_root, composition.sources.store.as_str());
let wallet_path =
resolve_composition_source_path(workspace_root, composition.sources.wallet.as_str());
let execution_path =
resolve_composition_source_path(workspace_root, composition.sources.execution.as_str());
let transport =
match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) {
std::result::Result::Ok(value) => value,
@@ -264,34 +345,120 @@ pub fn read_composed_app_config_with_environment(
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let app_config = match compose_app_config(&composition, &transport, &listeners) {
let store = match crate::read_store_json_file_with_environment(&store_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(crate::ComposedAppConfig {
let wallet = match crate::read_wallet_json_file_with_environment(&wallet_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let execution =
match crate::read_execution_json_file_with_environment(&execution_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
match validate_logging_references_with_environment(&logging_path, workspace_root, &composition)
{
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let app_config =
match compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(ComposedAppConfig {
composition,
app_config,
logging_path,
transport_path,
listeners_path,
store_path,
wallet_path,
execution_path,
});
}
/// Resolved application configuration and the source documents needed by a binary.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComposedAppConfig {
/// Parsed binary composition document.
pub composition: CompositionConfigDocument,
/// Resolved runtime application configuration.
pub app_config: crate::AppConfig,
/// Resolved transport source path.
pub transport_path: std::path::PathBuf,
/// Resolved listener source path.
pub listeners_path: std::path::PathBuf,
/// Loads canonical shared documents and composes their independent default profiles without an application composition file.
pub fn read_default_shared_app_config_with_environment(
workspace_root: &std::path::Path,
) -> ks_core::Result<crate::AppConfig> {
let transport_path = workspace_root.join(DEFAULT_TRANSPORT_CONFIG_PATH);
let listeners_path = workspace_root.join(DEFAULT_LISTENERS_CONFIG_PATH);
let store_path = workspace_root.join(DEFAULT_STORE_CONFIG_PATH);
let wallet_path = workspace_root.join(DEFAULT_WALLET_CONFIG_PATH);
let execution_path = workspace_root.join(DEFAULT_EXECUTION_CONFIG_PATH);
let transport =
match crate::read_transport_json_file_with_environment(&transport_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let listeners =
match crate::read_listeners_json_file_with_environment(&listeners_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match crate::read_store_json_file_with_environment(&store_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet = match crate::read_wallet_json_file_with_environment(&wallet_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let execution =
match crate::read_execution_json_file_with_environment(&execution_path, workspace_root) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let defaults = [
transport.default_profile.as_str(),
listeners.default_profile.as_str(),
store.default_profile.as_str(),
wallet.default_profile.as_str(),
execution.default_profile.as_str(),
];
let profile_name = if defaults.iter().all(|value| return *value == defaults[0]) {
defaults[0].to_string()
} else {
"shared_default".to_string()
};
let composition = CompositionConfigDocument {
active_profile: profile_name.clone(),
sources: CompositionConfigSources {
logging: DEFAULT_LOGGING_CONFIG_PATH.to_string(),
transport: DEFAULT_TRANSPORT_CONFIG_PATH.to_string(),
listeners: DEFAULT_LISTENERS_CONFIG_PATH.to_string(),
store: DEFAULT_STORE_CONFIG_PATH.to_string(),
wallet: DEFAULT_WALLET_CONFIG_PATH.to_string(),
execution: DEFAULT_EXECUTION_CONFIG_PATH.to_string(),
},
profiles: vec![CompositionProfileConfig {
name: profile_name.clone(),
app: crate::AppSectionConfig {
name: "khadhroony-solana".to_string(),
environment: profile_name,
},
logging_profile: std::option::Option::None,
transport_profile: std::option::Option::None,
listeners_profile: std::option::Option::None,
store_profile: std::option::Option::None,
wallet_profile: std::option::Option::None,
execution_profile: std::option::Option::None,
demo: crate::DemoConfig {
live_demo_enabled: false,
trading_demo_enabled: false,
},
}],
};
return compose_app_config(&composition, &transport, &listeners, &store, &wallet, &execution);
}
/// Validates a typed binary composition document before referenced documents are loaded.
pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks_core::Result<()> {
match crate::require_non_empty(&document.active_profile, "active_profile") {
match crate::require_non_empty(&document.active_profile, "composition.active_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
@@ -299,6 +466,9 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
&document.sources.logging,
&document.sources.transport,
&document.sources.listeners,
&document.sources.store,
&document.sources.wallet,
&document.sources.execution,
] {
match crate::require_non_empty(source, "composition.sources") {
std::result::Result::Ok(()) => (),
@@ -317,12 +487,19 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
for reference in
[&profile.logging_profile, &profile.transport_profile, &profile.listeners_profile]
{
match crate::require_non_empty(reference, "composition.profile.reference") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
for reference in [
profile.logging_profile.as_ref(),
profile.transport_profile.as_ref(),
profile.listeners_profile.as_ref(),
profile.store_profile.as_ref(),
profile.wallet_profile.as_ref(),
profile.execution_profile.as_ref(),
] {
if let std::option::Option::Some(value) = reference
&& let std::result::Result::Err(error) =
crate::require_non_empty(value, "composition.profile.reference")
{
return std::result::Result::Err(error);
}
}
if !names.insert(profile.name.clone()) {
@@ -338,14 +515,85 @@ pub fn validate_composition_document(document: &CompositionConfigDocument) -> ks
};
}
fn validate_logging_references_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
composition: &CompositionConfigDocument,
) -> ks_core::Result<()> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"composition_logging_file_read_failed",
error.to_string(),
));
},
};
let resolved = crate::resolve_environment_placeholders(&raw_json);
let value = match serde_json::from_str::<serde_json::Value>(&resolved) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"composition_logging_json_parse_failed",
error.to_string(),
));
},
};
let default_profile = match value.get("default_profile").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) if !value.trim().is_empty() => value,
_ => {
return std::result::Result::Err(ks_core::Error::new(
"composition_logging_default_profile_missing",
path.display().to_string(),
));
},
};
let profiles = match value.get("profiles").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(ks_core::Error::new(
"composition_logging_profiles_missing",
path.display().to_string(),
));
},
};
let mut names = std::collections::BTreeSet::<std::string::String>::new();
for profile in profiles {
if let std::option::Option::Some(name) =
profile.get("name").and_then(serde_json::Value::as_str)
{
names.insert(name.to_string());
}
}
if !names.contains(default_profile) {
return std::result::Result::Err(ks_core::Error::new(
"composition_logging_default_profile_not_found",
default_profile.to_string(),
));
}
for profile in &composition.profiles {
let selected =
resolve_profile_selection(profile.logging_profile.as_deref(), default_profile);
if !names.contains(selected) {
return std::result::Result::Err(ks_core::Error::new(
"composition_logging_profile_not_found",
selected.to_string(),
));
}
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
const DEFAULT_COMPOSITION: &str =
include_str!("../../config/kb-app-demo-desktop.default.config.json");
const EXAMPLE_COMPOSITION: &str =
include_str!("../../config/example.kb-app-demo-desktop.default.config.json");
const DEFAULT_TRANSPORT: &str = include_str!("../../config/transport.config.json");
const DEFAULT_LISTENERS: &str = include_str!("../../config/listeners.config.json");
#[test]
fn default_and_example_compositions_validate() {
@@ -354,20 +602,19 @@ mod tests {
}
#[test]
fn default_composition_resolves_shared_transport_and_listener_profiles() {
fn local_devnet_composition_uses_shared_defaults() {
let composition = match super::parse_composition_json(DEFAULT_COMPOSITION) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default composition must parse: {error}"),
};
let transport = match crate::parse_transport_json(DEFAULT_TRANSPORT) {
let profile = match super::composition_profile(&composition, "local_devnet") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default transport must parse: {error}"),
std::result::Result::Err(error) => {
panic!("local Devnet composition must resolve: {error}")
},
};
let listeners = match crate::parse_listeners_json(DEFAULT_LISTENERS) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default listeners must parse: {error}"),
};
let resolved = super::compose_app_config(&composition, &transport, &listeners);
assert!(resolved.is_ok());
assert!(profile.logging_profile.is_none());
assert!(profile.transport_profile.is_none());
assert!(profile.store_profile.is_none());
}
}

172
ks-config/src/execution.rs Normal file
View File

@@ -0,0 +1,172 @@
// file: ks-config/src/execution.rs
// version: 1
//! Independent execution policy configuration document loading and validation.
const EXECUTION_JSON_SCHEMA: &str =
include_str!("../../config/schemas/execution.config.schema.json");
/// Root execution policy document with one default profile and named alternatives.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct ExecutionConfigDocument {
/// Default execution profile used when a composition does not override it.
pub default_profile: std::string::String,
/// Named execution policy profiles.
pub profiles: std::vec::Vec<ExecutionProfileConfig>,
}
/// Named execution policy profile.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct ExecutionProfileConfig {
/// Profile code.
pub name: std::string::String,
/// Runtime execution safety configuration.
#[serde(flatten)]
pub config: crate::ExecutionConfig,
}
/// Returns the embedded execution JSON Schema text.
pub fn execution_json_schema_text() -> &'static str {
return EXECUTION_JSON_SCHEMA;
}
/// Parses and validates one execution configuration document.
pub fn parse_execution_json(raw_json: &str) -> ks_core::Result<ExecutionConfigDocument> {
let schema = match serde_json::from_str::<serde_json::Value>(EXECUTION_JSON_SCHEMA) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"execution_config_schema_parse_failed",
error.to_string(),
));
},
};
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"execution_config_json_parse_failed",
error.to_string(),
));
},
};
let validator = match jsonschema::validator_for(&schema) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"execution_config_schema_compile_failed",
error.to_string(),
));
},
};
if let std::result::Result::Err(error) = validator.validate(&instance) {
return std::result::Result::Err(ks_core::Error::new(
"execution_config_schema_validation_failed",
error.to_string(),
));
}
let document = match serde_json::from_str::<ExecutionConfigDocument>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"execution_config_json_decode_failed",
error.to_string(),
));
},
};
return match validate_execution_document(&document) {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Loads environment placeholders and parses one execution configuration file.
pub fn read_execution_json_file_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<ExecutionConfigDocument> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"execution_config_file_read_failed",
error.to_string(),
));
},
};
return parse_execution_json(&crate::resolve_environment_placeholders(&raw_json));
}
/// Returns one named execution profile.
pub fn execution_profile<'a>(
document: &'a ExecutionConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a ExecutionProfileConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
}
}
return std::result::Result::Err(ks_core::Error::new(
"execution_profile_not_found",
profile_name.to_string(),
));
}
/// Returns the default execution profile.
pub fn default_execution_profile(
document: &ExecutionConfigDocument,
) -> ks_core::Result<&ExecutionProfileConfig> {
return execution_profile(document, document.default_profile.as_str());
}
/// Validates one typed execution configuration document.
pub fn validate_execution_document(document: &ExecutionConfigDocument) -> ks_core::Result<()> {
match crate::require_non_empty(&document.default_profile, "execution.default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if document.profiles.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"execution_config_profiles_empty",
"at least one execution profile is required",
));
}
let mut names = std::collections::BTreeSet::<std::string::String>::new();
for profile in &document.profiles {
match crate::require_non_empty(&profile.name, "execution.profile.name") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if !names.insert(profile.name.clone()) {
return std::result::Result::Err(ks_core::Error::new(
"execution_profile_name_duplicate",
profile.name.clone(),
));
}
match crate::validate_execution(&profile.config) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return match default_execution_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
#[cfg(test)]
mod tests {
const DEFAULT_EXECUTION: &str = include_str!("../../config/execution.config.json");
const EXAMPLE_EXECUTION: &str = include_str!("../../config/example.execution.config.json");
#[test]
fn default_and_example_execution_configs_validate() {
assert!(super::parse_execution_json(DEFAULT_EXECUTION).is_ok());
assert!(super::parse_execution_json(EXAMPLE_EXECUTION).is_ok());
}
}

View File

@@ -1,16 +1,19 @@
// file: ks-config/src/lib.rs
// version: 7
// version: 8
//! Khadhroony Solana application configuration contract and loading helpers.
//! Khadhroony Solana shared configuration contracts and composition helpers.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
mod composition;
mod environment;
mod execution;
mod listeners;
mod settings;
mod store;
mod transport;
mod wallet;
/// Exposes the resolved application configuration plus its composition source.
pub use self::composition::ComposedAppConfig;
@@ -38,8 +41,12 @@ pub use self::composition::read_composed_app_config_with_environment;
pub use self::composition::read_composition_json_file;
/// Exposes binary composition loading with workspace environment resolution.
pub use self::composition::read_composition_json_file_with_environment;
/// Exposes canonical shared configuration loading without an application composition file.
pub use self::composition::read_default_shared_app_config_with_environment;
/// Exposes composition source path resolution.
pub use self::composition::resolve_composition_source_path;
/// Exposes composition profile fallback resolution.
pub use self::composition::resolve_profile_selection;
/// Exposes typed binary composition validation.
pub use self::composition::validate_composition_document;
/// Exposes binary composition JSON Schema validation.
@@ -50,12 +57,28 @@ pub use self::environment::EnvironmentLoadReport;
pub use self::environment::load_workspace_environment;
/// Exposes environment placeholder resolution.
pub use self::environment::resolve_environment_placeholders;
/// Exposes the independent execution configuration document.
pub use self::execution::ExecutionConfigDocument;
/// Exposes one named execution profile.
pub use self::execution::ExecutionProfileConfig;
/// Exposes the default execution profile resolver.
pub use self::execution::default_execution_profile;
/// Exposes the execution JSON Schema text.
pub use self::execution::execution_json_schema_text;
/// Exposes one named execution profile resolver.
pub use self::execution::execution_profile;
/// Exposes execution configuration parsing from JSON.
pub use self::execution::parse_execution_json;
/// Exposes execution configuration loading with workspace environment resolution.
pub use self::execution::read_execution_json_file_with_environment;
/// Exposes typed execution document validation.
pub use self::execution::validate_execution_document;
/// Exposes the independent listener configuration document.
pub use self::listeners::ListenersConfigDocument;
/// Exposes one named listener profile.
pub use self::listeners::ListenersProfileConfig;
/// Exposes the active listener profile resolver.
pub use self::listeners::active_listeners_profile;
/// Exposes the default listener profile resolver.
pub use self::listeners::default_listeners_profile;
/// Exposes the listener JSON Schema text.
pub use self::listeners::listeners_json_schema_text;
/// Exposes the listener JSON Schema value parser.
@@ -76,15 +99,13 @@ pub use self::listeners::validate_listeners_document;
pub use self::listeners::validate_listeners_json_schema;
/// Exposes the account listener configuration type.
pub use self::settings::AccountListenerConfig;
/// Exposes the root application configuration type.
/// Exposes the root resolved runtime configuration type.
pub use self::settings::AppConfig;
/// Exposes the application section configuration type.
/// Exposes the application metadata configuration type.
pub use self::settings::AppSectionConfig;
/// Exposes the local data configuration type.
pub use self::settings::DataConfig;
/// Exposes the database configuration type.
pub use self::settings::DatabaseConfig;
/// Exposes the demo application configuration type.
/// Exposes the temporary demo application configuration type.
pub use self::settings::DemoConfig;
/// Exposes the endpoint role configuration type.
pub use self::settings::EndpointRoleConfig;
@@ -118,9 +139,9 @@ pub use self::settings::config_json_schema_text;
pub use self::settings::config_json_schema_value;
/// Exposes resolved runtime configuration parsing from a JSON string.
pub use self::settings::parse_config_json;
/// Exposes resolved runtime configuration loading from a filesystem path.
/// Exposes resolved runtime snapshot loading from a filesystem path.
pub use self::settings::read_config_json_file;
/// Exposes resolved runtime configuration loading with workspace environment resolution.
/// Exposes resolved runtime snapshot loading with workspace environment resolution.
pub use self::settings::read_config_json_file_with_environment;
/// Exposes the compact JSON serializer for resolved runtime configuration values.
pub use self::settings::serialize_config_json;
@@ -130,16 +151,42 @@ pub use self::settings::serialize_config_json_pretty;
pub use self::settings::validate_config;
/// Exposes the JSON Schema validator for raw resolved runtime configuration JSON.
pub use self::settings::validate_config_json_schema;
/// Exposes the independent store configuration document.
pub use self::store::StoreConfigDocument;
/// Exposes one named store profile.
pub use self::store::StoreProfileConfig;
/// Exposes the default store profile resolver.
pub use self::store::default_store_profile;
/// Exposes store configuration parsing from JSON.
pub use self::store::parse_store_json;
/// Exposes store configuration loading with workspace environment resolution.
pub use self::store::read_store_json_file_with_environment;
/// Exposes the store JSON Schema text.
pub use self::store::store_json_schema_text;
/// Exposes the store JSON Schema value parser.
pub use self::store::store_json_schema_value;
/// Exposes one named store profile resolver.
pub use self::store::store_profile;
/// Exposes typed store document validation.
pub use self::store::validate_store_document;
/// Exposes store JSON Schema validation.
pub use self::store::validate_store_json_schema;
/// Exposes the independent transport configuration document.
pub use self::transport::TransportConfigDocument;
/// Exposes one named transport profile.
/// Exposes one fully resolved transport profile.
pub use self::transport::TransportProfileConfig;
/// Exposes the active transport profile resolver.
pub use self::transport::active_transport_profile;
/// Exposes one transport source profile.
pub use self::transport::TransportProfileSourceConfig;
/// Exposes WebSocket endpoint default values.
pub use self::transport::WsEndpointDefaultsConfig;
/// Exposes WebSocket endpoint-specific overrides.
pub use self::transport::WsEndpointOverridesConfig;
/// Exposes one source WebSocket endpoint before defaults are resolved.
pub use self::transport::WsEndpointSourceConfig;
/// Exposes the default transport profile resolver.
pub use self::transport::default_transport_profile;
/// Exposes transport configuration parsing from JSON.
pub use self::transport::parse_transport_json;
/// Exposes transport configuration loading from a filesystem path.
pub use self::transport::read_transport_json_file;
/// Exposes transport configuration loading with workspace environment resolution.
pub use self::transport::read_transport_json_file_with_environment;
/// Exposes the transport JSON Schema text.
@@ -152,12 +199,36 @@ pub use self::transport::transport_profile;
pub use self::transport::validate_transport_document;
/// Exposes transport JSON Schema validation.
pub use self::transport::validate_transport_json_schema;
/// Exposes the independent wallet configuration document.
pub use self::wallet::WalletConfigDocument;
/// Exposes one named wallet source profile.
pub use self::wallet::WalletProfileConfig;
/// Exposes the default wallet source profile resolver.
pub use self::wallet::default_wallet_profile;
/// Exposes wallet configuration parsing from JSON.
pub use self::wallet::parse_wallet_json;
/// Exposes wallet configuration loading with workspace environment resolution.
pub use self::wallet::read_wallet_json_file_with_environment;
/// Exposes one resolved runtime wallet profile.
pub use self::wallet::resolved_wallet_profile;
/// Exposes typed wallet document validation.
pub use self::wallet::validate_wallet_document;
/// Exposes the wallet JSON Schema text.
pub use self::wallet::wallet_json_schema_text;
/// Exposes one named wallet source profile resolver.
pub use self::wallet::wallet_profile;
/// Internal non-empty string validation shared by configuration documents.
/// Keeps `require_non_empty` available to internal configuration modules.
pub(crate) use self::settings::require_non_empty;
/// Internal typed HTTP endpoint validation shared by resolved and transport documents.
/// Keeps `validate_database` available to internal configuration modules.
pub(crate) use self::settings::validate_database;
/// Keeps `validate_execution` available to internal configuration modules.
pub(crate) use self::settings::validate_execution;
/// Keeps `validate_http_endpoint` available to internal configuration modules.
pub(crate) use self::settings::validate_http_endpoint;
/// Internal typed listener validation shared by resolved and listener documents.
/// Keeps `validate_listeners` available to internal configuration modules.
pub(crate) use self::settings::validate_listeners;
/// Internal typed WebSocket endpoint validation shared by resolved and transport documents.
/// Keeps `validate_wallet` available to internal configuration modules.
pub(crate) use self::settings::validate_wallet;
/// Keeps `validate_ws_endpoint` available to internal configuration modules.
pub(crate) use self::settings::validate_ws_endpoint;

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/listeners.rs
// version: 1
// version: 2
//! Independent Solana listener configuration document loading and validation.
@@ -9,8 +9,8 @@ const LISTENERS_JSON_SCHEMA: &str =
/// Root listener document containing independently selectable named profiles.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct ListenersConfigDocument {
/// Active listener profile used by consumers that do not provide an explicit selection.
pub active_profile: std::string::String,
/// Default listener profile used by consumers that do not provide an explicit selection.
pub default_profile: std::string::String,
/// Named listener profiles available in this document.
pub profiles: std::vec::Vec<ListenersProfileConfig>,
}
@@ -157,11 +157,11 @@ pub fn listeners_profile<'a>(
));
}
/// Returns the active listener profile declared by the document.
pub fn active_listeners_profile(
/// Returns the default listener profile declared by the document.
pub fn default_listeners_profile(
document: &ListenersConfigDocument,
) -> ks_core::Result<&ListenersProfileConfig> {
return listeners_profile(document, document.active_profile.as_str());
return listeners_profile(document, document.default_profile.as_str());
}
/// Converts one named listener profile into the resolved runtime listener contract.
@@ -177,7 +177,7 @@ pub fn resolved_listener_config(profile: &ListenersProfileConfig) -> crate::List
/// Validates a typed listener configuration document.
pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_core::Result<()> {
match crate::require_non_empty(&document.active_profile, "active_profile") {
match crate::require_non_empty(&document.default_profile, "default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
@@ -205,7 +205,7 @@ pub fn validate_listeners_document(document: &ListenersConfigDocument) -> ks_cor
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return match active_listeners_profile(document) {
return match default_listeners_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};

View File

@@ -1,5 +1,5 @@
// file: ks-config/src/settings.rs
// version: 21
// version: 22
//! Resolved runtime configuration models retained during the `0.5.1` source-document split.
@@ -31,8 +31,6 @@ pub struct ProfileConfig {
pub app: AppSectionConfig,
/// Database configuration.
pub database: DatabaseConfig,
/// Local data directories.
pub data: DataConfig,
/// Solana endpoint and listener configuration.
pub solana: SolanaConfig,
/// Wallet configuration.
@@ -54,8 +52,6 @@ pub struct AppSectionConfig {
pub name: std::string::String,
/// Environment name.
pub environment: std::string::String,
/// Default auto reconnect flag used when a transport does not override it.
pub auto_reconnect_default: bool,
}
/// Database backend configuration.
@@ -112,16 +108,6 @@ pub struct SqliteConfig {
pub use_wal: bool,
}
/// Local data directory configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/DataConfig.ts")]
pub struct DataConfig {
/// Wallet directory path.
pub wallets_directory: std::string::String,
/// Logs directory path.
pub logs_directory: std::string::String,
}
/// Solana endpoints and listener configuration.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/SolanaConfig.ts")]
@@ -317,14 +303,6 @@ pub struct WalletConfig {
pub temporary_wallet_alias: std::string::String,
/// Persists the managed temporary wallet between process restarts.
pub temporary_wallet_persist: bool,
/// Enables local validator transaction sending.
pub localnet_send_enabled: bool,
/// Enables devnet transaction sending.
pub devnet_send_enabled: bool,
/// Enables testnet transaction sending.
pub testnet_send_enabled: bool,
/// Enables mainnet transaction sending.
pub mainnet_send_enabled: bool,
}
/// Execution safety configuration.
@@ -334,6 +312,14 @@ pub struct WalletConfig {
export_to = "../frontend/ts/bindings/ks_config/settings/ExecutionConfig.ts"
)]
pub struct ExecutionConfig {
/// Enables local validator transaction sending.
pub localnet_send_enabled: bool,
/// Enables devnet transaction sending.
pub devnet_send_enabled: bool,
/// Enables testnet transaction sending.
pub testnet_send_enabled: bool,
/// Enables mainnet transaction sending.
pub mainnet_send_enabled: bool,
/// Enables dry-run for newly prepared execution policies.
pub dry_run_default: bool,
/// Requires simulation before send.
@@ -598,10 +584,6 @@ fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match validate_data(&profile.data) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match validate_solana(&profile.solana) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -625,7 +607,7 @@ fn validate_app_section(config: &AppSectionConfig) -> ks_core::Result<()> {
return require_non_empty(&config.environment, "app.environment");
}
fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
pub(crate) fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
if config.backend != "postgres" && config.backend != "sqlite" {
return std::result::Result::Err(ks_core::Error::new(
"database_backend_invalid",
@@ -651,14 +633,6 @@ fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
return require_non_empty(&config.sqlite.path, "database.sqlite.path");
}
fn validate_data(config: &DataConfig) -> ks_core::Result<()> {
match require_non_empty(&config.wallets_directory, "data.wallets_directory") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
return require_non_empty(&config.logs_directory, "data.logs_directory");
}
fn validate_solana(config: &SolanaConfig) -> ks_core::Result<()> {
if config.http_endpoints.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
@@ -889,7 +863,7 @@ fn validate_account_listener(config: &AccountListenerConfig) -> ks_core::Result<
return require_non_empty(&config.account_pubkey, "account_listener.account_pubkey");
}
fn validate_wallet(config: &WalletConfig) -> ks_core::Result<()> {
pub(crate) fn validate_wallet(config: &WalletConfig) -> ks_core::Result<()> {
match require_non_empty(&config.wallet_dir, "wallet.wallet_dir") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -935,7 +909,7 @@ fn validate_wallet_alias(value: &str) -> ks_core::Result<()> {
return std::result::Result::Ok(());
}
fn validate_execution(config: &ExecutionConfig) -> ks_core::Result<()> {
pub(crate) fn validate_execution(config: &ExecutionConfig) -> ks_core::Result<()> {
let configured_spend = config
.localnet_max_spend_lamports
.saturating_add(config.devnet_max_spend_lamports)
@@ -985,11 +959,15 @@ fn validate_wallet_execution_pair(
execution: &ExecutionConfig,
) -> ks_core::Result<()> {
for (enabled, limit, cluster) in [
(wallet.localnet_send_enabled, execution.localnet_max_spend_lamports, "localnet"),
(wallet.devnet_send_enabled, execution.devnet_max_spend_lamports, "devnet"),
(wallet.testnet_send_enabled, execution.testnet_max_spend_lamports, "testnet"),
(
wallet.mainnet_send_enabled,
execution.localnet_send_enabled,
execution.localnet_max_spend_lamports,
"localnet",
),
(execution.devnet_send_enabled, execution.devnet_max_spend_lamports, "devnet"),
(execution.testnet_send_enabled, execution.testnet_max_spend_lamports, "testnet"),
(
execution.mainnet_send_enabled,
execution.mainnet_max_spend_lamports,
"mainnet-beta",
),
@@ -1001,7 +979,7 @@ fn validate_wallet_execution_pair(
));
}
}
if wallet.mainnet_send_enabled && !execution.require_operator_confirmation {
if execution.mainnet_send_enabled && !execution.require_operator_confirmation {
return std::result::Result::Err(ks_core::Error::new(
"execution_mainnet_send_without_confirmation",
"mainnet sending requires operator confirmation",

186
ks-config/src/store.rs Normal file
View File

@@ -0,0 +1,186 @@
// file: ks-config/src/store.rs
// version: 1
//! Independent store configuration document loading and validation.
const STORE_JSON_SCHEMA: &str = include_str!("../../config/schemas/store.config.schema.json");
/// Root store document with one default profile and named alternatives.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct StoreConfigDocument {
/// Default store profile used when a composition does not override it.
pub default_profile: std::string::String,
/// Named store profiles.
pub profiles: std::vec::Vec<StoreProfileConfig>,
}
/// Named store profile.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct StoreProfileConfig {
/// Profile code.
pub name: std::string::String,
/// Database backend configuration.
pub database: crate::DatabaseConfig,
}
/// Returns the embedded store JSON Schema text.
pub fn store_json_schema_text() -> &'static str {
return STORE_JSON_SCHEMA;
}
/// Parses the embedded store JSON Schema into a JSON value.
pub fn store_json_schema_value() -> ks_core::Result<serde_json::Value> {
let result = serde_json::from_str::<serde_json::Value>(STORE_JSON_SCHEMA);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"store_config_schema_parse_failed",
error.to_string(),
)),
};
}
/// Validates raw store JSON against the embedded schema.
pub fn validate_store_json_schema(raw_json: &str) -> ks_core::Result<()> {
let schema = match store_json_schema_value() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"store_config_json_parse_failed",
error.to_string(),
));
},
};
let validator = match jsonschema::validator_for(&schema) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"store_config_schema_compile_failed",
error.to_string(),
));
},
};
return match validator.validate(&instance) {
std::result::Result::Ok(()) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"store_config_schema_validation_failed",
error.to_string(),
)),
};
}
/// Parses and validates one store configuration document.
pub fn parse_store_json(raw_json: &str) -> ks_core::Result<StoreConfigDocument> {
match validate_store_json_schema(raw_json) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let document = match serde_json::from_str::<StoreConfigDocument>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"store_config_json_decode_failed",
error.to_string(),
));
},
};
return match validate_store_document(&document) {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Loads environment placeholders and parses one store configuration file.
pub fn read_store_json_file_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<StoreConfigDocument> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"store_config_file_read_failed",
error.to_string(),
));
},
};
return parse_store_json(&crate::resolve_environment_placeholders(&raw_json));
}
/// Returns one named store profile.
pub fn store_profile<'a>(
document: &'a StoreConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a StoreProfileConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
}
}
return std::result::Result::Err(ks_core::Error::new(
"store_profile_not_found",
profile_name.to_string(),
));
}
/// Returns the default store profile.
pub fn default_store_profile(
document: &StoreConfigDocument,
) -> ks_core::Result<&StoreProfileConfig> {
return store_profile(document, document.default_profile.as_str());
}
/// Validates one typed store configuration document.
pub fn validate_store_document(document: &StoreConfigDocument) -> ks_core::Result<()> {
match crate::require_non_empty(&document.default_profile, "store.default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if document.profiles.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"store_config_profiles_empty",
"at least one store profile is required",
));
}
let mut names = std::collections::BTreeSet::<std::string::String>::new();
for profile in &document.profiles {
match crate::require_non_empty(&profile.name, "store.profile.name") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if !names.insert(profile.name.clone()) {
return std::result::Result::Err(ks_core::Error::new(
"store_profile_name_duplicate",
profile.name.clone(),
));
}
match crate::validate_database(&profile.database) {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return match default_store_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
#[cfg(test)]
mod tests {
const DEFAULT_STORE: &str = include_str!("../../config/store.config.json");
const EXAMPLE_STORE: &str = include_str!("../../config/example.store.config.json");
#[test]
fn default_and_example_store_configs_validate() {
assert!(super::parse_store_json(DEFAULT_STORE).is_ok());
assert!(super::parse_store_json(EXAMPLE_STORE).is_ok());
}
}

View File

@@ -1,28 +1,99 @@
// file: ks-config/src/transport.rs
// version: 1
// version: 2
//! Independent Solana transport configuration document loading and validation.
const TRANSPORT_JSON_SCHEMA: &str =
include_str!("../../config/schemas/transport.config.schema.json");
/// Root transport document containing independently selectable named profiles.
/// Root transport document containing shared endpoint defaults and named profiles.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct TransportConfigDocument {
/// Active transport profile used by consumers that do not provide an explicit selection.
pub active_profile: std::string::String,
/// Default transport profile used when a composition does not override it.
pub default_profile: std::string::String,
/// Named WebSocket endpoint defaults resolved before endpoint-specific overrides.
pub ws_endpoint_defaults: std::vec::Vec<WsEndpointDefaultsConfig>,
/// Named transport profiles available in this document.
pub profiles: std::vec::Vec<TransportProfileConfig>,
pub profiles: std::vec::Vec<TransportProfileSourceConfig>,
}
/// Named Solana HTTP and WebSocket endpoint profile.
/// Default values shared by one class of WebSocket endpoints.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct WsEndpointDefaultsConfig {
/// Defaults code referenced by endpoints.
pub name: std::string::String,
/// Connection timeout in milliseconds.
pub connect_timeout_ms: u64,
/// Request timeout in milliseconds.
pub request_timeout_ms: u64,
/// Unsubscribe timeout in milliseconds.
pub unsubscribe_timeout_ms: u64,
/// Writer channel capacity.
pub write_channel_capacity: u32,
/// Event channel capacity.
pub event_channel_capacity: u32,
/// Default automatic reconnect policy.
pub auto_reconnect: bool,
}
/// Endpoint-specific overrides applied on top of named WebSocket defaults.
#[derive(Clone, Debug, Default, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct WsEndpointOverridesConfig {
/// Optional connection timeout override.
pub connect_timeout_ms: std::option::Option<u64>,
/// Optional request timeout override.
pub request_timeout_ms: std::option::Option<u64>,
/// Optional unsubscribe timeout override.
pub unsubscribe_timeout_ms: std::option::Option<u64>,
/// Optional writer channel capacity override.
pub write_channel_capacity: std::option::Option<u32>,
/// Optional event channel capacity override.
pub event_channel_capacity: std::option::Option<u32>,
/// Optional automatic reconnect override.
pub auto_reconnect: std::option::Option<bool>,
}
/// Source shape of one WebSocket endpoint before defaults are resolved.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct WsEndpointSourceConfig {
/// Endpoint name.
pub name: std::string::String,
/// Named defaults to resolve before endpoint-specific overrides.
pub defaults: std::string::String,
/// Enables this endpoint.
pub enabled: bool,
/// Provider code.
pub provider: std::string::String,
/// Cluster code.
pub cluster: std::string::String,
/// Full endpoint URL.
pub url: std::string::String,
/// Optional endpoint-specific overrides.
#[serde(default)]
pub overrides: WsEndpointOverridesConfig,
/// Role-specific limits for this URL.
pub roles: std::vec::Vec<crate::EndpointRoleConfig>,
}
/// Named Solana HTTP and WebSocket endpoint source profile.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct TransportProfileSourceConfig {
/// Profile code.
pub name: std::string::String,
/// HTTP JSON-RPC endpoints.
pub http_endpoints: std::vec::Vec<crate::HttpEndpointConfig>,
/// Standard Solana WebSocket endpoints before shared defaults are resolved.
pub ws_endpoints: std::vec::Vec<WsEndpointSourceConfig>,
}
/// Fully resolved transport profile consumed by transport clients.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TransportProfileConfig {
/// Profile code.
pub name: std::string::String,
/// HTTP JSON-RPC endpoints.
pub http_endpoints: std::vec::Vec<crate::HttpEndpointConfig>,
/// Standard Solana WebSocket endpoints.
/// Standard Solana WebSocket endpoints with defaults and overrides resolved.
pub ws_endpoints: std::vec::Vec<crate::WsEndpointConfig>,
}
@@ -33,9 +104,9 @@ pub fn transport_json_schema_text() -> &'static str {
/// Parses the embedded transport JSON Schema into a JSON value.
pub fn transport_json_schema_value() -> ks_core::Result<serde_json::Value> {
let schema_result = serde_json::from_str::<serde_json::Value>(TRANSPORT_JSON_SCHEMA);
return match schema_result {
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
let result = serde_json::from_str::<serde_json::Value>(TRANSPORT_JSON_SCHEMA);
return match result {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
"transport_config_schema_parse_failed",
error.to_string(),
@@ -46,11 +117,11 @@ pub fn transport_json_schema_value() -> ks_core::Result<serde_json::Value> {
/// Validates raw transport JSON against the embedded transport schema.
pub fn validate_transport_json_schema(raw_json: &str) -> ks_core::Result<()> {
let schema = match transport_json_schema_value() {
std::result::Result::Ok(schema) => schema,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
std::result::Result::Ok(instance) => instance,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"transport_config_json_parse_failed",
@@ -59,7 +130,7 @@ pub fn validate_transport_json_schema(raw_json: &str) -> ks_core::Result<()> {
},
};
let validator = match jsonschema::validator_for(&schema) {
std::result::Result::Ok(validator) => validator,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"transport_config_schema_compile_failed",
@@ -83,7 +154,7 @@ pub fn parse_transport_json(raw_json: &str) -> ks_core::Result<TransportConfigDo
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let document = match serde_json::from_str::<TransportConfigDocument>(raw_json) {
std::result::Result::Ok(document) => document,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"transport_config_json_decode_failed",
@@ -97,23 +168,7 @@ pub fn parse_transport_json(raw_json: &str) -> ks_core::Result<TransportConfigDo
};
}
/// Reads and parses one transport configuration document from a filesystem path.
pub fn read_transport_json_file(
path: &std::path::Path,
) -> ks_core::Result<TransportConfigDocument> {
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(content) => content,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"transport_config_file_read_failed",
error.to_string(),
));
},
};
return parse_transport_json(&raw_json);
}
/// Loads workspace environment values, resolves placeholders and parses transport configuration.
/// Loads environment placeholders and parses one transport configuration file.
pub fn read_transport_json_file_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
@@ -123,7 +178,7 @@ pub fn read_transport_json_file_with_environment(
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(content) => content,
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"transport_config_file_read_failed",
@@ -131,15 +186,97 @@ pub fn read_transport_json_file_with_environment(
));
},
};
let resolved = crate::resolve_environment_placeholders(&raw_json);
return parse_transport_json(&resolved);
return parse_transport_json(&crate::resolve_environment_placeholders(&raw_json));
}
/// Returns one named transport profile.
pub fn transport_profile<'a>(
/// Resolves one named transport profile including WebSocket defaults and overrides.
pub fn transport_profile(
document: &TransportConfigDocument,
profile_name: &str,
) -> ks_core::Result<TransportProfileConfig> {
let source = match source_transport_profile(document, profile_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut ws_endpoints = std::vec::Vec::<crate::WsEndpointConfig>::new();
for endpoint in &source.ws_endpoints {
let defaults = match ws_endpoint_defaults(document, endpoint.defaults.as_str()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let resolved = crate::WsEndpointConfig {
name: endpoint.name.clone(),
enabled: endpoint.enabled,
provider: endpoint.provider.clone(),
cluster: endpoint.cluster.clone(),
url: endpoint.url.clone(),
connect_timeout_ms: if let std::option::Option::Some(value) =
endpoint.overrides.connect_timeout_ms
{
value
} else {
defaults.connect_timeout_ms
},
request_timeout_ms: if let std::option::Option::Some(value) =
endpoint.overrides.request_timeout_ms
{
value
} else {
defaults.request_timeout_ms
},
unsubscribe_timeout_ms: if let std::option::Option::Some(value) =
endpoint.overrides.unsubscribe_timeout_ms
{
value
} else {
defaults.unsubscribe_timeout_ms
},
write_channel_capacity: if let std::option::Option::Some(value) =
endpoint.overrides.write_channel_capacity
{
value
} else {
defaults.write_channel_capacity
},
event_channel_capacity: if let std::option::Option::Some(value) =
endpoint.overrides.event_channel_capacity
{
value
} else {
defaults.event_channel_capacity
},
auto_reconnect: if let std::option::Option::Some(value) =
endpoint.overrides.auto_reconnect
{
value
} else {
defaults.auto_reconnect
},
roles: endpoint.roles.clone(),
};
match crate::validate_ws_endpoint(&resolved) {
std::result::Result::Ok(()) => ws_endpoints.push(resolved),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return std::result::Result::Ok(TransportProfileConfig {
name: source.name.clone(),
http_endpoints: source.http_endpoints.clone(),
ws_endpoints,
});
}
/// Returns the default resolved transport profile.
pub fn default_transport_profile(
document: &TransportConfigDocument,
) -> ks_core::Result<TransportProfileConfig> {
return transport_profile(document, document.default_profile.as_str());
}
fn source_transport_profile<'a>(
document: &'a TransportConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a TransportProfileConfig> {
) -> ks_core::Result<&'a TransportProfileSourceConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
@@ -151,19 +288,57 @@ pub fn transport_profile<'a>(
));
}
/// Returns the active transport profile declared by the document.
pub fn active_transport_profile(
document: &TransportConfigDocument,
) -> ks_core::Result<&TransportProfileConfig> {
return transport_profile(document, document.active_profile.as_str());
fn ws_endpoint_defaults<'a>(
document: &'a TransportConfigDocument,
defaults_name: &str,
) -> ks_core::Result<&'a WsEndpointDefaultsConfig> {
for defaults in &document.ws_endpoint_defaults {
if defaults.name == defaults_name {
return std::result::Result::Ok(defaults);
}
}
return std::result::Result::Err(ks_core::Error::new(
"transport_ws_defaults_not_found",
defaults_name.to_string(),
));
}
/// Validates a typed transport configuration document.
pub fn validate_transport_document(document: &TransportConfigDocument) -> ks_core::Result<()> {
match crate::require_non_empty(&document.active_profile, "active_profile") {
match crate::require_non_empty(&document.default_profile, "transport.default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if document.ws_endpoint_defaults.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"transport_ws_defaults_empty",
"at least one WebSocket defaults profile is required",
));
}
let mut defaults_names = std::collections::BTreeSet::<std::string::String>::new();
for defaults in &document.ws_endpoint_defaults {
match crate::require_non_empty(&defaults.name, "transport.ws_endpoint_defaults.name") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if !defaults_names.insert(defaults.name.clone()) {
return std::result::Result::Err(ks_core::Error::new(
"transport_ws_defaults_duplicate",
defaults.name.clone(),
));
}
if defaults.connect_timeout_ms == 0
|| defaults.request_timeout_ms == 0
|| defaults.unsubscribe_timeout_ms == 0
|| defaults.write_channel_capacity == 0
|| defaults.event_channel_capacity == 0
{
return std::result::Result::Err(ks_core::Error::new(
"transport_ws_defaults_limit_zero",
defaults.name.clone(),
));
}
}
if document.profiles.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"transport_config_profiles_empty",
@@ -195,13 +370,21 @@ pub fn validate_transport_document(document: &TransportConfigDocument) -> ks_cor
}
}
for endpoint in &profile.ws_endpoints {
match crate::validate_ws_endpoint(endpoint) {
match crate::require_non_empty(&endpoint.defaults, "transport.ws_endpoint.defaults") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match ws_endpoint_defaults(document, endpoint.defaults.as_str()) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
match transport_profile(document, profile.name.as_str()) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return match active_transport_profile(document) {
return match default_transport_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
@@ -219,22 +402,19 @@ mod tests {
}
#[test]
fn schema_rejects_unsupported_transport_surface() {
let value_result = serde_json::from_str::<serde_json::Value>(DEFAULT_TRANSPORT);
let mut value = match value_result {
fn websocket_defaults_are_resolved_before_endpoint_overrides() {
let document = match super::parse_transport_json(DEFAULT_TRANSPORT) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("default transport must parse: {error}"),
};
let profile = match super::transport_profile(&document, "mainnet_research") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("default transport config must parse: {error}")
panic!("mainnet research transport must resolve: {error}")
},
};
value["profiles"][0]["advanced_streams"] = serde_json::json!([]);
let raw_result = serde_json::to_string(&value);
let raw = match raw_result {
std::result::Result::Ok(raw) => raw,
std::result::Result::Err(error) => {
panic!("transport test value must serialize: {error}")
},
};
assert!(super::validate_transport_json_schema(&raw).is_err());
assert_eq!(profile.ws_endpoints[0].event_channel_capacity, 1024);
assert_eq!(profile.ws_endpoints[1].event_channel_capacity, 4096);
assert!(!profile.ws_endpoints[0].auto_reconnect);
}
}

216
ks-config/src/wallet.rs Normal file
View File

@@ -0,0 +1,216 @@
// file: ks-config/src/wallet.rs
// version: 1
//! Independent wallet configuration document loading and validation.
const WALLET_JSON_SCHEMA: &str = include_str!("../../config/schemas/wallet.config.schema.json");
/// Root wallet document with global storage and named runtime profiles.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct WalletConfigDocument {
/// Root directory for wallet storage, independent from profile selection.
pub wallets_directory: std::string::String,
/// Default wallet profile used when a composition does not override it.
pub default_profile: std::string::String,
/// Named wallet profiles.
pub profiles: std::vec::Vec<WalletProfileConfig>,
}
/// Named wallet profile whose directory is relative to the global wallet root unless absolute.
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
pub struct WalletProfileConfig {
/// Profile code.
pub name: std::string::String,
/// Wallet subdirectory or absolute directory.
pub directory: std::string::String,
/// Selected cluster name.
pub cluster: std::string::String,
/// Enables the managed temporary wallet for this profile.
pub temporary_wallet_enabled: bool,
/// Alias used for the managed temporary wallet.
pub temporary_wallet_alias: std::string::String,
/// Persists the managed temporary wallet between process restarts.
pub temporary_wallet_persist: bool,
}
/// Returns the embedded wallet JSON Schema text.
pub fn wallet_json_schema_text() -> &'static str {
return WALLET_JSON_SCHEMA;
}
/// Parses and validates one wallet configuration document.
pub fn parse_wallet_json(raw_json: &str) -> ks_core::Result<WalletConfigDocument> {
let schema = match serde_json::from_str::<serde_json::Value>(WALLET_JSON_SCHEMA) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_config_schema_parse_failed",
error.to_string(),
));
},
};
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_config_json_parse_failed",
error.to_string(),
));
},
};
let validator = match jsonschema::validator_for(&schema) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_config_schema_compile_failed",
error.to_string(),
));
},
};
if let std::result::Result::Err(error) = validator.validate(&instance) {
return std::result::Result::Err(ks_core::Error::new(
"wallet_config_schema_validation_failed",
error.to_string(),
));
}
let document = match serde_json::from_str::<WalletConfigDocument>(raw_json) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_config_json_decode_failed",
error.to_string(),
));
},
};
return match validate_wallet_document(&document) {
std::result::Result::Ok(()) => std::result::Result::Ok(document),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Loads environment placeholders and parses one wallet configuration file.
pub fn read_wallet_json_file_with_environment(
path: &std::path::Path,
workspace_root: &std::path::Path,
) -> ks_core::Result<WalletConfigDocument> {
match crate::load_workspace_environment(workspace_root) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
let raw_json = match std::fs::read_to_string(path) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(ks_core::Error::new(
"wallet_config_file_read_failed",
error.to_string(),
));
},
};
return parse_wallet_json(&crate::resolve_environment_placeholders(&raw_json));
}
/// Returns one named wallet source profile.
pub fn wallet_profile<'a>(
document: &'a WalletConfigDocument,
profile_name: &str,
) -> ks_core::Result<&'a WalletProfileConfig> {
for profile in &document.profiles {
if profile.name == profile_name {
return std::result::Result::Ok(profile);
}
}
return std::result::Result::Err(ks_core::Error::new(
"wallet_profile_not_found",
profile_name.to_string(),
));
}
/// Returns the default wallet source profile.
pub fn default_wallet_profile(
document: &WalletConfigDocument,
) -> ks_core::Result<&WalletProfileConfig> {
return wallet_profile(document, document.default_profile.as_str());
}
/// Resolves one source wallet profile into the runtime wallet contract.
pub fn resolved_wallet_profile(
document: &WalletConfigDocument,
profile_name: &str,
) -> ks_core::Result<crate::WalletConfig> {
let profile = match wallet_profile(document, profile_name) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let configured = std::path::PathBuf::from(profile.directory.as_str());
let wallet_dir = if configured.is_absolute() {
configured
} else {
std::path::PathBuf::from(document.wallets_directory.as_str()).join(configured)
};
let resolved = crate::WalletConfig {
wallet_dir: wallet_dir.display().to_string(),
cluster: profile.cluster.clone(),
temporary_wallet_enabled: profile.temporary_wallet_enabled,
temporary_wallet_alias: profile.temporary_wallet_alias.clone(),
temporary_wallet_persist: profile.temporary_wallet_persist,
};
return match crate::validate_wallet(&resolved) {
std::result::Result::Ok(()) => std::result::Result::Ok(resolved),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Validates one typed wallet configuration document.
pub fn validate_wallet_document(document: &WalletConfigDocument) -> ks_core::Result<()> {
match crate::require_non_empty(&document.wallets_directory, "wallet.wallets_directory") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match crate::require_non_empty(&document.default_profile, "wallet.default_profile") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if document.profiles.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"wallet_config_profiles_empty",
"at least one wallet profile is required",
));
}
let mut names = std::collections::BTreeSet::<std::string::String>::new();
for profile in &document.profiles {
match crate::require_non_empty(&profile.name, "wallet.profile.name") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
match crate::require_non_empty(&profile.directory, "wallet.profile.directory") {
std::result::Result::Ok(()) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
if !names.insert(profile.name.clone()) {
return std::result::Result::Err(ks_core::Error::new(
"wallet_profile_name_duplicate",
profile.name.clone(),
));
}
match resolved_wallet_profile(document, profile.name.as_str()) {
std::result::Result::Ok(_) => (),
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
}
return match default_wallet_profile(document) {
std::result::Result::Ok(_) => std::result::Result::Ok(()),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
#[cfg(test)]
mod tests {
const DEFAULT_WALLET: &str = include_str!("../../config/wallet.config.json");
const EXAMPLE_WALLET: &str = include_str!("../../config/example.wallet.config.json");
#[test]
fn default_and_example_wallet_configs_validate() {
assert!(super::parse_wallet_json(DEFAULT_WALLET).is_ok());
assert!(super::parse_wallet_json(EXAMPLE_WALLET).is_ok());
}
}