1use serde::Deserialize;
2use std::collections::HashMap;
3use std::sync::OnceLock;
4
5const CANCHI_JSON: &str = include_str!("../data/canchi.json");
6const TIET_KHI_JSON: &str = include_str!("../data/tiet-khi.json");
7const TRUC_INSIGHT_JSON: &str = include_str!("../data/truc-insight.json");
8const DAY_DEITY_INSIGHT_JSON: &str = include_str!("../data/day-deity-insight.json");
9const NA_AM_INSIGHT_JSON: &str = include_str!("../data/na-am-insight.json");
10const TEN_GODS_INSIGHT_JSON: &str = include_str!("../data/ten-gods-insight.json");
11const TU_MENH_INSIGHT_JSON: &str = include_str!("../data/tu-menh-insight.json");
12const DAI_VAN_INSIGHT_JSON: &str = include_str!("../data/dai-van-insight.json");
13
14#[derive(Debug, Deserialize, Clone)]
15pub struct BilingualText {
16 pub vi: String,
17 pub en: String,
18}
19
20#[derive(Debug, Deserialize, Clone)]
21pub struct BilingualList {
22 pub vi: Vec<String>,
23 pub en: Vec<String>,
24}
25
26#[derive(Debug, Deserialize, Clone)]
27pub struct CanInfo {
28 pub name: String,
29 pub element: String,
30 pub meaning: BilingualText,
31 pub nature: BilingualText,
32}
33
34#[derive(Debug, Deserialize, Clone)]
35pub struct ChiInfo {
36 pub name: String,
37 pub animal: BilingualText,
38 pub element: String,
39 pub meaning: BilingualText,
40 pub hours: String,
41}
42
43#[derive(Debug, Deserialize, Clone)]
44pub struct ElementInfo {
45 pub name: BilingualText,
46 pub nature: BilingualText,
47}
48
49#[derive(Debug, Deserialize, Clone)]
50#[serde(rename_all = "camelCase")]
51pub struct DayGuidance {
52 pub good_for: BilingualList,
53 pub avoid_for: BilingualList,
54}
55
56#[derive(Debug, Deserialize)]
57#[serde(rename_all = "camelCase")]
58struct CanChiFile {
59 can: Vec<CanInfo>,
60 chi: Vec<ChiInfo>,
61 elements: HashMap<String, ElementInfo>,
62 day_guidance: HashMap<String, DayGuidance>,
63}
64
65#[derive(Debug, Deserialize, Clone)]
66pub struct TietKhiInsight {
67 pub id: String,
68 pub name: BilingualText,
69 pub longitude: i32,
70 pub meaning: BilingualText,
71 pub astronomy: BilingualText,
72 pub agriculture: BilingualList,
73 pub health: BilingualList,
74 pub weather: BilingualText,
75}
76
77#[derive(Debug, Deserialize)]
78#[serde(rename_all = "camelCase")]
79struct TietKhiFile {
80 tiet_khi: Vec<TietKhiInsight>,
81}
82
83#[derive(Debug, Clone, Deserialize)]
84pub struct TrucInsight {
85 pub id: String,
86 pub meaning: BilingualText,
87 pub good_for: BilingualList,
88 pub avoid_for: BilingualList,
89}
90
91#[derive(Debug, Deserialize)]
92struct TrucInsightFile {
93 truc: Vec<TrucInsight>,
94}
95
96#[derive(Debug, Clone, Deserialize)]
97pub struct DeityClassificationInsight {
98 pub id: String,
99 pub name: BilingualText,
100 pub meaning: BilingualText,
101}
102
103#[derive(Debug, Clone, Deserialize)]
104pub struct DeityInsight {
105 pub name: String,
106 pub classification: String,
107 pub meaning: BilingualText,
108}
109
110#[derive(Debug, Deserialize)]
111struct DayDeityInsightFile {
112 classifications: Vec<DeityClassificationInsight>,
113 deities: Vec<DeityInsight>,
114}
115
116#[derive(Debug, Clone, Deserialize)]
117pub struct NaAmInsight {
118 pub na_am: String,
119 pub element: String,
120 pub meaning: BilingualText,
121}
122
123#[derive(Debug, Deserialize)]
124struct NaAmInsightFile {
125 pairs: Vec<NaAmInsight>,
126}
127
128#[derive(Debug, Clone, Deserialize)]
129pub struct TenGodsInsight {
130 pub id: String,
131 pub name: BilingualText,
132 pub meaning: BilingualText,
133}
134
135#[derive(Debug, Deserialize)]
136struct TenGodsInsightFile {
137 gods: Vec<TenGodsInsight>,
138}
139
140#[derive(Debug, Clone, Deserialize)]
141pub struct KuaGroupInsight {
142 pub id: String,
143 pub name: BilingualText,
144 pub meaning: BilingualText,
145}
146
147#[derive(Debug, Clone, Deserialize)]
148pub struct KuaInsight {
149 pub number: u8,
150 pub trigram: BilingualText,
151 pub direction: BilingualText,
152 pub meaning: BilingualText,
153}
154
155#[derive(Debug, Deserialize)]
156struct TuMenhInsightFile {
157 groups: Vec<KuaGroupInsight>,
158 kua: Vec<KuaInsight>,
159}
160
161#[derive(Debug, Clone, Deserialize)]
162pub struct DaiVanDirectionInsight {
163 pub id: String,
164 pub name: BilingualText,
165 pub meaning: BilingualText,
166}
167
168#[derive(Debug, Clone, Deserialize)]
169pub struct DaiVanPhasesInsight {
170 pub meaning: BilingualText,
171}
172
173#[derive(Debug, Clone, Deserialize)]
174pub struct DaiVanElementInsight {
175 pub element: String,
176 pub meaning: BilingualText,
177}
178
179#[derive(Debug, Deserialize)]
180struct DaiVanInsightFile {
181 directions: Vec<DaiVanDirectionInsight>,
182 phases: DaiVanPhasesInsight,
183 elements: Vec<DaiVanElementInsight>,
184}
185
186static CANCHI_DATA: OnceLock<CanChiFile> = OnceLock::new();
187static TIET_KHI_DATA: OnceLock<TietKhiFile> = OnceLock::new();
188static TRUC_INSIGHT_DATA: OnceLock<Vec<TrucInsight>> = OnceLock::new();
189static DAY_DEITY_INSIGHT_DATA: OnceLock<DayDeityInsightFile> = OnceLock::new();
190static NA_AM_INSIGHT_DATA: OnceLock<Vec<NaAmInsight>> = OnceLock::new();
191static TEN_GODS_INSIGHT_DATA: OnceLock<Vec<TenGodsInsight>> = OnceLock::new();
192static TU_MENH_INSIGHT_DATA: OnceLock<TuMenhInsightFile> = OnceLock::new();
193static DAI_VAN_INSIGHT_DATA: OnceLock<DaiVanInsightFile> = OnceLock::new();
194
195fn canchi_data() -> &'static CanChiFile {
196 CANCHI_DATA.get_or_init(|| {
197 serde_json::from_str(CANCHI_JSON).expect("Failed to parse data/canchi.json")
198 })
199}
200
201fn tiet_khi_data() -> &'static TietKhiFile {
202 TIET_KHI_DATA.get_or_init(|| {
203 serde_json::from_str(TIET_KHI_JSON).expect("Failed to parse data/tiet-khi.json")
204 })
205}
206
207pub fn all_can() -> &'static [CanInfo] {
208 &canchi_data().can
209}
210
211pub fn all_chi() -> &'static [ChiInfo] {
212 &canchi_data().chi
213}
214
215pub fn all_elements() -> &'static HashMap<String, ElementInfo> {
216 &canchi_data().elements
217}
218
219pub fn all_day_guidance() -> &'static HashMap<String, DayGuidance> {
220 &canchi_data().day_guidance
221}
222
223pub fn all_tiet_khi_insights() -> &'static [TietKhiInsight] {
224 &tiet_khi_data().tiet_khi
225}
226
227pub fn find_can(name: &str) -> Option<&'static CanInfo> {
228 all_can().iter().find(|item| item.name == name)
229}
230
231pub fn find_chi(name: &str) -> Option<&'static ChiInfo> {
232 all_chi().iter().find(|item| item.name == name)
233}
234
235pub fn get_day_guidance(chi_name: &str) -> Option<&'static DayGuidance> {
236 all_day_guidance().get(chi_name)
237}
238
239pub fn find_tiet_khi_insight(term_name: &str) -> Option<&'static TietKhiInsight> {
240 all_tiet_khi_insights()
241 .iter()
242 .find(|item| item.name.vi == term_name || item.name.en == term_name)
243}
244
245pub fn all_truc_insights() -> &'static [TrucInsight] {
246 TRUC_INSIGHT_DATA
247 .get_or_init(|| {
248 let parsed: TrucInsightFile = serde_json::from_str(TRUC_INSIGHT_JSON)
249 .expect("Failed to parse data/truc-insight.json");
250 parsed.truc
251 })
252 .as_slice()
253}
254
255pub fn find_truc_insight(name: &str) -> Option<&'static TrucInsight> {
256 all_truc_insights().iter().find(|t| t.id == name)
257}
258
259fn day_deity_insight_data() -> &'static DayDeityInsightFile {
260 DAY_DEITY_INSIGHT_DATA.get_or_init(|| {
261 serde_json::from_str(DAY_DEITY_INSIGHT_JSON)
262 .expect("Failed to parse data/day-deity-insight.json")
263 })
264}
265
266pub fn find_deity_classification_insight(id: &str) -> Option<&'static DeityClassificationInsight> {
267 day_deity_insight_data()
268 .classifications
269 .iter()
270 .find(|c| c.id == id)
271}
272
273pub fn find_deity_insight(name: &str) -> Option<&'static DeityInsight> {
274 day_deity_insight_data()
275 .deities
276 .iter()
277 .find(|d| d.name == name)
278}
279
280pub fn all_na_am_insights() -> &'static [NaAmInsight] {
281 NA_AM_INSIGHT_DATA
282 .get_or_init(|| {
283 let parsed: NaAmInsightFile = serde_json::from_str(NA_AM_INSIGHT_JSON)
284 .expect("Failed to parse data/na-am-insight.json");
285 parsed.pairs
286 })
287 .as_slice()
288}
289
290pub fn find_na_am_insight(na_am: &str) -> Option<&'static NaAmInsight> {
291 all_na_am_insights().iter().find(|n| n.na_am == na_am)
292}
293
294pub fn all_ten_gods_insights() -> &'static [TenGodsInsight] {
295 TEN_GODS_INSIGHT_DATA
296 .get_or_init(|| {
297 let parsed: TenGodsInsightFile = serde_json::from_str(TEN_GODS_INSIGHT_JSON)
298 .expect("Failed to parse data/ten-gods-insight.json");
299 parsed.gods
300 })
301 .as_slice()
302}
303
304pub fn find_ten_gods_insight(id: &str) -> Option<&'static TenGodsInsight> {
305 all_ten_gods_insights().iter().find(|g| g.id == id)
306}
307
308fn tu_menh_insight_data() -> &'static TuMenhInsightFile {
309 TU_MENH_INSIGHT_DATA.get_or_init(|| {
310 serde_json::from_str(TU_MENH_INSIGHT_JSON)
311 .expect("Failed to parse data/tu-menh-insight.json")
312 })
313}
314
315pub fn all_kua_group_insights() -> &'static [KuaGroupInsight] {
316 &tu_menh_insight_data().groups
317}
318
319pub fn all_kua_insights() -> &'static [KuaInsight] {
320 &tu_menh_insight_data().kua
321}
322
323pub fn find_kua_insight(number: u8) -> Option<&'static KuaInsight> {
324 all_kua_insights().iter().find(|k| k.number == number)
325}
326
327pub fn find_kua_group_insight(id: &str) -> Option<&'static KuaGroupInsight> {
328 all_kua_group_insights().iter().find(|g| g.id == id)
329}
330
331fn dai_van_insight_data() -> &'static DaiVanInsightFile {
332 DAI_VAN_INSIGHT_DATA.get_or_init(|| {
333 serde_json::from_str(DAI_VAN_INSIGHT_JSON)
334 .expect("Failed to parse data/dai-van-insight.json")
335 })
336}
337
338pub fn all_dai_van_direction_insights() -> &'static [DaiVanDirectionInsight] {
339 &dai_van_insight_data().directions
340}
341
342pub fn dai_van_phases_insight() -> &'static DaiVanPhasesInsight {
343 &dai_van_insight_data().phases
344}
345
346pub fn all_dai_van_element_insights() -> &'static [DaiVanElementInsight] {
347 &dai_van_insight_data().elements
348}
349
350pub fn find_dai_van_element_insight(element: &str) -> Option<&'static DaiVanElementInsight> {
351 all_dai_van_element_insights()
352 .iter()
353 .find(|e| e.element == element)
354}
355
356pub fn find_dai_van_direction_insight(id: &str) -> Option<&'static DaiVanDirectionInsight> {
357 all_dai_van_direction_insights().iter().find(|d| d.id == id)
358}
359
360#[cfg(test)]
361mod tests {
362 use super::{
363 all_can, all_chi, all_day_guidance, all_elements, all_tiet_khi_insights,
364 find_tiet_khi_insight,
365 };
366
367 #[test]
368 fn parses_canchi_collections() {
369 assert_eq!(all_can().len(), 10);
370 assert_eq!(all_chi().len(), 12);
371 assert_eq!(all_elements().len(), 5);
372 assert_eq!(all_day_guidance().len(), 12);
373 }
374
375 #[test]
376 fn parses_all_tiet_khi() {
377 assert_eq!(all_tiet_khi_insights().len(), 24);
378 }
379
380 #[test]
381 fn lookup_tiet_khi_by_vi_name() {
382 let term = find_tiet_khi_insight("Xuân Phân").expect("Xuân Phân should exist");
383 assert_eq!(term.longitude, 0);
384 assert!(!term.health.vi.is_empty());
385 }
386
387 #[test]
388 fn all_truc_insights_has_12_entries() {
389 assert_eq!(super::all_truc_insights().len(), 12);
390 }
391
392 #[test]
393 fn find_truc_insight_returns_entry() {
394 let truc = super::find_truc_insight("Kiến");
395 assert!(truc.is_some());
396 assert!(!truc.unwrap().meaning.vi.is_empty());
397 }
398
399 #[test]
400 fn all_na_am_insights_has_30_entries() {
401 assert_eq!(super::all_na_am_insights().len(), 30);
402 }
403
404 #[test]
405 fn find_na_am_insight_returns_entry() {
406 let na_am = super::find_na_am_insight("Hải Trung Kim");
407 assert!(na_am.is_some());
408 }
409
410 #[test]
411 fn all_ten_gods_insights_has_10_entries() {
412 assert_eq!(super::all_ten_gods_insights().len(), 10);
413 }
414
415 #[test]
416 fn find_deity_classification_returns_entry() {
417 let cls = super::find_deity_classification_insight("HoangDao");
418 assert!(cls.is_some());
419 }
420
421 #[test]
422 fn find_deity_by_name_returns_entry() {
423 let deity = super::find_deity_insight("Thanh Long");
424 assert!(deity.is_some());
425 }
426
427 #[test]
428 fn tu_menh_kua_insights_has_8_entries() {
429 assert_eq!(super::all_kua_insights().len(), 8);
430 }
431
432 #[test]
433 fn find_kua_insight_by_number() {
434 let kua = super::find_kua_insight(1);
435 assert!(kua.is_some());
436 }
437
438 #[test]
439 fn tu_menh_group_insights_has_2_entries() {
440 assert_eq!(super::all_kua_group_insights().len(), 2);
441 }
442
443 #[test]
444 fn dai_van_element_insights_has_5_entries() {
445 assert_eq!(super::all_dai_van_element_insights().len(), 5);
446 }
447}