1use serde::{Deserialize, Serialize};
20
21use crate::data::stars::{MUTAGEN, StarKey};
22use crate::data::types::*;
23use crate::error::BridgeError;
24use crate::i18n::{
25 translate_brightness, translate_earthly_branch, translate_five_elements_class,
26 translate_gender, translate_heavenly_stem, translate_mutagen, translate_palace, translate_star,
27};
28use crate::models::astrolabe::Astrolabe;
29use crate::models::horoscope::{HoroscopeData, HoroscopeItem};
30use crate::models::palace::PalaceData;
31use crate::models::star::Star;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
39#[serde(rename_all = "camelCase")]
40pub struct StarDto {
41 pub key: String,
43 pub name: String,
45 #[serde(rename = "type")]
47 pub star_type: String,
48 pub scope: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub brightness: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub brightness_key: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub mutagen: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
61 pub mutagen_key: Option<String>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub struct DecadalDto {
68 pub range: [u32; 2],
70 pub heavenly_stem: String,
72 pub heavenly_stem_key: String,
74 pub earthly_branch: String,
76 pub earthly_branch_key: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct PalaceDto {
84 pub index: usize,
86 pub name: String,
88 pub name_key: String,
90 pub is_body_palace: bool,
92 pub is_original_palace: bool,
94 pub heavenly_stem: String,
96 pub heavenly_stem_key: String,
98 pub earthly_branch: String,
100 pub earthly_branch_key: String,
102 pub major_stars: Vec<StarDto>,
104 pub minor_stars: Vec<StarDto>,
106 pub adjective_stars: Vec<StarDto>,
108 pub changsheng12: String,
110 pub changsheng12_key: String,
112 pub boshi12: String,
114 pub boshi12_key: String,
116 pub jiangqian12: String,
118 pub jiangqian12_key: String,
120 pub suiqian12: String,
122 pub suiqian12_key: String,
124 pub decadal: DecadalDto,
126 pub ages: Vec<u32>,
128 pub mutagen_star_keys: [String; 4],
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136#[serde(rename_all = "camelCase")]
137pub struct RawLunarDateDto {
138 pub lunar_year: i64,
140 pub lunar_month: u32,
142 pub lunar_day: u32,
144 pub is_leap: bool,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150#[serde(rename_all = "camelCase")]
151pub struct RawChineseDateDto {
152 pub yearly: [String; 2],
154 pub monthly: [String; 2],
156 pub daily: [String; 2],
158 pub hourly: [String; 2],
160 pub yearly_keys: [String; 2],
162 pub monthly_keys: [String; 2],
164 pub daily_keys: [String; 2],
166 pub hourly_keys: [String; 2],
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct RawDatesDto {
174 pub lunar_date: RawLunarDateDto,
176 pub chinese_date: RawChineseDateDto,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct ConfigDto {
184 pub year_divide: String,
186 pub horoscope_divide: String,
188 pub age_divide: String,
190 pub day_divide: String,
192 pub algorithm: String,
194 pub astro_type: String,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200#[serde(rename_all = "camelCase")]
201pub struct AstrolabeDto {
202 pub gender: String,
204 pub solar_date: String,
206 pub lunar_date: String,
208 pub chinese_date: String,
210 pub raw_dates: RawDatesDto,
212 pub time: String,
214 pub time_range: String,
216 pub sign: String,
218 pub zodiac: String,
220 pub earthly_branch_of_soul_palace: String,
222 pub earthly_branch_of_soul_palace_key: String,
224 pub earthly_branch_of_body_palace: String,
226 pub earthly_branch_of_body_palace_key: String,
228 pub soul: String,
230 pub soul_key: String,
232 pub body: String,
234 pub body_key: String,
236 pub five_elements_class: String,
238 pub five_elements_class_key: String,
240 pub palaces: Vec<PalaceDto>,
242 pub gender_key: String,
244 pub time_index: u8,
246 pub fix_leap: bool,
248 pub language: String,
250 pub config: ConfigDto,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256#[serde(rename_all = "camelCase")]
257pub struct HoroscopeScopeDto {
258 pub index: usize,
260 pub name: String,
262 pub heavenly_stem: String,
264 pub heavenly_stem_key: String,
266 pub earthly_branch: String,
268 pub earthly_branch_key: String,
270 pub palace_names: Vec<String>,
272 pub palace_name_keys: Vec<String>,
274 pub mutagen: Vec<String>,
276 pub mutagen_keys: Vec<String>,
278 #[serde(skip_serializing_if = "Option::is_none")]
280 pub stars: Option<Vec<Vec<StarDto>>>,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285#[serde(rename_all = "camelCase")]
286pub struct AgeDto {
287 #[serde(flatten)]
289 pub base: HoroscopeScopeDto,
290 pub nominal_age: u32,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct YearlyDecStarDto {
297 pub suiqian12: Vec<String>,
299 #[serde(rename = "suiqian12Keys")]
301 pub suiqian12_keys: Vec<String>,
302 pub jiangqian12: Vec<String>,
304 #[serde(rename = "jiangqian12Keys")]
306 pub jiangqian12_keys: Vec<String>,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
311#[serde(rename_all = "camelCase")]
312pub struct YearlyDto {
313 #[serde(flatten)]
315 pub base: HoroscopeScopeDto,
316 pub yearly_dec_star: YearlyDecStarDto,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
322#[serde(rename_all = "camelCase")]
323pub struct HoroscopeDto {
324 pub lunar_date: String,
326 pub solar_date: String,
328 pub decadal: HoroscopeScopeDto,
330 pub age: AgeDto,
332 pub yearly: YearlyDto,
334 pub monthly: HoroscopeScopeDto,
336 pub daily: HoroscopeScopeDto,
338 pub hourly: HoroscopeScopeDto,
340}
341
342pub(crate) fn error_json(err: &BridgeError) -> String {
351 #[derive(Serialize)]
352 struct ErrorJson<'a> {
353 error: &'a str,
354 code: &'a str,
355 }
356 serde_json::to_string(&ErrorJson {
357 error: &err.message,
358 code: err.code,
359 })
360 .expect("错误 JSON 只含两个字符串字段,序列化不会失败")
361}
362
363pub(crate) fn panic_message(panic: &(dyn std::any::Any + Send)) -> &str {
366 panic
367 .downcast_ref::<String>()
368 .map(String::as_str)
369 .or_else(|| panic.downcast_ref::<&str>().copied())
370 .unwrap_or("computation panicked")
371}
372
373#[derive(Debug, Default, Deserialize)]
375#[serde(rename_all = "camelCase", default)]
376struct ConfigPatch {
377 year_divide: Option<String>,
378 horoscope_divide: Option<String>,
379 age_divide: Option<String>,
380 day_divide: Option<String>,
381 algorithm: Option<String>,
382 astro_type: Option<String>,
383 mutagens: Option<std::collections::HashMap<String, Vec<String>>>,
385 brightness: Option<std::collections::HashMap<String, Vec<String>>>,
387}
388
389pub fn parse_config_json(json: Option<&str>) -> Result<Config, BridgeError> {
395 let json = match json {
396 None => return Ok(Config::default()),
397 Some(s) if s.trim().is_empty() => return Ok(Config::default()),
398 Some(s) => s,
399 };
400 let patch: ConfigPatch = serde_json::from_str(json)
401 .map_err(|e| BridgeError::invalid_argument(format!("invalid config JSON: {e}")))?;
402 config_from_patch(patch)
403}
404
405pub fn parse_config_value(value: &serde_json::Value) -> Result<Config, BridgeError> {
411 let patch: ConfigPatch = serde_json::from_value(value.clone())
412 .map_err(|e| BridgeError::invalid_argument(format!("invalid config: {e}")))?;
413 config_from_patch(patch)
414}
415
416fn bad_switch(field: &str, value: &str, expected: &str) -> BridgeError {
418 BridgeError::invalid_argument(format!("invalid {field} '{value}': expected {expected}"))
419}
420
421fn config_from_patch(patch: ConfigPatch) -> Result<Config, BridgeError> {
422 let mut config = Config::default();
423 if let Some(v) = patch.year_divide {
424 config.year_divide = YearDivide::from_key(&v)
425 .ok_or_else(|| bad_switch("yearDivide", &v, "'normal' or 'exact'"))?;
426 }
427 if let Some(v) = patch.horoscope_divide {
428 config.horoscope_divide = HoroscopeDivide::from_key(&v)
429 .ok_or_else(|| bad_switch("horoscopeDivide", &v, "'normal' or 'exact'"))?;
430 }
431 if let Some(v) = patch.age_divide {
432 config.age_divide = AgeDivide::from_key(&v)
433 .ok_or_else(|| bad_switch("ageDivide", &v, "'normal' or 'birthday'"))?;
434 }
435 if let Some(v) = patch.day_divide {
436 config.day_divide = DayDivide::from_key(&v)
437 .ok_or_else(|| bad_switch("dayDivide", &v, "'forward' or 'current'"))?;
438 }
439 if let Some(v) = patch.algorithm {
440 config.algorithm = Algorithm::from_key(&v)
441 .ok_or_else(|| bad_switch("algorithm", &v, "'default' or 'zhongzhou'"))?;
442 }
443 if let Some(v) = patch.astro_type {
444 config.astro_type = AstroType::from_key(&v)
445 .ok_or_else(|| bad_switch("astroType", &v, "'heaven', 'earth' or 'human'"))?;
446 }
447
448 if let Some(map) = patch.mutagens {
450 for (stem_key, star_keys) in map {
451 let stem = HeavenlyStem::from_key(&stem_key).ok_or_else(|| {
452 BridgeError::invalid_argument(format!(
453 "invalid mutagens key '{stem_key}': unknown heavenly stem"
454 ))
455 })?;
456 if star_keys.len() != 4 {
457 return Err(BridgeError::invalid_argument(format!(
458 "invalid mutagens for '{stem_key}': expected 4 stars (lu, quan, ke, ji), got {}",
459 star_keys.len()
460 )));
461 }
462 let mut stars = [StarKey::ZiweiMaj; 4];
463 for (i, key) in star_keys.iter().enumerate() {
464 stars[i] = StarKey::from_key(key).ok_or_else(|| {
465 BridgeError::invalid_argument(format!(
466 "invalid mutagens for '{stem_key}': unknown star '{key}'"
467 ))
468 })?;
469 }
470 config = config.with_mutagens(stem, stars);
471 }
472 }
473 if let Some(map) = patch.brightness {
474 for (star_key, brightness_keys) in map {
475 let star = StarKey::from_key(&star_key).ok_or_else(|| {
476 BridgeError::invalid_argument(format!(
477 "invalid brightness key '{star_key}': unknown star"
478 ))
479 })?;
480 if brightness_keys.len() != 12 {
481 return Err(BridgeError::invalid_argument(format!(
482 "invalid brightness for '{star_key}': expected 12 entries, got {}",
483 brightness_keys.len()
484 )));
485 }
486 let mut table = [None; 12];
487 for (i, key) in brightness_keys.iter().enumerate() {
488 if key.is_empty() {
490 continue;
491 }
492 table[i] = Some(Brightness::from_key(key).ok_or_else(|| {
493 BridgeError::invalid_argument(format!(
494 "invalid brightness for '{star_key}': unknown brightness '{key}'"
495 ))
496 })?);
497 }
498 config = config.with_brightness(star, table);
499 }
500 }
501
502 Ok(config)
503}
504
505impl From<Config> for ConfigDto {
506 fn from(c: Config) -> Self {
507 ConfigDto {
508 year_divide: c.year_divide.as_key().to_string(),
509 horoscope_divide: c.horoscope_divide.as_key().to_string(),
510 age_divide: c.age_divide.as_key().to_string(),
511 day_divide: c.day_divide.as_key().to_string(),
512 algorithm: c.algorithm.as_key().to_string(),
513 astro_type: c.astro_type.as_key().to_string(),
514 }
515 }
516}
517
518fn is_mutagen_candidate(key: crate::data::stars::StarKey) -> bool {
525 use crate::data::stars::StarKey::*;
526 matches!(
527 key,
528 ZiweiMaj
529 | TianjiMaj
530 | TaiyangMaj
531 | WuquMaj
532 | TiantongMaj
533 | LianzhenMaj
534 | TianfuMaj
535 | TaiyinMaj
536 | TanlangMaj
537 | JumenMaj
538 | TianxiangMaj
539 | TianliangMaj
540 | QishaMaj
541 | PojunMaj
542 | WenchangMin
543 | WenquMin
544 | ZuofuMin
545 | YoubiMin
546 )
547}
548
549fn primary_star_dto(s: &Star, lang: Language) -> StarDto {
552 StarDto {
553 key: s.key.as_key().to_string(),
554 name: s.name.clone(),
555 star_type: StarType::as_key(s.star_type).to_string(),
556 scope: Scope::as_key(s.scope).to_string(),
557 brightness: Some(
558 s.brightness
559 .map(|b| translate_brightness(b, lang).to_string())
560 .unwrap_or_default(),
561 ),
562 brightness_key: s.brightness.map(|b| b.as_key().to_string()),
563 mutagen: if is_mutagen_candidate(s.key) {
564 Some(
565 s.mutagen
566 .map(|m| translate_mutagen(m, lang).to_string())
567 .unwrap_or_default(),
568 )
569 } else {
570 None
571 },
572 mutagen_key: s.mutagen.map(|m| m.as_key().to_string()),
573 }
574}
575
576fn bare_star_dto(s: &Star) -> StarDto {
578 StarDto {
579 key: s.key.as_key().to_string(),
580 name: s.name.clone(),
581 star_type: StarType::as_key(s.star_type).to_string(),
582 scope: Scope::as_key(s.scope).to_string(),
583 brightness: None,
584 brightness_key: None,
585 mutagen: None,
586 mutagen_key: None,
587 }
588}
589
590fn pillar(p: (HeavenlyStem, EarthlyBranch), lang: Language) -> [String; 2] {
591 [
592 translate_heavenly_stem(p.0, lang).to_string(),
593 translate_earthly_branch(p.1, lang).to_string(),
594 ]
595}
596
597fn pillar_keys(p: (HeavenlyStem, EarthlyBranch)) -> [String; 2] {
599 [p.0.as_key().to_string(), p.1.as_key().to_string()]
600}
601
602fn palace_dto(p: &PalaceData, lang: Language) -> PalaceDto {
603 PalaceDto {
604 index: p.index,
605 name: translate_palace(p.name, lang).to_string(),
606 name_key: p.name.as_key().to_string(),
607 is_body_palace: p.is_body_palace,
608 is_original_palace: p.is_original_palace,
609 heavenly_stem: translate_heavenly_stem(p.heavenly_stem, lang).to_string(),
610 heavenly_stem_key: p.heavenly_stem.as_key().to_string(),
611 earthly_branch: translate_earthly_branch(p.earthly_branch, lang).to_string(),
612 earthly_branch_key: p.earthly_branch.as_key().to_string(),
613 major_stars: p
614 .major_stars
615 .iter()
616 .map(|s| primary_star_dto(s, lang))
617 .collect(),
618 minor_stars: p
619 .minor_stars
620 .iter()
621 .map(|s| primary_star_dto(s, lang))
622 .collect(),
623 adjective_stars: p.adjective_stars.iter().map(bare_star_dto).collect(),
624 changsheng12: translate_star(p.changsheng12, lang).to_string(),
625 changsheng12_key: p.changsheng12.as_key().to_string(),
626 boshi12: translate_star(p.boshi12, lang).to_string(),
627 boshi12_key: p.boshi12.as_key().to_string(),
628 jiangqian12: translate_star(p.jiangqian12, lang).to_string(),
629 jiangqian12_key: p.jiangqian12.as_key().to_string(),
630 suiqian12: translate_star(p.suiqian12, lang).to_string(),
631 suiqian12_key: p.suiqian12.as_key().to_string(),
632 decadal: DecadalDto {
633 range: [p.decadal.range.0, p.decadal.range.1],
634 heavenly_stem: translate_heavenly_stem(p.decadal.heavenly_stem, lang).to_string(),
635 heavenly_stem_key: p.decadal.heavenly_stem.as_key().to_string(),
636 earthly_branch: translate_earthly_branch(p.decadal.earthly_branch, lang).to_string(),
637 earthly_branch_key: p.decadal.earthly_branch.as_key().to_string(),
638 },
639 ages: p.ages.clone(),
640 mutagen_star_keys: {
641 let stars = p.mutagen_stars(&MUTAGEN);
642 std::array::from_fn(|i| stars[i].as_key().to_string())
643 },
644 }
645}
646
647impl Astrolabe {
648 pub fn to_dto(&self) -> AstrolabeDto {
650 let lang = self.language;
651 AstrolabeDto {
652 gender: translate_gender(self.gender, lang).to_string(),
653 solar_date: self.solar_date.clone(),
654 lunar_date: self.lunar_date.clone(),
655 chinese_date: self.chinese_date.clone(),
656 raw_dates: RawDatesDto {
657 lunar_date: RawLunarDateDto {
658 lunar_year: self.raw_dates.lunar_date.lunar_year,
659 lunar_month: self.raw_dates.lunar_date.lunar_month,
660 lunar_day: self.raw_dates.lunar_date.lunar_day,
661 is_leap: self.raw_dates.lunar_date.is_leap,
662 },
663 chinese_date: RawChineseDateDto {
665 yearly: pillar(self.raw_dates.chinese_date.yearly, Language::ZhCN),
666 monthly: pillar(self.raw_dates.chinese_date.monthly, Language::ZhCN),
667 daily: pillar(self.raw_dates.chinese_date.daily, Language::ZhCN),
668 hourly: pillar(self.raw_dates.chinese_date.hourly, Language::ZhCN),
669 yearly_keys: pillar_keys(self.raw_dates.chinese_date.yearly),
670 monthly_keys: pillar_keys(self.raw_dates.chinese_date.monthly),
671 daily_keys: pillar_keys(self.raw_dates.chinese_date.daily),
672 hourly_keys: pillar_keys(self.raw_dates.chinese_date.hourly),
673 },
674 },
675 time: self.time.clone(),
676 time_range: self.time_range.clone(),
677 sign: self.sign.clone(),
678 zodiac: self.zodiac.clone(),
679 earthly_branch_of_soul_palace: translate_earthly_branch(
680 self.earthly_branch_of_soul_palace,
681 lang,
682 )
683 .to_string(),
684 earthly_branch_of_soul_palace_key: self
685 .earthly_branch_of_soul_palace
686 .as_key()
687 .to_string(),
688 earthly_branch_of_body_palace: translate_earthly_branch(
689 self.earthly_branch_of_body_palace,
690 lang,
691 )
692 .to_string(),
693 earthly_branch_of_body_palace_key: self
694 .earthly_branch_of_body_palace
695 .as_key()
696 .to_string(),
697 soul: translate_star(self.soul, lang).to_string(),
698 soul_key: self.soul.as_key().to_string(),
699 body: translate_star(self.body, lang).to_string(),
700 body_key: self.body.as_key().to_string(),
701 five_elements_class: translate_five_elements_class(self.five_elements_class, lang)
702 .to_string(),
703 five_elements_class_key: self.five_elements_class.as_key().to_string(),
704 palaces: self.palaces.iter().map(|p| palace_dto(p, lang)).collect(),
705 gender_key: match self.gender {
706 Gender::Male => "male",
707 Gender::Female => "female",
708 }
709 .to_string(),
710 time_index: self.time_index,
711 fix_leap: self.fix_leap,
712 language: lang.as_code().to_string(),
713 config: self.config.clone().into(),
714 }
715 }
716}
717
718fn scope_dto(item: &HoroscopeItem, lang: Language) -> HoroscopeScopeDto {
719 HoroscopeScopeDto {
720 index: item.index,
721 name: item.name.clone(),
722 heavenly_stem: translate_heavenly_stem(item.heavenly_stem, lang).to_string(),
723 heavenly_stem_key: item.heavenly_stem.as_key().to_string(),
724 earthly_branch: translate_earthly_branch(item.earthly_branch, lang).to_string(),
725 earthly_branch_key: item.earthly_branch.as_key().to_string(),
726 palace_names: item
727 .palace_names
728 .iter()
729 .map(|p| translate_palace(*p, lang).to_string())
730 .collect(),
731 palace_name_keys: item
732 .palace_names
733 .iter()
734 .map(|p| p.as_key().to_string())
735 .collect(),
736 mutagen: item
737 .mutagen
738 .iter()
739 .map(|k| translate_star(*k, lang).to_string())
740 .collect(),
741 mutagen_keys: item
742 .mutagen
743 .iter()
744 .map(|k| k.as_key().to_string())
745 .collect(),
746 stars: item.stars.as_ref().map(|groups| {
747 groups
748 .iter()
749 .map(|g| g.iter().map(bare_star_dto).collect())
750 .collect()
751 }),
752 }
753}
754
755impl HoroscopeData {
756 pub fn to_dto(&self, lang: Language) -> HoroscopeDto {
758 HoroscopeDto {
759 lunar_date: self.lunar_date.clone(),
760 solar_date: self.solar_date.clone(),
761 decadal: scope_dto(&self.decadal, lang),
762 age: AgeDto {
763 base: scope_dto(&self.age.base, lang),
764 nominal_age: self.age.nominal_age,
765 },
766 yearly: YearlyDto {
767 base: scope_dto(&self.yearly.base, lang),
768 yearly_dec_star: YearlyDecStarDto {
769 suiqian12: self
770 .yearly
771 .yearly_dec_star
772 .suiqian12
773 .iter()
774 .map(|k| translate_star(*k, lang).to_string())
775 .collect(),
776 suiqian12_keys: self
777 .yearly
778 .yearly_dec_star
779 .suiqian12
780 .iter()
781 .map(|k| k.as_key().to_string())
782 .collect(),
783 jiangqian12: self
784 .yearly
785 .yearly_dec_star
786 .jiangqian12
787 .iter()
788 .map(|k| translate_star(*k, lang).to_string())
789 .collect(),
790 jiangqian12_keys: self
791 .yearly
792 .yearly_dec_star
793 .jiangqian12
794 .iter()
795 .map(|k| k.as_key().to_string())
796 .collect(),
797 },
798 },
799 monthly: scope_dto(&self.monthly, lang),
800 daily: scope_dto(&self.daily, lang),
801 hourly: scope_dto(&self.hourly, lang),
802 }
803 }
804}