1pub 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");
31const FR_PACK_JSON: &str = include_str!("../../country-packs/FR.json");
32const JP_PACK_JSON: &str = include_str!("../../country-packs/JP.json");
33const CN_PACK_JSON: &str = include_str!("../../country-packs/CN.json");
34const IN_PACK_JSON: &str = include_str!("../../country-packs/IN.json");
35const IT_PACK_JSON: &str = include_str!("../../country-packs/IT.json");
36const ES_PACK_JSON: &str = include_str!("../../country-packs/ES.json");
37const CA_PACK_JSON: &str = include_str!("../../country-packs/CA.json");
38
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct CountryCode(String);
46
47impl CountryCode {
48 pub fn new(code: &str) -> Result<Self, CountryPackError> {
51 let code = code.trim().to_uppercase();
52 if code == "_DEFAULT" {
53 return Ok(Self(code));
54 }
55 if code.len() == 2 && code.chars().all(|c| c.is_ascii_uppercase()) {
56 Ok(Self(code))
57 } else {
58 Err(CountryPackError::InvalidCountryCode(code))
59 }
60 }
61
62 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65}
66
67impl fmt::Display for CountryCode {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 f.write_str(&self.0)
70 }
71}
72
73pub struct CountryPackRegistry {
80 default_pack: CountryPack,
81 packs: HashMap<CountryCode, CountryPack>,
82}
83
84impl CountryPackRegistry {
85 pub fn new(
88 external_dir: Option<&Path>,
89 overrides: &HashMap<String, serde_json::Value>,
90 ) -> Result<Self, CountryPackError> {
91 let mut registry = Self::builtin_only()?;
92
93 if let Some(dir) = external_dir {
95 registry.load_external_dir(dir)?;
96 }
97
98 for (code_str, value) in overrides {
100 let code = CountryCode::new(code_str)?;
101 if let Some(pack) = registry.packs.get_mut(&code) {
102 apply_override(pack, value)?;
103 } else {
104 let mut pack = registry.default_pack.clone();
106 pack.country_code = code_str.to_uppercase();
107 apply_override(&mut pack, value)?;
108 registry.packs.insert(code, pack);
109 }
110 }
111
112 Ok(registry)
113 }
114
115 pub fn builtin_only() -> Result<Self, CountryPackError> {
117 let default_pack: CountryPack = serde_json::from_str(DEFAULT_PACK_JSON)
118 .map_err(|e| CountryPackError::parse(format!("_default.json: {e}")))?;
119
120 let mut packs = HashMap::new();
121
122 for (json, label) in [
123 (US_PACK_JSON, "US.json"),
124 (DE_PACK_JSON, "DE.json"),
125 (GB_PACK_JSON, "GB.json"),
126 (FR_PACK_JSON, "FR.json"),
127 (JP_PACK_JSON, "JP.json"),
128 (CN_PACK_JSON, "CN.json"),
129 (IN_PACK_JSON, "IN.json"),
130 (IT_PACK_JSON, "IT.json"),
131 (ES_PACK_JSON, "ES.json"),
132 (CA_PACK_JSON, "CA.json"),
133 ] {
134 let pack = Self::parse_and_merge(&default_pack, json, label)?;
135 let code = CountryCode::new(&pack.country_code)?;
136 packs.insert(code, pack);
137 }
138
139 Ok(Self {
140 default_pack,
141 packs,
142 })
143 }
144
145 pub fn get(&self, code: &CountryCode) -> &CountryPack {
148 self.packs.get(code).unwrap_or(&self.default_pack)
149 }
150
151 pub fn get_by_str(&self, code: &str) -> &CountryPack {
154 match CountryCode::new(code) {
155 Ok(cc) => self.get(&cc),
156 Err(_) => &self.default_pack,
157 }
158 }
159
160 pub fn available_countries(&self) -> Vec<&CountryCode> {
162 self.packs.keys().collect()
163 }
164
165 pub fn default_pack(&self) -> &CountryPack {
167 &self.default_pack
168 }
169
170 fn parse_and_merge(
174 default: &CountryPack,
175 json: &str,
176 label: &str,
177 ) -> Result<CountryPack, CountryPackError> {
178 let country_value: serde_json::Value = serde_json::from_str(json)
179 .map_err(|e| CountryPackError::parse(format!("{label}: {e}")))?;
180
181 let mut base_value = serde_json::to_value(default)
182 .map_err(|e| CountryPackError::parse(format!("serialize default: {e}")))?;
183
184 deep_merge(&mut base_value, &country_value);
185
186 serde_json::from_value(base_value)
187 .map_err(|e| CountryPackError::parse(format!("{label} merge: {e}")))
188 }
189
190 fn load_external_dir(&mut self, dir: &Path) -> Result<(), CountryPackError> {
193 let entries = std::fs::read_dir(dir)
194 .map_err(|e| CountryPackError::directory(format!("{}: {e}", dir.display())))?;
195
196 for entry in entries {
197 let entry = entry
198 .map_err(|e| CountryPackError::directory(format!("{}: {e}", dir.display())))?;
199 let path = entry.path();
200
201 if path.extension().and_then(|e| e.to_str()) != Some("json") {
202 continue;
203 }
204
205 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
207 if stem == "_default" {
208 continue;
209 }
210 }
211
212 let json = std::fs::read_to_string(&path)
213 .map_err(|e| CountryPackError::directory(format!("{}: {e}", path.display())))?;
214
215 let label = path
216 .file_name()
217 .and_then(|n| n.to_str())
218 .unwrap_or("unknown");
219
220 let pack = Self::parse_and_merge(&self.default_pack, &json, label)?;
221 let code = CountryCode::new(&pack.country_code)?;
222 self.packs.insert(code, pack);
223 }
224
225 Ok(())
226 }
227}
228
229impl fmt::Debug for CountryPackRegistry {
230 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231 f.debug_struct("CountryPackRegistry")
232 .field("default", &self.default_pack.country_code)
233 .field(
234 "countries",
235 &self
236 .packs
237 .keys()
238 .map(CountryCode::as_str)
239 .collect::<Vec<_>>(),
240 )
241 .finish()
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_country_code_valid() {
251 assert!(CountryCode::new("US").is_ok());
252 assert!(CountryCode::new("de").is_ok()); assert!(CountryCode::new("GB").is_ok());
254 assert!(CountryCode::new("_DEFAULT").is_ok());
255 }
256
257 #[test]
258 fn test_country_code_invalid() {
259 assert!(CountryCode::new("").is_err());
260 assert!(CountryCode::new("USA").is_err());
261 assert!(CountryCode::new("1A").is_err());
262 assert!(CountryCode::new("A").is_err());
263 }
264
265 #[test]
266 fn test_builtin_only() {
267 let reg = CountryPackRegistry::builtin_only().expect("should load");
268 assert!(reg.available_countries().len() >= 3);
269
270 let us = reg.get_by_str("US");
271 assert_eq!(us.country_code, "US");
272 assert!(!us.holidays.fixed.is_empty());
273
274 let de = reg.get_by_str("DE");
275 assert_eq!(de.country_code, "DE");
276
277 let gb = reg.get_by_str("GB");
278 assert_eq!(gb.country_code, "GB");
279 }
280
281 #[test]
282 fn test_all_ten_packs_load() {
283 let reg = CountryPackRegistry::builtin_only().expect("should load");
284 assert_eq!(reg.available_countries().len(), 10);
285
286 for code in ["US", "DE", "GB", "FR", "JP", "CN", "IN", "IT", "ES", "CA"] {
287 let pack = reg.get_by_str(code);
288 assert_eq!(pack.country_code, code, "country_code mismatch for {code}");
289 assert!(
290 !pack.holidays.fixed.is_empty(),
291 "{code} has no fixed holidays"
292 );
293 assert!(
294 !pack.names.cultures.is_empty(),
295 "{code} has no name cultures"
296 );
297 }
298 }
299
300 #[test]
301 fn test_fr_pack_loads() {
302 let reg = CountryPackRegistry::builtin_only().expect("should load");
303 let fr = reg.get_by_str("FR");
304 assert_eq!(fr.country_code, "FR");
305 assert_eq!(fr.locale.default_currency, "EUR");
306 assert_eq!(fr.locale.default_timezone, "Europe/Paris");
307 assert!(fr.tax.vat.standard_rate > 0.19);
308 }
309
310 #[test]
311 fn test_jp_pack_loads() {
312 let reg = CountryPackRegistry::builtin_only().expect("should load");
313 let jp = reg.get_by_str("JP");
314 assert_eq!(jp.country_code, "JP");
315 assert_eq!(jp.locale.default_currency, "JPY");
316 assert_eq!(jp.locale.currency_decimal_places, 0);
317 assert_eq!(jp.locale.fiscal_year.start_month, 4);
318 }
319
320 #[test]
321 fn test_cn_pack_loads() {
322 let reg = CountryPackRegistry::builtin_only().expect("should load");
323 let cn = reg.get_by_str("CN");
324 assert_eq!(cn.country_code, "CN");
325 assert_eq!(cn.locale.default_currency, "CNY");
326 assert_eq!(cn.locale.default_timezone, "Asia/Shanghai");
327 }
328
329 #[test]
330 fn test_in_pack_loads() {
331 let reg = CountryPackRegistry::builtin_only().expect("should load");
332 let india = reg.get_by_str("IN");
333 assert_eq!(india.country_code, "IN");
334 assert_eq!(india.locale.default_currency, "INR");
335 assert_eq!(india.locale.number_format.grouping, vec![3, 2]);
336 assert_eq!(india.locale.fiscal_year.start_month, 4);
337 }
338
339 #[test]
340 fn test_it_pack_loads() {
341 let reg = CountryPackRegistry::builtin_only().expect("should load");
342 let it = reg.get_by_str("IT");
343 assert_eq!(it.country_code, "IT");
344 assert_eq!(it.locale.default_currency, "EUR");
345 assert!(it.payroll.thirteenth_month);
346 }
347
348 #[test]
349 fn test_es_pack_loads() {
350 let reg = CountryPackRegistry::builtin_only().expect("should load");
351 let es = reg.get_by_str("ES");
352 assert_eq!(es.country_code, "ES");
353 assert_eq!(es.locale.default_currency, "EUR");
354 assert_eq!(es.locale.default_timezone, "Europe/Madrid");
355 }
356
357 #[test]
358 fn test_ca_pack_loads() {
359 let reg = CountryPackRegistry::builtin_only().expect("should load");
360 let ca = reg.get_by_str("CA");
361 assert_eq!(ca.country_code, "CA");
362 assert_eq!(ca.locale.default_currency, "CAD");
363 assert!(!ca.tax.payroll_tax.income_tax_brackets.is_empty());
364 }
365
366 #[test]
367 fn test_fallback_to_default() {
368 let reg = CountryPackRegistry::builtin_only().expect("should load");
369 let unknown = reg.get_by_str("ZZ");
370 assert_eq!(unknown.country_code, "_DEFAULT");
371 }
372
373 #[test]
374 fn test_default_pack_parses() {
375 let pack: CountryPack =
376 serde_json::from_str(DEFAULT_PACK_JSON).expect("default pack should parse");
377 assert_eq!(pack.country_code, "_DEFAULT");
378 assert_eq!(pack.schema_version, "1.0");
379 }
380
381 #[test]
382 fn test_registry_with_overrides() {
383 let mut overrides = HashMap::new();
384 overrides.insert(
385 "US".to_string(),
386 serde_json::json!({"country_name": "USA Override"}),
387 );
388 let reg = CountryPackRegistry::new(None, &overrides).expect("should load");
389 let us = reg.get_by_str("US");
390 assert_eq!(us.country_name, "USA Override");
391 }
392}