1#![warn(missing_docs)]
2
3use serde::{Deserialize, Serialize};
8use std::{env, error::Error, fs, path::Path};
9use tracing::info;
10
11pub use itools_localization::{t, t_with_args};
13
14pub trait Parse {
16 fn parse(args: &mut Vec<String>) -> Self;
18}
19
20impl Parse for String {
22 fn parse(args: &mut Vec<String>) -> Self {
23 if args.is_empty() {
24 panic!("{}", t("errors.expected_string_argument"));
25 }
26 args.remove(0)
27 }
28}
29
30impl Parse for i32 {
32 fn parse(args: &mut Vec<String>) -> Self {
33 if args.is_empty() {
34 panic!("{}", t("errors.expected_integer_argument"));
35 }
36 args.remove(0).parse().expect(&t("errors.invalid_integer"))
37 }
38}
39
40impl Parse for u32 {
42 fn parse(args: &mut Vec<String>) -> Self {
43 if args.is_empty() {
44 panic!("{}", t("errors.expected_unsigned_integer_argument"));
45 }
46 args.remove(0).parse().expect(&t("errors.invalid_unsigned_integer"))
47 }
48}
49
50impl Parse for bool {
52 fn parse(args: &mut Vec<String>) -> Self {
53 if args.is_empty() {
54 panic!("{}", t("errors.expected_boolean_argument"));
55 }
56 args.remove(0).parse().expect(&t("errors.invalid_boolean"))
57 }
58}
59
60#[derive(Debug, Deserialize, Serialize, Clone)]
62pub struct ConfigFragment {
63 pub data: serde_json::Value,
65 pub priority: u32,
67}
68
69#[derive(Debug)]
71pub struct ConfigManager {
72 fragments: Vec<ConfigFragment>,
74 merged_config: serde_json::Value,
76}
77
78impl ConfigManager {
79 pub fn new() -> Self {
81 Self { fragments: Vec::new(), merged_config: serde_json::Value::Object(serde_json::Map::new()) }
82 }
83
84 pub fn load_fragment(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
86 info!("{}", t_with_args("messages.loading_config_fragment", &[("path", &path.to_string_lossy())]));
87
88 let content = fs::read_to_string(path)
89 .map_err(|_e| format!("{}", t_with_args("errors.config_not_found", &[("path", &path.to_string_lossy())])))?;
90
91 let data = match path.extension().and_then(|ext| ext.to_str()) {
92 Some("json") => serde_json::from_str(&content)?,
93 Some("toml") => {
94 let toml_value: toml::Value = toml::from_str(&content)?;
95 serde_json::to_value(toml_value)?
96 }
97 Some("yaml") | Some("yml") => {
98 let yaml_value: serde_yaml::Value = serde_yaml::from_str(&content)?;
99 serde_json::to_value(yaml_value)?
100 }
101 _ => return Err("Unsupported config file format".into()),
102 };
103
104 self.validate_config(&data)?;
106
107 let fragment = ConfigFragment { data, priority: self.fragments.len() as u32 };
108
109 self.fragments.push(fragment);
110 self.merge_configs();
111
112 Ok(())
113 }
114
115 pub fn validate_config(&self, config: &serde_json::Value) -> Result<(), Box<dyn Error>> {
117 if let serde_json::Value::Object(_map) = config {
122 }
124
125 Ok(())
126 }
127
128 pub fn validate_merged_config(&self) -> Result<(), Box<dyn Error>> {
130 self.validate_config(&self.merged_config)
131 }
132
133 pub fn load_directory(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
135 if !path.exists() || !path.is_dir() {
136 return Ok(());
137 }
138
139 let entries = fs::read_dir(path)?;
140 for entry in entries {
141 let entry = entry?;
142 let entry_path = entry.path();
143
144 if entry_path.is_dir() {
145 self.load_directory(&entry_path)?;
146 }
147 else if let Some(ext) = entry_path.extension() {
148 if ["json", "toml", "yaml", "yml"].contains(&ext.to_str().unwrap_or("")) {
149 self.load_fragment(&entry_path)?;
150 }
151 }
152 }
153
154 Ok(())
155 }
156
157 fn merge_configs(&mut self) {
159 self.fragments.sort_by(|a, b| a.priority.cmp(&b.priority));
161
162 self.merged_config = serde_json::Value::Object(serde_json::Map::new());
164
165 for fragment in &self.fragments {
167 self.merged_config = self.merge_values(self.merged_config.clone(), fragment.data.clone());
168 }
169 }
170
171 fn merge_values(&self, target: serde_json::Value, source: serde_json::Value) -> serde_json::Value {
173 match (target, source) {
174 (serde_json::Value::Object(mut target_map), serde_json::Value::Object(source_map)) => {
175 for (key, source_value) in source_map {
176 if target_map.contains_key(&key) {
177 let target_value = target_map.remove(&key).unwrap();
178 target_map.insert(key, self.merge_values(target_value, source_value));
179 }
180 else {
181 target_map.insert(key, source_value);
182 }
183 }
184 serde_json::Value::Object(target_map)
185 }
186 (_, source) => source, }
188 }
189
190 pub fn get_config(&self) -> &serde_json::Value {
192 &self.merged_config
193 }
194
195 pub fn override_from_args(&mut self, args: &[String]) {
197 for arg in args {
199 if arg.starts_with("--config.") {
200 let parts: Vec<&str> = arg.split('=').collect();
201 if parts.len() == 2 {
202 let key_path = parts[0].trim_start_matches("--config.");
203 let value = parts[1];
204 self.set_config_value(key_path, value);
205 }
206 }
207 }
208 }
209
210 pub fn override_from_env(&mut self) {
212 for (key, value) in env::vars() {
214 if key.starts_with("ITOOLS_CONFIG_") {
215 let key_path = key.trim_start_matches("ITOOLS_CONFIG_").to_lowercase().replace('_', ".");
216 self.set_config_value(&key_path, &value);
217 }
218 }
219 }
220
221 fn set_config_value(&mut self, key_path: &str, value: &str) {
223 let keys: Vec<&str> = key_path.split('.').collect();
224 let mut config = self.merged_config.clone();
225
226 let mut current = &mut config;
228 for (i, key) in keys.iter().enumerate() {
229 if i == keys.len() - 1 {
230 *current = self.parse_value(value);
232 }
233 else {
234 if !current.is_object() {
236 *current = serde_json::Value::Object(serde_json::Map::new());
237 }
238 let current_map = current.as_object_mut().unwrap();
239 if !current_map.contains_key(*key) {
240 current_map.insert(key.to_string(), serde_json::Value::Object(serde_json::Map::new()));
241 }
242 current = current_map.get_mut(*key).unwrap();
243 }
244 }
245
246 self.merged_config = config;
247 }
248
249 fn parse_value(&self, value: &str) -> serde_json::Value {
251 if value == "true" {
253 serde_json::Value::Bool(true)
254 }
255 else if value == "false" {
256 serde_json::Value::Bool(false)
257 }
258 else if let Ok(num) = value.parse::<i64>() {
259 serde_json::Value::Number(serde_json::Number::from(num))
260 }
261 else if let Ok(num) = value.parse::<f64>() {
262 serde_json::Value::Number(serde_json::Number::from_f64(num).unwrap())
263 }
264 else {
265 serde_json::Value::String(value.to_string())
266 }
267 }
268}
269
270#[derive(Debug)]
272pub struct Cli {
273 pub command: Command,
275}
276
277#[derive(Debug)]
279pub enum Command {
280 Init {
282 name: String,
284 },
285 Build,
287 Dev,
289 Check,
291}
292
293impl Parse for Cli {
295 fn parse(args: &mut Vec<String>) -> Self {
296 Self { command: Command::parse(args) }
297 }
298}
299
300impl Parse for Command {
302 fn parse(args: &mut Vec<String>) -> Self {
303 if args.is_empty() {
304 panic!("{}", t("errors.expected_command"));
305 }
306 let cmd = args.remove(0);
307 match cmd.as_str() {
308 "init" => Self::Init { name: String::parse(args) },
309 "build" => Self::Build,
310 "dev" => Self::Dev,
311 "check" => Self::Check,
312 _ => panic!("{}", t_with_args("errors.unknown_command", &[("command", &cmd)])),
313 }
314 }
315}
316
317pub fn init_cli() {
319 tracing_subscriber::fmt().with_env_filter(tracing_subscriber::EnvFilter::from_default_env()).init();
321
322 info!("{}", t("messages.cli_initialized"));
323}
324
325pub mod tui {
327 use std::{
328 io::{self, Write},
329 thread,
330 time::Duration,
331 };
332
333 pub struct ProgressBar {
335 total: u64,
336 current: u64,
337 width: u16,
338 }
339
340 impl ProgressBar {
341 pub fn new(total: u64, width: u16) -> Self {
343 Self { total, current: 0, width }
344 }
345
346 pub fn update(&mut self, current: u64) {
348 self.current = current;
349 self.draw();
350 }
351
352 pub fn inc(&mut self, delta: u64) {
354 self.current += delta;
355 self.draw();
356 }
357
358 pub fn finish(&mut self) {
360 self.current = self.total;
361 self.draw();
362 println!();
363 }
364
365 fn draw(&self) {
367 let percentage = if self.total > 0 { (self.current as f64 / self.total as f64) * 100.0 } else { 0.0 };
368
369 let filled = (percentage / 100.0 * self.width as f64) as u16;
370 let empty = self.width - filled;
371
372 print!("\r[");
373 print!("{}", "=".repeat(filled as usize));
374 print!("{}", " ".repeat(empty as usize));
375 print!("}}] {:.1}%", percentage);
376 io::stdout().flush().unwrap();
377 }
378 }
379
380 pub fn select<T>(items: &[T], prompt: &str) -> Option<usize>
382 where
383 T: std::fmt::Display,
384 {
385 println!("{}", prompt);
386 for (i, item) in items.iter().enumerate() {
387 println!("{}: {}", i + 1, item);
388 }
389
390 loop {
391 print!("请选择 (1-{}): ", items.len());
392 io::stdout().flush().unwrap();
393
394 let mut input = String::new();
395 io::stdin().read_line(&mut input).unwrap();
396
397 match input.trim().parse::<usize>() {
398 Ok(choice) if choice >= 1 && choice <= items.len() => {
399 return Some(choice - 1);
400 }
401 _ => {
402 println!("无效的选择,请重新输入");
403 }
404 }
405 }
406 }
407
408 pub fn loading_animation(duration: Duration, message: &str) {
410 let chars = ["|", "/", "-", "\\"];
411 let start = std::time::Instant::now();
412
413 print!("{}", message);
414 io::stdout().flush().unwrap();
415
416 let mut i = 0;
417 while start.elapsed() < duration {
418 print!("\r{}{}", message, chars[i % chars.len()]);
419 io::stdout().flush().unwrap();
420 thread::sleep(Duration::from_millis(100));
421 i += 1;
422 }
423
424 print!("\r{}{}", message, " ");
425 println!();
426 }
427}
428
429pub fn execute_command(cli: Cli) -> Result<(), Box<dyn Error>> {
431 match cli.command {
432 Command::Init { name } => {
433 info!("{}", t_with_args("messages.initializing_project", &[("name", &name)]));
434 let mut pb = tui::ProgressBar::new(100, 50);
436 for i in 0..=100 {
437 pb.update(i);
438 std::thread::sleep(std::time::Duration::from_millis(50));
439 }
440 pb.finish();
441 Ok(())
442 }
443 Command::Build => {
444 info!("{}", t("messages.building_project"));
445 tui::loading_animation(std::time::Duration::from_secs(3), "构建中...");
447 Ok(())
448 }
449 Command::Dev => {
450 info!("{}", t("messages.starting_dev_server"));
451 let items = ["选项 1", "选项 2", "选项 3"];
453 if let Some(choice) = tui::select(&items, "请选择一个选项:") {
454 info!("选择了: {}", items[choice]);
455 }
456 Ok(())
457 }
458 Command::Check => {
459 info!("{}", t("messages.checking_project"));
460 let mut pb = tui::ProgressBar::new(50, 30);
462 for i in 0..=50 {
463 pb.update(i);
464 std::thread::sleep(std::time::Duration::from_millis(100));
465 }
466 pb.finish();
467 Ok(())
468 }
469 }
470}
471
472pub mod test_macro;
474pub use test_macro::test_macro;