Skip to main content

amlich_api/
convert.rs

1use crate::dto::{
2    ActiveRecommendationPackDto, ActivityLabelDto, CanChiDto, CanChiInfoDto, CanInsightDto,
3    ChiInsightDto, ConventionMetadataDto, DailyRecommendationsDto, DayConflictDto, DayDeityDto,
4    DayElementDto, DayFortuneDto, DayGuidanceDto, DayInfoDto, DayStarDto, DayStarsDto, DayTabooDto,
5    DayTenGodsDto, ElementInsightDto, FestivalInsightDto, FoodInsightDto, GioHoangDaoDto,
6    HolidayDto, HolidayInsightDto, HourInfoDto, KuaResultDto, LocalizedListDto, LocalizedTextDto,
7    LunarDto, NaAmErrorDto, NaAmLookupResultDto, NguHanhDto, ProverbInsightDto,
8    RecommendationBucketDto, RecommendationEvidenceDto, RecommendationEvidenceSourceDto,
9    RecommendationPackCatalogEntryDto, RecommendationReasonDto, RecommendationScopeDto,
10    RecommendationSeverityDto, RegionsInsightDto, RuleEvidenceDto, RulesetCatalogEntryDto,
11    RulesetDefaultsDto, RulesetSourceNoteDto, SolarDto, StarRuleEvidenceDto,
12    SynthesizedRecommendationDto, TabooInsightDto, TangCanDto, ThapThanResultDto, TietKhiDto,
13    TietKhiInsightDto, TravelDirectionDto, TrucDto, XungHopDto,
14};
15
16impl From<&amlich_core::NguHanh> for NguHanhDto {
17    fn from(value: &amlich_core::NguHanh) -> Self {
18        Self {
19            can: value.can.clone(),
20            chi: value.chi.clone(),
21        }
22    }
23}
24
25impl From<&amlich_core::CanChi> for CanChiDto {
26    fn from(value: &amlich_core::CanChi) -> Self {
27        Self {
28            can_index: value.can_index,
29            chi_index: value.chi_index,
30            can: value.can.clone(),
31            chi: value.chi.clone(),
32            full: value.full.clone(),
33            con_giap: value.con_giap.clone(),
34            ngu_hanh: NguHanhDto::from(&value.ngu_hanh),
35        }
36    }
37}
38
39impl From<&amlich_core::CanChiSet> for CanChiInfoDto {
40    fn from(value: &amlich_core::CanChiSet) -> Self {
41        Self {
42            day: CanChiDto::from(&value.day),
43            month: CanChiDto::from(&value.month),
44            year: CanChiDto::from(&value.year),
45            full: format!(
46                "{}, tháng {}, năm {}",
47                value.day.full, value.month.full, value.year.full
48            ),
49        }
50    }
51}
52
53impl From<&amlich_core::SolarDate> for SolarDto {
54    fn from(value: &amlich_core::SolarDate) -> Self {
55        Self {
56            day: value.day,
57            month: value.month,
58            year: value.year,
59            day_of_week: value.day_of_week,
60            day_of_week_name: amlich_core::THU[value.day_of_week].to_string(),
61            date_string: format!("{}-{:02}-{:02}", value.year, value.month, value.day),
62        }
63    }
64}
65
66impl From<&amlich_core::lunar::LunarDate> for LunarDto {
67    fn from(value: &amlich_core::lunar::LunarDate) -> Self {
68        Self {
69            day: value.day,
70            month: value.month,
71            year: value.year,
72            is_leap_month: value.is_leap,
73            date_string: format!(
74                "{}/{}/{}{}",
75                value.day,
76                value.month,
77                value.year,
78                if value.is_leap { " (nhuận)" } else { "" }
79            ),
80        }
81    }
82}
83
84impl From<&amlich_core::tietkhi::SolarTerm> for TietKhiDto {
85    fn from(value: &amlich_core::tietkhi::SolarTerm) -> Self {
86        Self {
87            index: value.index,
88            name: value.name.clone(),
89            description: value.description.clone(),
90            longitude: value.longitude,
91            current_longitude: value.current_longitude,
92            season: value.season.clone(),
93        }
94    }
95}
96
97impl From<&amlich_core::gio_hoang_dao::HourInfo> for HourInfoDto {
98    fn from(value: &amlich_core::gio_hoang_dao::HourInfo) -> Self {
99        Self {
100            hour_index: value.hour_index,
101            hour_chi: value.hour_chi.clone(),
102            time_range: value.time_range.clone(),
103            star: value.star.clone(),
104            is_good: value.is_good,
105        }
106    }
107}
108
109impl From<&amlich_core::gio_hoang_dao::GioHoangDao> for GioHoangDaoDto {
110    fn from(value: &amlich_core::gio_hoang_dao::GioHoangDao) -> Self {
111        Self {
112            day_chi: value.day_chi.clone(),
113            good_hour_count: value.good_hour_count,
114            good_hours: value.good_hours.iter().map(HourInfoDto::from).collect(),
115            all_hours: value.all_hours.iter().map(HourInfoDto::from).collect(),
116            summary: value.summary.clone(),
117        }
118    }
119}
120
121impl From<&amlich_core::almanac::types::DayElement> for DayElementDto {
122    fn from(value: &amlich_core::almanac::types::DayElement) -> Self {
123        Self {
124            na_am: value.na_am.clone(),
125            element: value.element.clone(),
126            can_element: value.can_element.clone(),
127            chi_element: value.chi_element.clone(),
128            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
129        }
130    }
131}
132
133impl From<&amlich_core::almanac::types::RuleEvidence> for RuleEvidenceDto {
134    fn from(value: &amlich_core::almanac::types::RuleEvidence) -> Self {
135        Self {
136            source_id: value.source_id.clone(),
137            method: value.method.clone(),
138            profile: value.profile.clone(),
139        }
140    }
141}
142
143impl From<&amlich_core::almanac::types::DayConflict> for DayConflictDto {
144    fn from(value: &amlich_core::almanac::types::DayConflict) -> Self {
145        Self {
146            opposing_chi: value.opposing_chi.clone(),
147            opposing_con_giap: value.opposing_con_giap.clone(),
148            tuoi_xung: value.tuoi_xung.clone(),
149            sat_huong: value.sat_huong.clone(),
150            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
151        }
152    }
153}
154
155impl From<&amlich_core::almanac::types::TravelDirection> for TravelDirectionDto {
156    fn from(value: &amlich_core::almanac::types::TravelDirection) -> Self {
157        Self {
158            xuat_hanh_huong: value.xuat_hanh_huong.clone(),
159            tai_than: value.tai_than.clone(),
160            hy_than: value.hy_than.clone(),
161            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
162        }
163    }
164}
165
166impl From<&amlich_core::almanac::types::DayStar> for DayStarDto {
167    fn from(value: &amlich_core::almanac::types::DayStar) -> Self {
168        let system = match value.system {
169            amlich_core::almanac::types::StarSystem::NhiThapBatTu => "nhi-thap-bat-tu",
170        }
171        .to_string();
172        let quality = match value.quality {
173            amlich_core::almanac::types::StarQuality::Cat => "cat",
174            amlich_core::almanac::types::StarQuality::Hung => "hung",
175            amlich_core::almanac::types::StarQuality::Binh => "binh",
176        }
177        .to_string();
178        Self {
179            system,
180            index: value.index,
181            name: value.name.clone(),
182            quality,
183            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
184        }
185    }
186}
187
188impl From<&amlich_core::almanac::types::StarRuleEvidence> for StarRuleEvidenceDto {
189    fn from(value: &amlich_core::almanac::types::StarRuleEvidence) -> Self {
190        let quality = match value.quality {
191            amlich_core::almanac::types::StarQuality::Cat => "cat",
192            amlich_core::almanac::types::StarQuality::Hung => "hung",
193            amlich_core::almanac::types::StarQuality::Binh => "binh",
194        }
195        .to_string();
196        Self {
197            name: value.name.clone(),
198            quality,
199            category: value.category.clone(),
200            source_id: value.source_id.clone(),
201            method: value.method.clone(),
202            profile: value.profile.clone(),
203        }
204    }
205}
206
207impl From<&amlich_core::almanac::types::DayStars> for DayStarsDto {
208    fn from(value: &amlich_core::almanac::types::DayStars) -> Self {
209        let star_system = value.star_system.as_ref().map(|system| match system {
210            amlich_core::almanac::types::StarSystem::NhiThapBatTu => "nhi-thap-bat-tu",
211        });
212
213        Self {
214            cat_tinh: value.cat_tinh.clone(),
215            sat_tinh: value.sat_tinh.clone(),
216            day_star: value.day_star.as_ref().map(DayStarDto::from),
217            star_system: star_system.map(str::to_string),
218            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
219            matched_rules: value
220                .matched_rules
221                .iter()
222                .map(StarRuleEvidenceDto::from)
223                .collect(),
224        }
225    }
226}
227
228impl From<&amlich_core::almanac::types::XungHopResult> for XungHopDto {
229    fn from(value: &amlich_core::almanac::types::XungHopResult) -> Self {
230        Self {
231            luc_xung: value.luc_xung.clone(),
232            tam_hop: value.tam_hop.clone(),
233            tu_hanh_xung: value.tu_hanh_xung.clone(),
234            liu_he: value.liu_he.clone(),
235            xiang_hai: value.xiang_hai.clone(),
236            xiang_xing: value.xiang_xing.clone(),
237        }
238    }
239}
240
241impl From<&amlich_core::almanac::types::TangCan> for TangCanDto {
242    fn from(value: &amlich_core::almanac::types::TangCan) -> Self {
243        Self {
244            main: value.main.clone(),
245            central: value.central.clone(),
246            residual: value.residual.clone(),
247            strength: value.strength,
248        }
249    }
250}
251
252impl From<&amlich_core::almanac::types::TrucInfo> for TrucDto {
253    fn from(value: &amlich_core::almanac::types::TrucInfo) -> Self {
254        Self {
255            index: value.index,
256            name: value.name.clone(),
257            quality: value.quality.clone(),
258            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
259        }
260    }
261}
262
263impl From<&amlich_core::almanac::types::DayTaboo> for DayTabooDto {
264    fn from(value: &amlich_core::almanac::types::DayTaboo) -> Self {
265        Self {
266            rule_id: value.rule_id.clone(),
267            name: value.name.clone(),
268            severity: value.severity.clone(),
269            reason: value.reason.clone(),
270            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
271        }
272    }
273}
274
275impl From<&amlich_core::almanac::types::DayDeity> for DayDeityDto {
276    fn from(value: &amlich_core::almanac::types::DayDeity) -> Self {
277        let classification = match value.classification {
278            amlich_core::almanac::types::DayDeityClassification::HoangDao => "hoang_dao",
279            amlich_core::almanac::types::DayDeityClassification::HacDao => "hac_dao",
280        }
281        .to_string();
282
283        Self {
284            name: value.name.clone(),
285            classification,
286            evidence: value.evidence.as_ref().map(RuleEvidenceDto::from),
287        }
288    }
289}
290
291impl From<&amlich_core::almanac::types::DayTenGods> for DayTenGodsDto {
292    fn from(value: &amlich_core::almanac::types::DayTenGods) -> Self {
293        Self {
294            to_year_stem: value.to_year_stem.as_ref().map(ThapThanResultDto::from),
295            to_self: value.to_self.as_ref().map(ThapThanResultDto::from),
296        }
297    }
298}
299
300impl From<&amlich_core::almanac::types::ThapThanResult> for ThapThanResultDto {
301    fn from(value: &amlich_core::almanac::types::ThapThanResult) -> Self {
302        let relation = match value.relation {
303            amlich_core::almanac::types::FiveElementRelation::Same => "same".to_string(),
304            amlich_core::almanac::types::FiveElementRelation::DayGeneratesTarget => {
305                "day_generates_target".to_string()
306            }
307            amlich_core::almanac::types::FiveElementRelation::TargetGeneratesDay => {
308                "target_generates_day".to_string()
309            }
310            amlich_core::almanac::types::FiveElementRelation::DayControlsTarget => {
311                "day_controls_target".to_string()
312            }
313            amlich_core::almanac::types::FiveElementRelation::TargetControlsDay => {
314                "target_controls_day".to_string()
315            }
316        };
317
318        let label = match value.label {
319            amlich_core::almanac::types::ThapThanLabel::TyKien => "ty_kien".to_string(),
320            amlich_core::almanac::types::ThapThanLabel::KiepTai => "kiep_tai".to_string(),
321            amlich_core::almanac::types::ThapThanLabel::ThucThan => "thuc_than".to_string(),
322            amlich_core::almanac::types::ThapThanLabel::ThuongQuan => "thuong_quan".to_string(),
323            amlich_core::almanac::types::ThapThanLabel::ChinhTai => "chinh_tai".to_string(),
324            amlich_core::almanac::types::ThapThanLabel::ThienTai => "thien_tai".to_string(),
325            amlich_core::almanac::types::ThapThanLabel::ChinhQuan => "chinh_quan".to_string(),
326            amlich_core::almanac::types::ThapThanLabel::ThatSat => "that_sat".to_string(),
327            amlich_core::almanac::types::ThapThanLabel::ChinhAn => "chinh_an".to_string(),
328            amlich_core::almanac::types::ThapThanLabel::ThienAn => "thien_an".to_string(),
329        };
330
331        Self {
332            label,
333            relation,
334            same_polarity: value.same_polarity,
335            evidence: RuleEvidenceDto::from(&value.evidence),
336        }
337    }
338}
339
340impl From<&amlich_core::almanac::tu_menh::KuaResult> for KuaResultDto {
341    fn from(value: &amlich_core::almanac::tu_menh::KuaResult) -> Self {
342        let group = match value.group {
343            amlich_core::almanac::tu_menh::KuaGroup::East => "east".to_string(),
344            amlich_core::almanac::tu_menh::KuaGroup::West => "west".to_string(),
345        };
346
347        let favorable_directions = value
348            .favorable_directions
349            .iter()
350            .map(|d| d.to_string())
351            .collect();
352
353        let unfavorable_directions = value
354            .unfavorable_directions
355            .iter()
356            .map(|d| d.to_string())
357            .collect();
358
359        let convention = ConventionMetadataDto {
360            year_basis: value.convention.year_basis.clone(),
361            kua_five_resolution: value.convention.kua5_resolution.clone(),
362            gender_encoding: value.convention.gender_encoding.clone(),
363        };
364
365        Self {
366            kua: value.kua,
367            group,
368            favorable_directions,
369            unfavorable_directions,
370            convention,
371        }
372    }
373}
374
375impl From<&amlich_core::almanac::types::DayFortune> for DayFortuneDto {
376    fn from(value: &amlich_core::almanac::types::DayFortune) -> Self {
377        Self {
378            ruleset_id: value.ruleset_id.clone(),
379            ruleset_version: value.ruleset_version.clone(),
380            profile: value.profile.clone(),
381            day_element: DayElementDto::from(&value.day_element),
382            conflict: DayConflictDto::from(&value.conflict),
383            travel: TravelDirectionDto::from(&value.travel),
384            stars: DayStarsDto::from(&value.stars),
385            day_deity: value.day_deity.as_ref().map(DayDeityDto::from),
386            taboos: value.taboos.iter().map(DayTabooDto::from).collect(),
387            xung_hop: XungHopDto::from(&value.xung_hop),
388            truc: TrucDto::from(&value.truc),
389            tang_can: value.tang_can.as_ref().map(TangCanDto::from),
390            ten_gods: value.ten_gods.as_ref().map(DayTenGodsDto::from),
391            tu_menh: value.tu_menh.as_ref().map(KuaResultDto::from),
392        }
393    }
394}
395
396fn activity_id_to_snake_case(
397    activity_id: amlich_core::almanac::recommendation::ActivityId,
398) -> String {
399    match activity_id {
400        amlich_core::almanac::recommendation::ActivityId::Travel => "travel",
401        amlich_core::almanac::recommendation::ActivityId::MeetingSocial => "meeting_social",
402        amlich_core::almanac::recommendation::ActivityId::OpeningStart => "opening_start",
403        amlich_core::almanac::recommendation::ActivityId::ContractAgreement => "contract_agreement",
404        amlich_core::almanac::recommendation::ActivityId::BusinessTrade => "business_trade",
405        amlich_core::almanac::recommendation::ActivityId::FinanceInvestment => "finance_investment",
406        amlich_core::almanac::recommendation::ActivityId::ConstructionGroundbreaking => {
407            "construction_groundbreaking"
408        }
409        amlich_core::almanac::recommendation::ActivityId::RepairRenovation => "repair_renovation",
410        amlich_core::almanac::recommendation::ActivityId::MoveRelocation => "move_relocation",
411        amlich_core::almanac::recommendation::ActivityId::WeddingEngagement => "wedding_engagement",
412        amlich_core::almanac::recommendation::ActivityId::LawsuitDispute => "lawsuit_dispute",
413        amlich_core::almanac::recommendation::ActivityId::PrayerOffering => "prayer_offering",
414        amlich_core::almanac::recommendation::ActivityId::MedicalTreatment => "medical_treatment",
415        amlich_core::almanac::recommendation::ActivityId::BurialMemorial => "burial_memorial",
416        amlich_core::almanac::recommendation::ActivityId::CleaningPurging => "cleaning_purging",
417    }
418    .to_string()
419}
420
421impl From<&amlich_core::almanac::recommendation::ActivityLabel> for ActivityLabelDto {
422    fn from(value: &amlich_core::almanac::recommendation::ActivityLabel) -> Self {
423        Self {
424            vi: value.vi.clone(),
425            en: value.en.clone(),
426        }
427    }
428}
429
430impl From<amlich_core::almanac::recommendation::RecommendationScope> for RecommendationScopeDto {
431    fn from(value: amlich_core::almanac::recommendation::RecommendationScope) -> Self {
432        match value {
433            amlich_core::almanac::recommendation::RecommendationScope::GeneralDay => {
434                RecommendationScopeDto::GeneralDay
435            }
436        }
437    }
438}
439
440impl From<amlich_core::almanac::recommendation::RecommendationBucket> for RecommendationBucketDto {
441    fn from(value: amlich_core::almanac::recommendation::RecommendationBucket) -> Self {
442        match value {
443            amlich_core::almanac::recommendation::RecommendationBucket::Nen => {
444                RecommendationBucketDto::Nen
445            }
446            amlich_core::almanac::recommendation::RecommendationBucket::CoThe => {
447                RecommendationBucketDto::CoThe
448            }
449            amlich_core::almanac::recommendation::RecommendationBucket::Tranh => {
450                RecommendationBucketDto::Tranh
451            }
452            amlich_core::almanac::recommendation::RecommendationBucket::KyManh => {
453                RecommendationBucketDto::KyManh
454            }
455        }
456    }
457}
458
459impl From<amlich_core::almanac::recommendation::RecommendationSeverity>
460    for RecommendationSeverityDto
461{
462    fn from(value: amlich_core::almanac::recommendation::RecommendationSeverity) -> Self {
463        match value {
464            amlich_core::almanac::recommendation::RecommendationSeverity::Primary => {
465                RecommendationSeverityDto::Primary
466            }
467            amlich_core::almanac::recommendation::RecommendationSeverity::Supporting => {
468                RecommendationSeverityDto::Supporting
469            }
470            amlich_core::almanac::recommendation::RecommendationSeverity::Override => {
471                RecommendationSeverityDto::Override
472            }
473        }
474    }
475}
476
477impl From<amlich_core::almanac::recommendation::RecommendationEvidenceSource>
478    for RecommendationEvidenceSourceDto
479{
480    fn from(value: amlich_core::almanac::recommendation::RecommendationEvidenceSource) -> Self {
481        match value {
482            amlich_core::almanac::recommendation::RecommendationEvidenceSource::DayGuidance => {
483                RecommendationEvidenceSourceDto::DayGuidance
484            }
485            amlich_core::almanac::recommendation::RecommendationEvidenceSource::Truc => {
486                RecommendationEvidenceSourceDto::Truc
487            }
488            amlich_core::almanac::recommendation::RecommendationEvidenceSource::Stars => {
489                RecommendationEvidenceSourceDto::Stars
490            }
491            amlich_core::almanac::recommendation::RecommendationEvidenceSource::DayDeity => {
492                RecommendationEvidenceSourceDto::DayDeity
493            }
494            amlich_core::almanac::recommendation::RecommendationEvidenceSource::Taboo => {
495                RecommendationEvidenceSourceDto::Taboo
496            }
497            amlich_core::almanac::recommendation::RecommendationEvidenceSource::XungHop => {
498                RecommendationEvidenceSourceDto::XungHop
499            }
500            amlich_core::almanac::recommendation::RecommendationEvidenceSource::TietKhi => {
501                RecommendationEvidenceSourceDto::TietKhi
502            }
503            amlich_core::almanac::recommendation::RecommendationEvidenceSource::GioHoangDao => {
504                RecommendationEvidenceSourceDto::GioHoangDao
505            }
506            amlich_core::almanac::recommendation::RecommendationEvidenceSource::Travel => {
507                RecommendationEvidenceSourceDto::Travel
508            }
509            amlich_core::almanac::recommendation::RecommendationEvidenceSource::ProductRule => {
510                RecommendationEvidenceSourceDto::ProductRule
511            }
512        }
513    }
514}
515
516impl From<&amlich_core::almanac::recommendation::RecommendationEvidence>
517    for RecommendationEvidenceDto
518{
519    fn from(value: &amlich_core::almanac::recommendation::RecommendationEvidence) -> Self {
520        Self {
521            source: RecommendationEvidenceSourceDto::from(value.source),
522            code: value.code.clone(),
523            note: value.note.clone(),
524        }
525    }
526}
527
528impl From<&amlich_core::almanac::recommendation::RecommendationReason> for RecommendationReasonDto {
529    fn from(value: &amlich_core::almanac::recommendation::RecommendationReason) -> Self {
530        Self {
531            rule_id: value.rule_id.clone(),
532            severity: RecommendationSeverityDto::from(value.severity),
533            summary_vi: value.summary_vi.clone(),
534            summary_en: value.summary_en.clone(),
535            evidence: RecommendationEvidenceDto::from(&value.evidence),
536        }
537    }
538}
539
540impl From<&amlich_core::almanac::recommendation::ActiveRecommendationPack>
541    for ActiveRecommendationPackDto
542{
543    fn from(value: &amlich_core::almanac::recommendation::ActiveRecommendationPack) -> Self {
544        Self {
545            pack_id: value.pack_id.clone(),
546            version: value.version.clone(),
547            source_family: value.source_family.clone(),
548            mode: match value.mode {
549                amlich_core::almanac::recommendation::RecommendationPackMode::Advisory => {
550                    "advisory"
551                }
552                amlich_core::almanac::recommendation::RecommendationPackMode::TraditionVariant => {
553                    "tradition_variant"
554                }
555                amlich_core::almanac::recommendation::RecommendationPackMode::Experimental => {
556                    "experimental"
557                }
558            }
559            .to_string(),
560        }
561    }
562}
563
564impl From<&amlich_core::almanac::types::RuleSetDefaults> for RulesetDefaultsDto {
565    fn from(value: &amlich_core::almanac::types::RuleSetDefaults) -> Self {
566        Self {
567            tz_offset: value.tz_offset,
568            meridian: value.meridian.clone(),
569        }
570    }
571}
572
573impl From<&amlich_core::almanac::types::RuleSetSourceNote> for RulesetSourceNoteDto {
574    fn from(value: &amlich_core::almanac::types::RuleSetSourceNote) -> Self {
575        Self {
576            family: value.family.clone(),
577            source_id: value.source_id.clone(),
578            note: value.note.clone(),
579        }
580    }
581}
582
583impl From<&amlich_core::almanac::data::RulesetRegistryEntry> for RulesetCatalogEntryDto {
584    fn from(value: &amlich_core::almanac::data::RulesetRegistryEntry) -> Self {
585        let descriptor = value.descriptor.to_document_descriptor();
586
587        Self {
588            id: descriptor.id,
589            canonical_id: value.descriptor.id.to_string(),
590            version: descriptor.version,
591            region: descriptor.region,
592            profile: descriptor.profile,
593            schema_version: descriptor.schema_version,
594            is_default: value.descriptor.id == amlich_core::almanac::data::DEFAULT_RULESET_ID,
595            aliases: value
596                .aliases
597                .iter()
598                .map(|alias| (*alias).to_string())
599                .collect(),
600            defaults: RulesetDefaultsDto::from(&descriptor.defaults),
601            source_notes: descriptor
602                .source_notes
603                .iter()
604                .map(RulesetSourceNoteDto::from)
605                .collect(),
606        }
607    }
608}
609
610impl From<&amlich_core::almanac::recommendation::RecommendationPackDescriptor>
611    for RecommendationPackCatalogEntryDto
612{
613    fn from(value: &amlich_core::almanac::recommendation::RecommendationPackDescriptor) -> Self {
614        Self {
615            pack_id: value.pack_id.to_string(),
616            request_field: "enabled_pack_ids".to_string(),
617            version: value.version.to_string(),
618            source_family: value.source_family.to_string(),
619            mode: match value.mode {
620                amlich_core::almanac::recommendation::RecommendationPackMode::Advisory => {
621                    "advisory"
622                }
623                amlich_core::almanac::recommendation::RecommendationPackMode::TraditionVariant => {
624                    "tradition_variant"
625                }
626                amlich_core::almanac::recommendation::RecommendationPackMode::Experimental => {
627                    "experimental"
628                }
629            }
630            .to_string(),
631        }
632    }
633}
634
635impl From<&amlich_core::almanac::recommendation::SynthesizedRecommendation>
636    for SynthesizedRecommendationDto
637{
638    fn from(value: &amlich_core::almanac::recommendation::SynthesizedRecommendation) -> Self {
639        Self {
640            activity_id: activity_id_to_snake_case(value.activity_id),
641            label: ActivityLabelDto::from(&value.label),
642            bucket: RecommendationBucketDto::from(value.bucket),
643            reasons: value
644                .reasons
645                .iter()
646                .map(RecommendationReasonDto::from)
647                .collect(),
648        }
649    }
650}
651
652impl From<&amlich_core::almanac::recommendation::DailyRecommendations> for DailyRecommendationsDto {
653    fn from(value: &amlich_core::almanac::recommendation::DailyRecommendations) -> Self {
654        Self {
655            ruleset_id: value.ruleset_id.clone(),
656            ruleset_version: value.ruleset_version.clone(),
657            profile: value.profile.clone(),
658            scope: RecommendationScopeDto::from(value.scope),
659            version: value.version.clone(),
660            summary_vi: value.summary_vi.clone(),
661            summary_en: value.summary_en.clone(),
662            active_packs: value
663                .active_packs
664                .iter()
665                .map(ActiveRecommendationPackDto::from)
666                .collect(),
667            activities: value
668                .activities
669                .iter()
670                .map(SynthesizedRecommendationDto::from)
671                .collect(),
672        }
673    }
674}
675
676impl From<&amlich_core::DaySnapshot> for DayInfoDto {
677    fn from(value: &amlich_core::DaySnapshot) -> Self {
678        Self {
679            ruleset_id: value.ruleset_id.clone(),
680            ruleset_version: value.ruleset_version.clone(),
681            profile: value.profile.clone(),
682            solar: SolarDto::from(&value.context.solar),
683            lunar: LunarDto::from(&value.context.lunar),
684            jd: value.context.jd,
685            canchi: CanChiInfoDto::from(&value.context.canchi),
686            tiet_khi: TietKhiDto::from(&value.context.tiet_khi),
687            gio_hoang_dao: GioHoangDaoDto::from(&value.context.gio_hoang_dao),
688            day_fortune: Some(DayFortuneDto::from(&value.day_fortune)),
689            daily_recommendations: DailyRecommendationsDto::from(&value.daily_recommendations),
690            contextual_recommendations: value
691                .contextual_recommendations
692                .as_ref()
693                .map(DailyRecommendationsDto::from),
694        }
695    }
696}
697
698impl From<&amlich_core::holidays::Holiday> for HolidayDto {
699    fn from(value: &amlich_core::holidays::Holiday) -> Self {
700        Self {
701            name: value.name.clone(),
702            description: value.description.clone(),
703            solar_day: value.solar_day,
704            solar_month: value.solar_month,
705            solar_year: value.solar_year,
706            lunar_day: value.lunar_date.as_ref().map(|d| d.day),
707            lunar_month: value.lunar_date.as_ref().map(|d| d.month),
708            lunar_year: value.lunar_date.as_ref().map(|d| d.year),
709            is_solar: value.is_solar,
710            category: value.category.clone(),
711            is_major: value.is_major,
712        }
713    }
714}
715
716impl From<&amlich_core::holiday_data::BilingualText> for LocalizedTextDto {
717    fn from(value: &amlich_core::holiday_data::BilingualText) -> Self {
718        Self {
719            vi: value.vi.clone(),
720            en: value.en.clone(),
721        }
722    }
723}
724
725impl From<&amlich_core::insight_data::BilingualText> for LocalizedTextDto {
726    fn from(value: &amlich_core::insight_data::BilingualText) -> Self {
727        Self {
728            vi: value.vi.clone(),
729            en: value.en.clone(),
730        }
731    }
732}
733
734impl From<&amlich_core::insight_data::BilingualList> for LocalizedListDto {
735    fn from(value: &amlich_core::insight_data::BilingualList) -> Self {
736        Self {
737            vi: value.vi.clone(),
738            en: value.en.clone(),
739        }
740    }
741}
742
743impl From<&amlich_core::holiday_data::BilingualList> for LocalizedListDto {
744    fn from(value: &amlich_core::holiday_data::BilingualList) -> Self {
745        Self {
746            vi: value.vi.clone(),
747            en: value.en.clone(),
748        }
749    }
750}
751
752impl From<&amlich_core::holiday_data::FoodItem> for FoodInsightDto {
753    fn from(value: &amlich_core::holiday_data::FoodItem) -> Self {
754        Self {
755            name: LocalizedTextDto::from(&value.name),
756            description: LocalizedTextDto::from(&value.description),
757        }
758    }
759}
760
761impl From<&amlich_core::holiday_data::TabooItem> for TabooInsightDto {
762    fn from(value: &amlich_core::holiday_data::TabooItem) -> Self {
763        Self {
764            action: LocalizedTextDto::from(&value.action),
765            reason: LocalizedTextDto::from(&value.reason),
766        }
767    }
768}
769
770impl From<&amlich_core::holiday_data::ProverbItem> for ProverbInsightDto {
771    fn from(value: &amlich_core::holiday_data::ProverbItem) -> Self {
772        Self {
773            text: value.text.clone(),
774            meaning: LocalizedTextDto::from(&value.meaning),
775        }
776    }
777}
778
779impl From<&amlich_core::holiday_data::Regions> for RegionsInsightDto {
780    fn from(value: &amlich_core::holiday_data::Regions) -> Self {
781        Self {
782            north: LocalizedTextDto::from(&value.north),
783            central: LocalizedTextDto::from(&value.central),
784            south: LocalizedTextDto::from(&value.south),
785        }
786    }
787}
788
789impl From<&amlich_core::holiday_data::LunarFestivalData> for FestivalInsightDto {
790    fn from(value: &amlich_core::holiday_data::LunarFestivalData) -> Self {
791        Self {
792            names: LocalizedListDto {
793                vi: value.names.vi.clone(),
794                en: value.names.en.clone(),
795            },
796            origin: value.origin.as_ref().map(LocalizedTextDto::from),
797            activities: value.activities.as_ref().map(LocalizedListDto::from),
798            food: value.food.iter().map(FoodInsightDto::from).collect(),
799            taboos: value.taboos.iter().map(TabooInsightDto::from).collect(),
800            proverbs: value.proverbs.iter().map(ProverbInsightDto::from).collect(),
801            regions: value.regions.as_ref().map(RegionsInsightDto::from),
802            category: value.category.clone(),
803            is_major: value.is_major,
804        }
805    }
806}
807
808impl From<&amlich_core::holiday_data::SolarHolidayData> for HolidayInsightDto {
809    fn from(value: &amlich_core::holiday_data::SolarHolidayData) -> Self {
810        Self {
811            names: LocalizedListDto {
812                vi: value.names.vi.clone(),
813                en: value.names.en.clone(),
814            },
815            origin: value.origin.as_ref().map(LocalizedTextDto::from),
816            significance: value.significance.as_ref().map(LocalizedTextDto::from),
817            activities: value.activities.as_ref().map(LocalizedListDto::from),
818            traditions: value.traditions.as_ref().map(LocalizedListDto::from),
819            food: value.food.iter().map(FoodInsightDto::from).collect(),
820            taboos: value.taboos.iter().map(TabooInsightDto::from).collect(),
821            proverbs: value.proverbs.iter().map(ProverbInsightDto::from).collect(),
822            regions: value.regions.as_ref().map(RegionsInsightDto::from),
823            category: value.category.clone(),
824            is_major: value.is_major,
825        }
826    }
827}
828
829impl From<(&String, &amlich_core::insight_data::ElementInfo)> for ElementInsightDto {
830    fn from((key, value): (&String, &amlich_core::insight_data::ElementInfo)) -> Self {
831        Self {
832            key: key.clone(),
833            name: LocalizedTextDto::from(&value.name),
834            nature: LocalizedTextDto::from(&value.nature),
835        }
836    }
837}
838
839impl From<&amlich_core::insight_data::CanInfo> for CanInsightDto {
840    fn from(value: &amlich_core::insight_data::CanInfo) -> Self {
841        Self {
842            name: value.name.clone(),
843            element: value.element.clone(),
844            meaning: LocalizedTextDto::from(&value.meaning),
845            nature: LocalizedTextDto::from(&value.nature),
846        }
847    }
848}
849
850impl From<&amlich_core::insight_data::ChiInfo> for ChiInsightDto {
851    fn from(value: &amlich_core::insight_data::ChiInfo) -> Self {
852        Self {
853            name: value.name.clone(),
854            animal: LocalizedTextDto::from(&value.animal),
855            element: value.element.clone(),
856            meaning: LocalizedTextDto::from(&value.meaning),
857            hours: value.hours.clone(),
858        }
859    }
860}
861
862impl From<&amlich_core::insight_data::DayGuidance> for DayGuidanceDto {
863    fn from(value: &amlich_core::insight_data::DayGuidance) -> Self {
864        Self {
865            good_for: LocalizedListDto::from(&value.good_for),
866            avoid_for: LocalizedListDto::from(&value.avoid_for),
867        }
868    }
869}
870
871impl From<&amlich_core::insight_data::TietKhiInsight> for TietKhiInsightDto {
872    fn from(value: &amlich_core::insight_data::TietKhiInsight) -> Self {
873        Self {
874            id: value.id.clone(),
875            name: LocalizedTextDto::from(&value.name),
876            longitude: value.longitude,
877            meaning: LocalizedTextDto::from(&value.meaning),
878            astronomy: LocalizedTextDto::from(&value.astronomy),
879            agriculture: LocalizedListDto::from(&value.agriculture),
880            health: LocalizedListDto::from(&value.health),
881            weather: LocalizedTextDto::from(&value.weather),
882        }
883    }
884}
885
886// Na Am conversion implementations
887
888impl From<amlich_core::almanac::na_am::NaAmError> for NaAmErrorDto {
889    fn from(error: amlich_core::almanac::na_am::NaAmError) -> Self {
890        let (error_type, message) = match error {
891            amlich_core::almanac::na_am::NaAmError::InvalidCycleIndex => (
892                "invalid_cycle_index".to_string(),
893                "Cycle index must be between 1 and 60".to_string(),
894            ),
895            amlich_core::almanac::na_am::NaAmError::InvalidStemBranchPair => (
896                "invalid_stem_branch_pair".to_string(),
897                "Stem and branch must have matching parity (both odd or both even)".to_string(),
898            ),
899            amlich_core::almanac::na_am::NaAmError::UnknownStem => (
900                "unknown_stem".to_string(),
901                "Unknown heavenly stem name".to_string(),
902            ),
903            amlich_core::almanac::na_am::NaAmError::UnknownBranch => (
904                "unknown_branch".to_string(),
905                "Unknown earthly branch name".to_string(),
906            ),
907        };
908
909        Self {
910            error: error_type,
911            message,
912        }
913    }
914}
915
916impl From<&amlich_core::almanac::data::NaAmEntry> for NaAmLookupResultDto {
917    fn from(entry: &amlich_core::almanac::data::NaAmEntry) -> Self {
918        // Find indices for can and chi
919        use amlich_core::types::{CAN, CHI};
920        let can_idx = CAN.iter().position(|&c| c == entry.can).unwrap_or(0);
921        let chi_idx = CHI.iter().position(|&c| c == entry.chi).unwrap_or(0);
922
923        // Convert to cycle index using sexagenary cycle utilities
924        use amlich_core::almanac::sexagenary_cycle::canchi_to_cycle_index;
925        let cycle_index = canchi_to_cycle_index(can_idx, chi_idx).unwrap_or(1);
926
927        // Get metadata and profile from ruleset data
928        use amlich_core::almanac::data::get_ruleset_data;
929        let ruleset =
930            get_ruleset_data("vn_baseline_v1").expect("default ruleset should be available");
931        let meta = &ruleset.na_am_meta;
932
933        Self {
934            cycle_index,
935            can: entry.can.clone(),
936            chi: entry.chi.clone(),
937            na_am: entry.na_am.clone(),
938            element: entry.element.clone(),
939            source_id: meta.source_id.clone(),
940            method: meta.method.clone(),
941            profile: ruleset.profile.clone(),
942        }
943    }
944}