Skip to content

Commit bc6b55e

Browse files
authored
Merge pull request #542 from usmanimamu17-create/fix/issue-506-config-validator
feat(devkit): Implement config validator and related features
2 parents b1f513b + 87e89fb commit bc6b55e

6 files changed

Lines changed: 723 additions & 0 deletions

File tree

packages/devkit/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ clap_complete = "4"
1212
rand = { version = "0.8", features = ["small_rng"] }
1313
serde = { version = "1", features = ["derive"] }
1414
serde_json = "1"
15+
toml = "0.8"
1516
thiserror = "1"
1617
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
1718

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Stellar Fee Tracker Devkit Configuration
2+
# ==========================================
3+
#
4+
# Copy this file to devkit.toml and customize as needed.
5+
# Environment variables override these values (prefix: DEVKIT_).
6+
7+
# Database configuration
8+
db_path = "stellar_fees.db"
9+
10+
# Scenario: normal, congested, spike, calm, stress
11+
scenario = "normal"
12+
13+
# Mock server port
14+
port = 8090
15+
16+
# Verbose output
17+
verbose = false
18+
19+
# Horizon API base URL
20+
horizon_url = "https://horizon-testnet.stellar.org"
21+
22+
# Polling interval in seconds (minimum recommended: 5s)
23+
poll_interval_secs = 10
24+
25+
# Retry configuration
26+
retry_attempts = 3
27+
base_retry_delay_ms = 1000
28+
29+
# Simulation settings
30+
simulation_duration = 1000
31+
simulation_base_fee = 100
32+
simulation_spike_prob = 0.05
33+
34+
# Sandbox settings
35+
sandbox_time_offset_secs = 0
36+
37+
# Analysis settings
38+
analysis_window_hours = 24

