Skip to content

Commit 7f8d2ad

Browse files
committed
feat(config): support config file reading and global access
1 parent 1c2c14c commit 7f8d2ad

5 files changed

Lines changed: 270 additions & 0 deletions

File tree

src/config/error.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use crate::i18n::t;
2+
3+
#[derive(Debug)]
4+
pub enum ConfigError {
5+
FileNotFound(String),
6+
ParseError(String),
7+
InvalidConfig(String),
8+
}
9+
10+
impl std::error::Error for ConfigError {}
11+
12+
impl std::fmt::Display for ConfigError {
13+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
14+
match self {
15+
ConfigError::FileNotFound(msg) => {
16+
write!(f, "{}", t("config_file_not_found").replace("{}", msg))
17+
}
18+
ConfigError::ParseError(msg) => {
19+
write!(f, "{}", t("failed_parse_config").replace("{}", msg))
20+
}
21+
ConfigError::InvalidConfig(msg) => {
22+
write!(f, "{}", t("config_invalid").replace("{}", msg))
23+
}
24+
}
25+
}
26+
}

src/config/mod.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
mod error;
2+
mod schema;
3+
mod store;
4+
mod types;
5+
6+
use error::ConfigError;
7+
use schema::ConfigFile;
8+
use std::path::Path;
9+
pub use store::{init, is_chinese, print_config};
10+
pub use types::{Config, Language};
11+
12+
const CONFIG_FILE_PATH: &str = "config.json";
13+
14+
/// Initialize configuration from file or use default settings
15+
pub fn init_config() -> Result<Config, ConfigError> {
16+
if !Path::new(CONFIG_FILE_PATH).exists() {
17+
return Err(ConfigError::FileNotFound(
18+
"config.json not found".to_string(),
19+
));
20+
}
21+
22+
ConfigFile::from_file(CONFIG_FILE_PATH)
23+
.map_err(|e| ConfigError::ParseError(e.to_string()))
24+
.and_then(|file_config| {
25+
let config = Config::new_from_file(&file_config)?;
26+
init(config.clone());
27+
Ok(config)
28+
})
29+
}

src/config/schema.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use super::error::ConfigError;
2+
use serde::Deserialize;
3+
use std::{collections::HashMap, fs};
4+
5+
// The schema of the config file
6+
#[derive(Debug, Deserialize)]
7+
pub struct ConfigFile {
8+
/// Language setting: 'en' or 'zh'
9+
pub lang: String,
10+
11+
/// List of author names
12+
/// Vec<String> is equivalent to string[] in TypeScript
13+
pub authors: Vec<String>,
14+
15+
/// Date range: [start_date, end_date]
16+
/// Vec<String> here represents a fixed-length array of 2 strings
17+
#[serde(rename = "dateRange")]
18+
pub date_range: Vec<String>,
19+
20+
/// List of Git repository paths
21+
pub repos: Vec<String>,
22+
23+
/// Repository name formatting map
24+
/// HashMap<K,V> is equivalent to Record<K,V> in TypeScript
25+
/// or { [key: string]: string }
26+
pub format: HashMap<String, String>,
27+
28+
/// List of commit types to include
29+
pub includes: Vec<String>,
30+
31+
/// List of keywords to exclude
32+
pub excludes: Vec<String>,
33+
}
34+
35+
impl ConfigFile {
36+
pub fn from_file(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
37+
// Reading file contents
38+
let content = fs::read_to_string(path)?;
39+
40+
// Parsing JSON
41+
let config: ConfigFile = serde_json::from_str(&content)?;
42+
43+
// Validate the config
44+
config.validate()?;
45+
46+
Ok(config)
47+
}
48+
49+
fn validate(&self) -> Result<(), ConfigError> {
50+
if self.date_range.len() != 2 {
51+
return Err(ConfigError::InvalidConfig(
52+
"date_range must contain exactly 2 dates".to_string(),
53+
));
54+
}
55+
if self.authors.is_empty() {
56+
return Err(ConfigError::InvalidConfig(
57+
"authors list cannot be empty".to_string(),
58+
));
59+
}
60+
if self.repos.is_empty() {
61+
return Err(ConfigError::InvalidConfig(
62+
"repos list cannot be empty".to_string(),
63+
));
64+
}
65+
Ok(())
66+
}
67+
}

