datasynth_core/country/
mod.rs1pub mod easter;
9pub mod error;
10pub mod lunar;
11pub mod merge;
12pub mod schema;
13
14pub use error::CountryPackError;
15pub use schema::CountryPack;
16
17use std::collections::HashMap;
18use std::fmt;
19use std::path::Path;
20
21use merge::{apply_override, deep_merge};
22
23const DEFAULT_PACK_JSON: &str = include_str!("../../country-packs/_default.json");
28const US_PACK_JSON: &str = include_str!("../../country-packs/US.json");
29const DE_PACK_JSON: &str = include_str!("../../country-packs/DE.json");
30const GB_PACK_JSON: &str = include_str!("../../country-packs/GB.json");
31
32#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub struct CountryCode(String);
39
40impl CountryCode {
41 pub fn new(code: &str) -> Result<Self, CountryPackError> {
44 let code = code.trim().to_uppercase();
45 if code == "_DEFAULT" {
46 return Ok(Self(code));
47 }
48 if code.len() == 2 && code.chars().all(|c| c.is_ascii_uppercase()) {
49 Ok(Self(code))
50 } else {
51 Err(CountryPackError::InvalidCountryCode(code))
52 }
53 }
54
55 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58}
59
60impl fmt::Display for CountryCode {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str(&self.0)
63 }
64}
65
66pub struct CountryPackRegistry {
73 default_pack: CountryPack,
74 packs: HashMap<CountryCode, CountryPack>,
75}
76
77impl CountryPackRegistry {
78 pub fn new(
81 external_dir: Option<&Path>,
82 overrides: &HashMap<String, serde_json::Value>,
83 ) -> Result<Self, CountryPackError> {
84 let mut registry = Self::builtin_only()?;
85
86 if let Some(dir) = external_dir {
88 registry.load_external_dir(dir)?;
89 }
90
91 for (code_str, value) in overrides {
93 let code = CountryCode::new(code_str)?;
94 if let Some(pack) = registry.packs.get_mut(&code) {
95 apply_override(pack, value)?;
96 } else {
97 let mut pack = registry.default_pack.clone();
99 pack.country_code = code_str.to_uppercase();
100 apply_override(&mut pack, value)?;
101 registry.packs.insert(code, pack);
102 }
103 }
104
105 Ok(registry)
106 }
107
108 pub fn builtin_only() -> Result<Self, CountryPackError> {
110 let default_pack: CountryPack = serde_json::from_str(DEFAULT_PACK_JSON)
111 .map_err(|e| CountryPackError::parse(format!("_default.json: {e}")))?;
112
113 let mut packs = HashMap::new();
114
115 for (json, label) in [
116 (US_PACK_JSON, "US.json"),
117 (DE_PACK_JSON, "DE.json"),
118 (GB_PACK_JSON, "GB.json"),
119 ] {
120 let pack = Self::parse_and_merge(&default_pack, json, label)?;
121 let code = CountryCode::new(&pack.country_code)?;
122 packs.insert(code, pack);
123 }
124
125 Ok(Self {
126 default_pack,
127 packs,
128 })
129 }
130
131 pub fn get(&self, code: &CountryCode) -> &CountryPack {
134 self.packs.get(code).unwrap_or(&self.default_pack)
135 }
136
137 pub fn get_by_str(&self, code: &str) -> &CountryPack {
140 match CountryCode::new(code) {
141 Ok(cc) => self.get(&cc),
142 Err(_) => &self.default_pack,
143 }
144 }
145
146 pub fn available_countries(&self) -> Vec<&CountryCode> {
148 self.packs.keys().collect()
149 }
150
151 pub fn default_pack(&self) -> &CountryPack {
153 &self.default_pack
154 }
155
156 fn parse_and_merge(
160 default: &CountryPack,
161 json: &str,
162 label: &str,
163 ) -> Result<CountryPack, CountryPackError> {
164 let country_value: serde_json::Value = serde_json::from_str(json)
165 .map_err(|e| CountryPackError::parse(format!("{label}: {e}")))?;
166
167 let mut base_value = serde_json::to_value(default)
168 .map_err(|e| CountryPackError::parse(format!("serialize default: {e}")))?;
169
170 deep_merge(&mut base_value, &country_value);
171
172 serde_json::from_value(base_value)
173 .map_err(|e| CountryPackError::parse(format!("{label} merge: {e}")))
174 }
175
176 fn load_external_dir(&mut self, dir: &Path) -> Result<(), CountryPackError> {
179 let entries = std::fs::read_dir(dir)
180 .map_err(|e| CountryPackError::directory(format!("{}: {e}", dir.display())))?;
181
182 for entry in entries {
183 let entry = entry
184 .map_err(|e| CountryPackError::directory(format!("{}: {e}", dir.display())))?;
185 let path = entry.path();
186
187 if path.extension().and_then(|e| e.to_str()) != Some("json") {
188 continue;
189 }
190
191 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
193 if stem == "_default" {
194 continue;
195 }
196 }
197
198 let json = std::fs::read_to_string(&path)
199 .map_err(|e| CountryPackError::directory(format!("{}: {e}", path.display())))?;
200
201 let label = path
202 .file_name()
203 .and_then(|n| n.to_str())
204 .unwrap_or("unknown");
205
206 let pack = Self::parse_and_merge(&self.default_pack, &json, label)?;
207 let code = CountryCode::new(&pack.country_code)?;
208 self.packs.insert(code, pack);
209 }
210
211 Ok(())
212 }
213}
214
215impl fmt::Debug for CountryPackRegistry {
216 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
217 f.debug_struct("CountryPackRegistry")
218 .field("default", &self.default_pack.country_code)
219 .field(
220 "countries",
221 &self.packs.keys().map(|c| c.as_str()).collect::<Vec<_>>(),
222 )
223 .finish()
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_country_code_valid() {
233 assert!(CountryCode::new("US").is_ok());
234 assert!(CountryCode::new("de").is_ok()); assert!(CountryCode::new("GB").is_ok());
236 assert!(CountryCode::new("_DEFAULT").is_ok());
237 }
238
239 #[test]
240 fn test_country_code_invalid() {
241 assert!(CountryCode::new("").is_err());
242 assert!(CountryCode::new("USA").is_err());
243 assert!(CountryCode::new("1A").is_err());
244 assert!(CountryCode::new("A").is_err());
245 }
246
247 #[test]
248 fn test_builtin_only() {
249 let reg = CountryPackRegistry::builtin_only().expect("should load");
250 assert!(reg.available_countries().len() >= 3);
251
252 let us = reg.get_by_str("US");
253 assert_eq!(us.country_code, "US");
254 assert!(!us.holidays.fixed.is_empty());
255
256 let de = reg.get_by_str("DE");
257 assert_eq!(de.country_code, "DE");
258
259 let gb = reg.get_by_str("GB");
260 assert_eq!(gb.country_code, "GB");
261 }
262
263 #[test]
264 fn test_fallback_to_default() {
265 let reg = CountryPackRegistry::builtin_only().expect("should load");
266 let unknown = reg.get_by_str("ZZ");
267 assert_eq!(unknown.country_code, "_DEFAULT");
268 }
269
270 #[test]
271 fn test_default_pack_parses() {
272 let pack: CountryPack =
273 serde_json::from_str(DEFAULT_PACK_JSON).expect("default pack should parse");
274 assert_eq!(pack.country_code, "_DEFAULT");
275 assert_eq!(pack.schema_version, "1.0");
276 }
277
278 #[test]
279 fn test_registry_with_overrides() {
280 let mut overrides = HashMap::new();
281 overrides.insert(
282 "US".to_string(),
283 serde_json::json!({"country_name": "USA Override"}),
284 );
285 let reg = CountryPackRegistry::new(None, &overrides).expect("should load");
286 let us = reg.get_by_str("US");
287 assert_eq!(us.country_name, "USA Override");
288 }
289}