packages/devkit/src/config/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
mod types;
2+
mod validator;
3+
4+
pub use types::DevkitConfig;
5+
pub use validator::{ConfigValidator, ValidationResult, ValidationIssue, ValidationSeverity};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use std::collections::BTreeMap;
2+
use std::path::PathBuf;
3+
4+
use serde::Deserialize;
5+
6+
/// Serializable TOML representation of DevkitConfig.
7+
#[derive(Debug, Deserialize, Default)]
8+
struct DevkitConfigToml {
9+
db_path: Option<String>,
10+
scenario: Option<String>,
11+
port: Option<u16>,
12+
verbose: Option<bool>,
13+
horizon_url: Option<String>,
14+
poll_interval_secs: Option<u64>,
15+
retry_attempts: Option<u32>,
16+
base_retry_delay_ms: Option<u64>,
17+
simulation_duration: Option<u64>,
18+
simulation_base_fee: Option<u64>,
19+
simulation_spike_prob: Option<f64>,
20+
sandbox_time_offset_secs: Option<i64>,
21+
analysis_window_hours: Option<u32>,
22+
}
23+
24+
/// Master configuration struct for the Stellar fee tracker devkit.
25+
///
26+
/// Aggregates all configuration options across CLI, simulation, sandbox,
27+
/// and analysis modules. Can be loaded from a TOML file, environment
28+
/// variables, or constructed with defaults.
29+
#[derive(Debug, Clone)]
30+
pub struct DevkitConfig {
31+
/// Path to the fee database (SQLite).
32+
pub db_path: PathBuf,
33+
/// Default scenario file for mock data.
34+
pub scenario: String,
35+
/// Mock server port.
36+
pub port: u16,
37+
/// Whether to show detailed output.
38+
pub verbose: bool,
39+
/// Horizon API base URL.
40+
pub horizon_url: String,
41+
/// Polling interval in seconds for fee data collection.
42+
pub poll_interval_secs: u64,
43+
/// Maximum number of retry attempts for failed requests.
44+
pub retry_attempts: u32,
45+
/// Base delay in milliseconds between retries.
46+
pub base_retry_delay_ms: u64,
47+
/// Number of ledgers to simulate.
48+
pub simulation_duration: u64,
49+
/// Base fee floor in stroops for simulation.
50+
pub simulation_base_fee: u64,
51+
/// Probability of a fee spike on any given ledger [0.0, 1.0].
52+
pub simulation_spike_prob: f64,
53+
/// Sandbox time travel offset in seconds.
54+
pub sandbox_time_offset_secs: i64,
55+
/// Analysis window size in hours.
56+
pub analysis_window_hours: u32,
57+
/// Custom key-value overrides.
58+
pub overrides: BTreeMap<String, String>,
59+
}
60+
61+
impl Default for DevkitConfig {
62+
fn default() -> Self {
63+
Self {
64+
db_path: PathBuf::from("stellar_fees.db"),
65+
scenario: String::from("normal"),
66+
port: 8090,
67+
verbose: false,
68+
horizon_url: String::from("https://horizon-testnet.stellar.org"),
69+
poll_interval_secs: 10,
70+
retry_attempts: 3,
71+
base_retry_delay_ms: 1000,
72+
simulation_duration: 1000,
73+
simulation_base_fee: 100,
74+
simulation_spike_prob: 0.05,
75+
sandbox_time_offset_secs: 0,
76+
analysis_window_hours: 24,
77+
overrides: BTreeMap::new(),
78+
}
79+
}
80+
}
81+
82+
impl DevkitConfig {
83+
/// Load configuration from a TOML file.
84+
pub fn from_toml_file(path: &PathBuf) -> Result<Self, String> {
85+
let content =
86+
std::fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?;
87+
let toml_cfg: DevkitConfigToml =
88+
toml::from_str(&content).map_err(|e| format!("Failed to parse config file: {}", e))?;
89+
90+
let mut cfg = Self::default();
91+
if let Some(v) = toml_cfg.db_path {
92+
cfg.db_path = PathBuf::from(v);
93+
}
94+
if let Some(v) = toml_cfg.scenario {
95+
cfg.scenario = v;
96+
}
97+
if let Some(v) = toml_cfg.port {
98+
cfg.port = v;
99+
}
100+
if let Some(v) = toml_cfg.verbose {
101+
cfg.verbose = v;
102+
}
103+
if let Some(v) = toml_cfg.horizon_url {
104+
cfg.horizon_url = v;
105+
}
106+
if let Some(v) = toml_cfg.poll_interval_secs {
107+
cfg.poll_interval_secs = v;
108+
}
109+
if let Some(v) = toml_cfg.retry_attempts {
110+
cfg.retry_attempts = v;
111+
}
112+
if let Some(v) = toml_cfg.base_retry_delay_ms {
113+
cfg.base_retry_delay_ms = v;
114+
}
115+
if let Some(v) = toml_cfg.simulation_duration {
116+
cfg.simulation_duration = v;
117+
}
118+
if let Some(v) = toml_cfg.simulation_base_fee {
119+
cfg.simulation_base_fee = v;
120+
}
121+
if let Some(v) = toml_cfg.simulation_spike_prob {
122+
cfg.simulation_spike_prob = v;
123+
}
124+
if let Some(v) = toml_cfg.sandbox_time_offset_secs {
125+
cfg.sandbox_time_offset_secs = v;
126+
}
127+
if let Some(v) = toml_cfg.analysis_window_hours {
128+
cfg.analysis_window_hours = v;
129+
}
130+
131+
Ok(cfg)
132+
}
133+
134+
/// Load configuration from environment variables.
135+
pub fn from_env() -> Self {
136+
let mut cfg = Self::default();
137+
cfg.apply_env();
138+
cfg
139+
}
140+
141+
/// Apply environment variable overrides on top of the current config.
142+
pub fn apply_env(&mut self) {
143+
if let Ok(v) = std::env::var("DEVKIT_DB_PATH") {
144+
self.db_path = PathBuf::from(v);
145+
}
146+
if let Ok(v) = std::env::var("DEVKIT_SCENARIO") {
147+
self.scenario = v;
148+
}
149+
if let Ok(v) = std::env::var("DEVKIT_PORT") {
150+
self.port = v.parse().unwrap_or(self.port);
151+
}
152+
if let Ok(v) = std::env::var("DEVKIT_VERBOSE") {
153+
self.verbose = v == "true" || v == "1";
154+
}
155+
if let Ok(v) = std::env::var("DEVKIT_HORIZON_URL") {
156+
self.horizon_url = v;
157+
}
158+
if let Ok(v) = std::env::var("DEVKIT_POLL_INTERVAL_SECS") {
159+
self.poll_interval_secs = v.parse().unwrap_or(self.poll_interval_secs);
160+
}
161+
if let Ok(v) = std::env::var("DEVKIT_RETRY_ATTEMPTS") {
162+
self.retry_attempts = v.parse().unwrap_or(self.retry_attempts);
163+
}
164+
if let Ok(v) = std::env::var("DEVKIT_BASE_RETRY_DELAY_MS") {
165+
self.base_retry_delay_ms = v.parse().unwrap_or(self.base_retry_delay_ms);
166+
}
167+
if let Ok(v) = std::env::var("DEVKIT_SIMULATION_DURATION") {
168+
self.simulation_duration = v.parse().unwrap_or(self.simulation_duration);
169+
}
170+
if let Ok(v) = std::env::var("DEVKIT_SIMULATION_BASE_FEE") {
171+
self.simulation_base_fee = v.parse().unwrap_or(self.simulation_base_fee);
172+
}
173+
if let Ok(v) = std::env::var("DEVKIT_SIMULATION_SPIKE_PROB") {
174+
self.simulation_spike_prob = v.parse().unwrap_or(self.simulation_spike_prob);
175+
}
176+
if let Ok(v) = std::env::var("DEVKIT_SANDBOX_TIME_OFFSET_SECS") {
177+
self.sandbox_time_offset_secs = v.parse().unwrap_or(self.sandbox_time_offset_secs);
178+
}
179+
if let Ok(v) = std::env::var("DEVKIT_ANALYSIS_WINDOW_HOURS") {
180+
self.analysis_window_hours = v.parse().unwrap_or(self.analysis_window_hours);
181+
}
182+
}
183+
184+
/// Display the full configuration as a formatted key/value report.
185+
pub fn display(&self) -> String {
186+
let mut out = String::new();
187+
out.push_str("devkit configuration\n");
188+
out.push_str("====================\n");
189+
190+
let fields = [
191+
("db_path", &self.db_path.display().to_string()),
192+
("scenario", &self.scenario),
193+
("port", &self.port.to_string()),
194+
("verbose", &self.verbose.to_string()),
195+
("horizon_url", &self.horizon_url),
196+
("poll_interval_secs", &self.poll_interval_secs.to_string()),
197+
("retry_attempts", &self.retry_attempts.to_string()),
198+
("base_retry_delay_ms", &self.base_retry_delay_ms.to_string()),
199+
("simulation_duration", &self.simulation_duration.to_string()),
200+
("simulation_base_fee", &self.simulation_base_fee.to_string()),
201+
("simulation_spike_prob", &self.simulation_spike_prob.to_string()),
202+
(
203+
"sandbox_time_offset_secs",
204+
&self.sandbox_time_offset_secs.to_string(),
205+
),
206+
(
207+
"analysis_window_hours",
208+
&self.analysis_window_hours.to_string(),
209+
),
210+
];
211+
212+
out.push_str(&format!(
213+
"{:<12} {:<30} {}\n",
214+
"key", "value", "source"
215+
));
216+
for (key, value) in &fields {
217+
let source = if std::env::var(format!("DEVKIT_{}", key.to_uppercase())).is_ok() {
218+
"env"
219+
} else {
220+
"default"
221+
};
222+
out.push_str(&format!("{:<12} {:<30} {}\n", key, value, source));
223+
}
224+
225+
if !self.overrides.is_empty() {
226+
out.push_str("overrides:\n");
227+
for (k, v) in &self.overrides {
228+
out.push_str(&format!(" {} = {}\n", k, v));
229+
}
230+
}
231+
out
232+
}
233+
}

0 commit comments

Comments
 (0)