src/config/store.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use super::error::ConfigError;
2+
use super::types::Config;
3+
use crate::i18n::t;
4+
use std::sync::OnceLock;
5+
6+
// Global Configuration Instance
7+
static GLOBAL_CONFIG: OnceLock<Config> = OnceLock::new();
8+
9+
/// Initialize the global configuration
10+
pub fn init(config: Config) {
11+
let _ = GLOBAL_CONFIG.set(config);
12+
}
13+
14+
/// Get the global configuration instance
15+
/// Returns an error if the configuration is not initialized
16+
pub fn global() -> Result<&'static Config, ConfigError> {
17+
GLOBAL_CONFIG
18+
.get()
19+
.ok_or_else(|| ConfigError::InvalidConfig(t("global_config_not_initialized").to_string()))
20+
}
21+
22+
/// Check if the current language is Chinese
23+
pub fn is_chinese() -> bool {
24+
global()
25+
.map(|config| matches!(config.language, super::types::Language::Chinese))
26+
.unwrap_or(false)
27+
}
28+
29+
pub fn print_config() {
30+
match global() {
31+
Ok(config) => {
32+
println!();
33+
println!("{}", config);
34+
println!();
35+
}
36+
Err(e) => {
37+
eprintln!("{}", t("failed_print_config").replace("{}", &e.to_string()));
38+
}
39+
}
40+
}

src/config/types.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use super::{error::ConfigError, schema::ConfigFile};
2+
use crate::i18n::t;
3+
use std::fmt;
4+
5+
/// Language options for the application
6+
#[derive(Debug, Clone)]
7+
#[allow(dead_code)]
8+
pub enum Language {
9+
English,
10+
Chinese,
11+
}
12+
13+
// Implement the Display trait
14+
// e.g. `println!("{}", Language::English);` will print `en`
15+
impl fmt::Display for Language {
16+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17+
match self {
18+
Language::English => write!(f, "en"),
19+
Language::Chinese => write!(f, "zh"),
20+
}
21+
}
22+
}
23+
24+
/// Parsing from string to enumeration implementation
25+
impl Language {
26+
pub fn from_str(s: &str) -> Option<Self> {
27+
match s.to_lowercase().as_str() {
28+
"en" => Some(Language::English),
29+
"zh" => Some(Language::Chinese),
30+
_ => None,
31+
}
32+
}
33+
}
34+
35+
/// Global configuration structure
36+
#[derive(Debug, Clone)]
37+
pub struct Config {
38+
pub language: Language,
39+
pub authors: Vec<String>,
40+
pub date_range: Vec<String>,
41+
pub repos: Vec<String>,
42+
pub format: std::collections::HashMap<String, String>,
43+
pub includes: Vec<String>,
44+
pub excludes: Vec<String>,
45+
}
46+
47+
impl Config {
48+
/// Create a new configuration instance with default values
49+
pub fn new_from_file(file_config: &ConfigFile) -> Result<Self, ConfigError> {
50+
let language = Language::from_str(&file_config.lang)
51+
.ok_or_else(|| ConfigError::InvalidConfig("Invalid language setting".to_string()))?;
52+
53+
Ok(Self {
54+
language,
55+
authors: file_config.authors.clone(),
56+
date_range: file_config.date_range.clone(),
57+
repos: file_config.repos.clone(),
58+
format: file_config.format.clone(),
59+
includes: file_config.includes.clone(),
60+
excludes: file_config.excludes.clone(),
61+
})
62+
}
63+
}
64+
65+
impl fmt::Display for Config {
66+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67+
writeln!(f, "")?;
68+
writeln!(f, "{}", t("config_details"))?;
69+
writeln!(f, "")?;
70+
writeln!(
71+
f,
72+
"{}: {}",
73+
t("language"),
74+
if matches!(self.language, Language::Chinese) {
75+
"Chinese"
76+
} else {
77+
"English"
78+
}
79+
)?;
80+
writeln!(f, "{}: {:?}", t("authors"), self.authors)?;
81+
writeln!(
82+
f,
83+
"{}: {} - {}",
84+
t("date_range"),
85+
self.date_range.get(0).unwrap_or(&"N/A".to_string()),
86+
self.date_range.get(1).unwrap_or(&"N/A".to_string())
87+
)?;
88+
writeln!(f, "{}:", t("repos"))?;
89+
for repo in &self.repos {
90+
if let Some(display_name) = self.format.get(repo) {
91+
writeln!(f, " - {} ({})", display_name, repo)?;
92+
} else {
93+
writeln!(f, " - {}", repo)?;
94+
}
95+
}
96+
writeln!(f, "{}:", t("includes"))?;
97+
for inc in &self.includes {
98+
writeln!(f, " - {}", inc)?;
99+
}
100+
writeln!(f, "{}:", t("excludes"))?;
101+
for exc in &self.excludes {
102+
writeln!(f, " - {}", exc)?;
103+
}
104+
writeln!(f, "")?;
105+
writeln!(f, "--------------------------------")?;
106+
Ok(())
107+
}
108+
}

0 commit comments

Comments
 (0)