v0.5.1-pre.008
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
// file: ks-config/src/settings.rs
|
||||
// version: 22
|
||||
// version: 23
|
||||
|
||||
//! Resolved runtime configuration models retained during the `0.5.1` source-document split.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
const CONFIG_JSON_SCHEMA: &str =
|
||||
include_str!("../../config/schemas/resolved.app.config.schema.json");
|
||||
|
||||
/// Resolved runtime configuration containing every composed named profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/AppConfig.ts")]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct AppConfig {
|
||||
/// Active profile name.
|
||||
pub active_profile: std::string::String,
|
||||
@@ -19,16 +16,10 @@ pub struct AppConfig {
|
||||
}
|
||||
|
||||
/// Resolved runtime profile selected by the root active profile name.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/ProfileConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct ProfileConfig {
|
||||
/// Profile code.
|
||||
pub name: std::string::String,
|
||||
/// Application metadata.
|
||||
pub app: AppSectionConfig,
|
||||
/// Database configuration.
|
||||
pub database: DatabaseConfig,
|
||||
/// Solana endpoint and listener configuration.
|
||||
@@ -37,29 +28,10 @@ pub struct ProfileConfig {
|
||||
pub wallet: WalletConfig,
|
||||
/// Execution safety configuration.
|
||||
pub execution: ExecutionConfig,
|
||||
/// Demo application configuration.
|
||||
pub demo: DemoConfig,
|
||||
}
|
||||
|
||||
/// Application metadata for a profile.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/AppSectionConfig.ts"
|
||||
)]
|
||||
pub struct AppSectionConfig {
|
||||
/// Application name.
|
||||
pub name: std::string::String,
|
||||
/// Environment name.
|
||||
pub environment: std::string::String,
|
||||
}
|
||||
|
||||
/// Database backend configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/DatabaseConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct DatabaseConfig {
|
||||
/// Enables database storage.
|
||||
pub enabled: bool,
|
||||
@@ -72,33 +44,26 @@ pub struct DatabaseConfig {
|
||||
}
|
||||
|
||||
/// PostgreSQL backend configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/PostgresConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct PostgresConfig {
|
||||
/// PostgreSQL URL or local placeholder.
|
||||
pub url: std::string::String,
|
||||
/// Maximum connection count.
|
||||
pub max_connections: u32,
|
||||
/// Connection timeout in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub connect_timeout_ms: u64,
|
||||
/// Enables schema initialization at startup.
|
||||
pub auto_initialize_schema: bool,
|
||||
}
|
||||
|
||||
/// SQLite backend configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/SqliteConfig.ts")]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct SqliteConfig {
|
||||
/// SQLite database path.
|
||||
pub path: std::string::String,
|
||||
/// Creates the file when missing.
|
||||
pub create_if_missing: bool,
|
||||
/// Busy timeout in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub busy_timeout_ms: u64,
|
||||
/// Maximum connection count.
|
||||
pub max_connections: u32,
|
||||
@@ -109,8 +74,7 @@ pub struct SqliteConfig {
|
||||
}
|
||||
|
||||
/// 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")]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct SolanaConfig {
|
||||
/// HTTP JSON-RPC endpoints.
|
||||
pub http_endpoints: std::vec::Vec<HttpEndpointConfig>,
|
||||
@@ -121,11 +85,7 @@ pub struct SolanaConfig {
|
||||
}
|
||||
|
||||
/// HTTP JSON-RPC endpoint configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/HttpEndpointConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct HttpEndpointConfig {
|
||||
/// Endpoint name.
|
||||
pub name: std::string::String,
|
||||
@@ -138,10 +98,8 @@ pub struct HttpEndpointConfig {
|
||||
/// Full endpoint URL.
|
||||
pub url: std::string::String,
|
||||
/// Connection timeout in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub connect_timeout_ms: u64,
|
||||
/// Request timeout in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub request_timeout_ms: u64,
|
||||
/// Maximum idle connections per host.
|
||||
pub max_idle_connections_per_host: u32,
|
||||
@@ -150,11 +108,7 @@ pub struct HttpEndpointConfig {
|
||||
}
|
||||
|
||||
/// Standard Solana WebSocket endpoint configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/WsEndpointConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct WsEndpointConfig {
|
||||
/// Endpoint name.
|
||||
pub name: std::string::String,
|
||||
@@ -167,13 +121,10 @@ pub struct WsEndpointConfig {
|
||||
/// Full endpoint URL.
|
||||
pub url: std::string::String,
|
||||
/// Connection timeout in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub connect_timeout_ms: u64,
|
||||
/// Request timeout in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub request_timeout_ms: u64,
|
||||
/// Unsubscribe timeout in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub unsubscribe_timeout_ms: u64,
|
||||
/// Writer channel capacity.
|
||||
pub write_channel_capacity: u32,
|
||||
@@ -186,11 +137,7 @@ pub struct WsEndpointConfig {
|
||||
}
|
||||
|
||||
/// Role-specific endpoint limits.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/EndpointRoleConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct EndpointRoleConfig {
|
||||
/// Role code used by endpoint pools.
|
||||
pub role: std::string::String,
|
||||
@@ -209,16 +156,11 @@ pub struct EndpointRoleConfig {
|
||||
/// Maximum subscriptions allowed for this role on this URL.
|
||||
pub max_subscriptions: u32,
|
||||
/// Pause after a rate limit response in milliseconds.
|
||||
#[ts(type = "number")]
|
||||
pub pause_after_rate_limit_ms: u64,
|
||||
}
|
||||
|
||||
/// Listener configuration used by standard WebSocket subscriptions.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/ListenerConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct ListenerConfig {
|
||||
/// Enables listener creation.
|
||||
pub enabled: bool,
|
||||
@@ -233,11 +175,7 @@ pub struct ListenerConfig {
|
||||
}
|
||||
|
||||
/// Log listener filtered by a program identifier mention.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/LogListenerConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct LogListenerConfig {
|
||||
/// Listener name.
|
||||
pub name: std::string::String,
|
||||
@@ -252,11 +190,7 @@ pub struct LogListenerConfig {
|
||||
}
|
||||
|
||||
/// Program account listener.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/ProgramListenerConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct ProgramListenerConfig {
|
||||
/// Listener name.
|
||||
pub name: std::string::String,
|
||||
@@ -271,11 +205,7 @@ pub struct ProgramListenerConfig {
|
||||
}
|
||||
|
||||
/// Account listener.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/AccountListenerConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct AccountListenerConfig {
|
||||
/// Listener name.
|
||||
pub name: std::string::String,
|
||||
@@ -290,8 +220,7 @@ pub struct AccountListenerConfig {
|
||||
}
|
||||
|
||||
/// Wallet configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/WalletConfig.ts")]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct WalletConfig {
|
||||
/// Wallet directory path.
|
||||
pub wallet_dir: std::string::String,
|
||||
@@ -306,11 +235,7 @@ pub struct WalletConfig {
|
||||
}
|
||||
|
||||
/// Execution safety configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/ks_config/settings/ExecutionConfig.ts"
|
||||
)]
|
||||
#[derive(Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
pub struct ExecutionConfig {
|
||||
/// Enables local validator transaction sending.
|
||||
pub localnet_send_enabled: bool,
|
||||
@@ -327,48 +252,29 @@ pub struct ExecutionConfig {
|
||||
/// Requires explicit operator confirmation before send.
|
||||
pub require_operator_confirmation: bool,
|
||||
/// Maximum spend in lamports for local validator tests.
|
||||
#[ts(type = "number")]
|
||||
pub localnet_max_spend_lamports: u64,
|
||||
/// Maximum spend in lamports for devnet tests.
|
||||
#[ts(type = "number")]
|
||||
pub devnet_max_spend_lamports: u64,
|
||||
/// Maximum spend in lamports for testnet tests.
|
||||
#[ts(type = "number")]
|
||||
pub testnet_max_spend_lamports: u64,
|
||||
/// Maximum spend in lamports for mainnet operations.
|
||||
#[ts(type = "number")]
|
||||
pub mainnet_max_spend_lamports: u64,
|
||||
/// Maximum estimated transaction fee in lamports.
|
||||
#[ts(type = "number")]
|
||||
pub max_fee_lamports: u64,
|
||||
/// Maximum compute-unit price in micro-lamports.
|
||||
#[ts(type = "number")]
|
||||
pub max_compute_unit_price_micro_lamports: u64,
|
||||
/// Maximum accepted age for a recent blockhash in slots.
|
||||
#[ts(type = "number")]
|
||||
pub recent_blockhash_max_age_slots: u64,
|
||||
/// Maximum node retransmission retries requested by `sendTransaction`.
|
||||
pub send_max_retries: u32,
|
||||
/// Delay between transaction confirmation polls.
|
||||
#[ts(type = "number")]
|
||||
pub confirmation_poll_interval_ms: u64,
|
||||
/// Maximum number of transaction confirmation polls.
|
||||
pub confirmation_max_attempts: u32,
|
||||
/// Maximum devnet faucet airdrop allowed for a temporary wallet.
|
||||
#[ts(type = "number")]
|
||||
pub devnet_airdrop_max_lamports: u64,
|
||||
}
|
||||
|
||||
/// Demo application configuration.
|
||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||
#[ts(export, export_to = "../frontend/ts/bindings/ks_config/settings/DemoConfig.ts")]
|
||||
pub struct DemoConfig {
|
||||
/// Enables live demo pages.
|
||||
pub live_demo_enabled: bool,
|
||||
/// Enables trading demo pages.
|
||||
pub trading_demo_enabled: bool,
|
||||
}
|
||||
|
||||
/// Returns the embedded JSON Schema text used for resolved runtime contract validation.
|
||||
pub fn config_json_schema_text() -> &'static str {
|
||||
return CONFIG_JSON_SCHEMA;
|
||||
@@ -413,9 +319,9 @@ pub fn validate_config_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||
let validation_result = validator.validate(&instance);
|
||||
return match validation_result {
|
||||
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
std::result::Result::Err(_) => std::result::Result::Err(ks_core::Error::new(
|
||||
"config_json_schema_validation_failed",
|
||||
error.to_string(),
|
||||
"resolved configuration does not satisfy its schema",
|
||||
)),
|
||||
};
|
||||
}
|
||||
@@ -428,10 +334,10 @@ pub fn parse_config_json(raw_json: &str) -> ks_core::Result<AppConfig> {
|
||||
}
|
||||
let config = match serde_json::from_str::<AppConfig>(raw_json) {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => {
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"config_json_decode_failed",
|
||||
error.to_string(),
|
||||
"resolved configuration could not be decoded",
|
||||
));
|
||||
},
|
||||
};
|
||||
@@ -477,36 +383,6 @@ pub fn read_config_json_file_with_environment(
|
||||
return parse_config_json(&resolved);
|
||||
}
|
||||
|
||||
/// Serializes a configuration value to compact JSON.
|
||||
pub fn serialize_config_json(config: &AppConfig) -> ks_core::Result<std::string::String> {
|
||||
match validate_config(config) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
return match serde_json::to_string(config) {
|
||||
std::result::Result::Ok(serialized) => std::result::Result::Ok(serialized),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"config_json_serialize_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Serializes a configuration value to pretty JSON.
|
||||
pub fn serialize_config_json_pretty(config: &AppConfig) -> ks_core::Result<std::string::String> {
|
||||
match validate_config(config) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
return match serde_json::to_string_pretty(config) {
|
||||
std::result::Result::Ok(serialized) => std::result::Result::Ok(serialized),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||
"config_json_pretty_serialize_failed",
|
||||
error.to_string(),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the active profile declared by the root configuration.
|
||||
pub fn active_profile(config: &AppConfig) -> ks_core::Result<&ProfileConfig> {
|
||||
for profile in &config.profiles {
|
||||
@@ -576,10 +452,6 @@ fn validate_root_config(config: &AppConfig) -> ks_core::Result<()> {
|
||||
}
|
||||
|
||||
fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> {
|
||||
match validate_app_section(&profile.app) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
match validate_database(&profile.database) {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
@@ -599,14 +471,6 @@ fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> {
|
||||
return validate_wallet_execution_pair(&profile.wallet, &profile.execution);
|
||||
}
|
||||
|
||||
fn validate_app_section(config: &AppSectionConfig) -> ks_core::Result<()> {
|
||||
match require_non_empty(&config.name, "app.name") {
|
||||
std::result::Result::Ok(()) => (),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
return require_non_empty(&config.environment, "app.environment");
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -683,7 +547,7 @@ pub(crate) fn validate_http_endpoint(config: &HttpEndpointConfig) -> ks_core::Re
|
||||
if !config.url.starts_with("http://") && !config.url.starts_with("https://") {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"http_endpoint_url_invalid",
|
||||
config.url.clone(),
|
||||
config.name.clone(),
|
||||
));
|
||||
}
|
||||
if config.connect_timeout_ms == 0
|
||||
@@ -714,7 +578,7 @@ pub(crate) fn validate_ws_endpoint(config: &WsEndpointConfig) -> ks_core::Result
|
||||
if !config.url.starts_with("ws://") && !config.url.starts_with("wss://") {
|
||||
return std::result::Result::Err(ks_core::Error::new(
|
||||
"ws_endpoint_url_invalid",
|
||||
config.url.clone(),
|
||||
config.name.clone(),
|
||||
));
|
||||
}
|
||||
if config.connect_timeout_ms == 0
|
||||
@@ -1006,24 +870,6 @@ pub(crate) fn require_non_empty(value: &str, field_name: &str) -> ks_core::Resul
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
#[test]
|
||||
fn exported_json_configuration_numbers_do_not_use_bigint() {
|
||||
let config = ts_rs::Config::default();
|
||||
let declarations = [
|
||||
<crate::PostgresConfig as TS>::decl(&config),
|
||||
<crate::SqliteConfig as TS>::decl(&config),
|
||||
<crate::HttpEndpointConfig as TS>::decl(&config),
|
||||
<crate::WsEndpointConfig as TS>::decl(&config),
|
||||
<crate::EndpointRoleConfig as TS>::decl(&config),
|
||||
<crate::ExecutionConfig as TS>::decl(&config),
|
||||
];
|
||||
for declaration in declarations {
|
||||
assert!(!declaration.contains("bigint"));
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: &str =
|
||||
include_str!("../../test-fixtures/config/resolved.app.config.json");
|
||||
const EXAMPLE_CONFIG: &str =
|
||||
@@ -1077,29 +923,6 @@ mod tests {
|
||||
//assert_eq!(active.name, "local_devnet");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_app_config_serializes_and_roundtrips() {
|
||||
let config_result = super::parse_config_json(DEFAULT_CONFIG);
|
||||
assert!(config_result.is_ok());
|
||||
let config = match config_result {
|
||||
std::result::Result::Ok(config) => config,
|
||||
std::result::Result::Err(error) => panic!("default app config must parse: {error}"),
|
||||
};
|
||||
let serialized_result = super::serialize_config_json_pretty(&config);
|
||||
assert!(serialized_result.is_ok());
|
||||
let serialized = match serialized_result {
|
||||
std::result::Result::Ok(serialized) => serialized,
|
||||
std::result::Result::Err(error) => panic!("default app config must serialize: {error}"),
|
||||
};
|
||||
let reparsed_result = super::parse_config_json(&serialized);
|
||||
assert!(reparsed_result.is_ok());
|
||||
let reparsed = match reparsed_result {
|
||||
std::result::Result::Ok(reparsed) => reparsed,
|
||||
std::result::Result::Err(error) => panic!("serialized config must parse: {error}"),
|
||||
};
|
||||
assert_eq!(config, reparsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parser_rejects_missing_active_profile() {
|
||||
let mut value = parse_default_value();
|
||||
|
||||
Reference in New Issue
Block a user