use serde::{Deserialize, Serialize};
use crate::data::stars::{MUTAGEN, StarKey};
use crate::data::types::*;
use crate::error::BridgeError;
use crate::i18n::{
translate_brightness, translate_earthly_branch, translate_five_elements_class,
translate_gender, translate_heavenly_stem, translate_mutagen, translate_palace, translate_star,
};
use crate::models::astrolabe::Astrolabe;
use crate::models::horoscope::{HoroscopeData, HoroscopeItem};
use crate::models::palace::PalaceData;
use crate::models::star::Star;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StarDto {
pub key: String,
pub name: String,
#[serde(rename = "type")]
pub star_type: String,
pub scope: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub brightness: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub brightness_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mutagen: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mutagen_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DecadalDto {
pub range: [u32; 2],
pub heavenly_stem: String,
pub heavenly_stem_key: String,
pub earthly_branch: String,
pub earthly_branch_key: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PalaceDto {
pub index: usize,
pub name: String,
pub name_key: String,
pub is_body_palace: bool,
pub is_original_palace: bool,
pub heavenly_stem: String,
pub heavenly_stem_key: String,
pub earthly_branch: String,
pub earthly_branch_key: String,
pub major_stars: Vec<StarDto>,
pub minor_stars: Vec<StarDto>,
pub adjective_stars: Vec<StarDto>,
pub changsheng12: String,
pub changsheng12_key: String,
pub boshi12: String,
pub boshi12_key: String,
pub jiangqian12: String,
pub jiangqian12_key: String,
pub suiqian12: String,
pub suiqian12_key: String,
pub decadal: DecadalDto,
pub ages: Vec<u32>,
pub mutagen_star_keys: [String; 4],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawLunarDateDto {
pub lunar_year: i64,
pub lunar_month: u32,
pub lunar_day: u32,
pub is_leap: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawChineseDateDto {
pub yearly: [String; 2],
pub monthly: [String; 2],
pub daily: [String; 2],
pub hourly: [String; 2],
pub yearly_keys: [String; 2],
pub monthly_keys: [String; 2],
pub daily_keys: [String; 2],
pub hourly_keys: [String; 2],
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RawDatesDto {
pub lunar_date: RawLunarDateDto,
pub chinese_date: RawChineseDateDto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigDto {
pub year_divide: String,
pub horoscope_divide: String,
pub age_divide: String,
pub day_divide: String,
pub algorithm: String,
pub astro_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AstrolabeDto {
pub gender: String,
pub solar_date: String,
pub lunar_date: String,
pub chinese_date: String,
pub raw_dates: RawDatesDto,
pub time: String,
pub time_range: String,
pub sign: String,
pub zodiac: String,
pub earthly_branch_of_soul_palace: String,
pub earthly_branch_of_soul_palace_key: String,
pub earthly_branch_of_body_palace: String,
pub earthly_branch_of_body_palace_key: String,
pub soul: String,
pub soul_key: String,
pub body: String,
pub body_key: String,
pub five_elements_class: String,
pub five_elements_class_key: String,
pub palaces: Vec<PalaceDto>,
pub gender_key: String,
pub time_index: u8,
pub fix_leap: bool,
pub language: String,
pub config: ConfigDto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HoroscopeScopeDto {
pub index: usize,
pub name: String,
pub heavenly_stem: String,
pub heavenly_stem_key: String,
pub earthly_branch: String,
pub earthly_branch_key: String,
pub palace_names: Vec<String>,
pub palace_name_keys: Vec<String>,
pub mutagen: Vec<String>,
pub mutagen_keys: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stars: Option<Vec<Vec<StarDto>>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgeDto {
#[serde(flatten)]
pub base: HoroscopeScopeDto,
pub nominal_age: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct YearlyDecStarDto {
pub suiqian12: Vec<String>,
#[serde(rename = "suiqian12Keys")]
pub suiqian12_keys: Vec<String>,
pub jiangqian12: Vec<String>,
#[serde(rename = "jiangqian12Keys")]
pub jiangqian12_keys: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct YearlyDto {
#[serde(flatten)]
pub base: HoroscopeScopeDto,
pub yearly_dec_star: YearlyDecStarDto,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HoroscopeDto {
pub lunar_date: String,
pub solar_date: String,
pub decadal: HoroscopeScopeDto,
pub age: AgeDto,
pub yearly: YearlyDto,
pub monthly: HoroscopeScopeDto,
pub daily: HoroscopeScopeDto,
pub hourly: HoroscopeScopeDto,
}
pub(crate) fn error_json(err: &BridgeError) -> String {
#[derive(Serialize)]
struct ErrorJson<'a> {
error: &'a str,
code: &'a str,
}
serde_json::to_string(&ErrorJson {
error: &err.message,
code: err.code,
})
.expect("错误 JSON 只含两个字符串字段,序列化不会失败")
}
pub(crate) fn panic_message(panic: &(dyn std::any::Any + Send)) -> &str {
panic
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic.downcast_ref::<&str>().copied())
.unwrap_or("computation panicked")
}
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase", default)]
struct ConfigPatch {
year_divide: Option<String>,
horoscope_divide: Option<String>,
age_divide: Option<String>,
day_divide: Option<String>,
algorithm: Option<String>,
astro_type: Option<String>,
mutagens: Option<std::collections::HashMap<String, Vec<String>>>,
brightness: Option<std::collections::HashMap<String, Vec<String>>>,
}
pub fn parse_config_json(json: Option<&str>) -> Result<Config, BridgeError> {
let json = match json {
None => return Ok(Config::default()),
Some(s) if s.trim().is_empty() => return Ok(Config::default()),
Some(s) => s,
};
let patch: ConfigPatch = serde_json::from_str(json)
.map_err(|e| BridgeError::invalid_argument(format!("invalid config JSON: {e}")))?;
config_from_patch(patch)
}
pub fn parse_config_value(value: &serde_json::Value) -> Result<Config, BridgeError> {
let patch: ConfigPatch = serde_json::from_value(value.clone())
.map_err(|e| BridgeError::invalid_argument(format!("invalid config: {e}")))?;
config_from_patch(patch)
}
fn bad_switch(field: &str, value: &str, expected: &str) -> BridgeError {
BridgeError::invalid_argument(format!("invalid {field} '{value}': expected {expected}"))
}
fn config_from_patch(patch: ConfigPatch) -> Result<Config, BridgeError> {
let mut config = Config::default();
if let Some(v) = patch.year_divide {
config.year_divide = YearDivide::from_key(&v)
.ok_or_else(|| bad_switch("yearDivide", &v, "'normal' or 'exact'"))?;
}
if let Some(v) = patch.horoscope_divide {
config.horoscope_divide = HoroscopeDivide::from_key(&v)
.ok_or_else(|| bad_switch("horoscopeDivide", &v, "'normal' or 'exact'"))?;
}
if let Some(v) = patch.age_divide {
config.age_divide = AgeDivide::from_key(&v)
.ok_or_else(|| bad_switch("ageDivide", &v, "'normal' or 'birthday'"))?;
}
if let Some(v) = patch.day_divide {
config.day_divide = DayDivide::from_key(&v)
.ok_or_else(|| bad_switch("dayDivide", &v, "'forward' or 'current'"))?;
}
if let Some(v) = patch.algorithm {
config.algorithm = Algorithm::from_key(&v)
.ok_or_else(|| bad_switch("algorithm", &v, "'default' or 'zhongzhou'"))?;
}
if let Some(v) = patch.astro_type {
config.astro_type = AstroType::from_key(&v)
.ok_or_else(|| bad_switch("astroType", &v, "'heaven', 'earth' or 'human'"))?;
}
if let Some(map) = patch.mutagens {
for (stem_key, star_keys) in map {
let stem = HeavenlyStem::from_key(&stem_key).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid mutagens key '{stem_key}': unknown heavenly stem"
))
})?;
if star_keys.len() != 4 {
return Err(BridgeError::invalid_argument(format!(
"invalid mutagens for '{stem_key}': expected 4 stars (lu, quan, ke, ji), got {}",
star_keys.len()
)));
}
let mut stars = [StarKey::ZiweiMaj; 4];
for (i, key) in star_keys.iter().enumerate() {
stars[i] = StarKey::from_key(key).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid mutagens for '{stem_key}': unknown star '{key}'"
))
})?;
}
config = config.with_mutagens(stem, stars);
}
}
if let Some(map) = patch.brightness {
for (star_key, brightness_keys) in map {
let star = StarKey::from_key(&star_key).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid brightness key '{star_key}': unknown star"
))
})?;
if brightness_keys.len() != 12 {
return Err(BridgeError::invalid_argument(format!(
"invalid brightness for '{star_key}': expected 12 entries, got {}",
brightness_keys.len()
)));
}
let mut table = [None; 12];
for (i, key) in brightness_keys.iter().enumerate() {
if key.is_empty() {
continue;
}
table[i] = Some(Brightness::from_key(key).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid brightness for '{star_key}': unknown brightness '{key}'"
))
})?);
}
config = config.with_brightness(star, table);
}
}
Ok(config)
}
impl From<Config> for ConfigDto {
fn from(c: Config) -> Self {
ConfigDto {
year_divide: c.year_divide.as_key().to_string(),
horoscope_divide: c.horoscope_divide.as_key().to_string(),
age_divide: c.age_divide.as_key().to_string(),
day_divide: c.day_divide.as_key().to_string(),
algorithm: c.algorithm.as_key().to_string(),
astro_type: c.astro_type.as_key().to_string(),
}
}
}
fn is_mutagen_candidate(key: crate::data::stars::StarKey) -> bool {
use crate::data::stars::StarKey::*;
matches!(
key,
ZiweiMaj
| TianjiMaj
| TaiyangMaj
| WuquMaj
| TiantongMaj
| LianzhenMaj
| TianfuMaj
| TaiyinMaj
| TanlangMaj
| JumenMaj
| TianxiangMaj
| TianliangMaj
| QishaMaj
| PojunMaj
| WenchangMin
| WenquMin
| ZuofuMin
| YoubiMin
)
}
fn primary_star_dto(s: &Star, lang: Language) -> StarDto {
StarDto {
key: s.key.as_key().to_string(),
name: s.name.clone(),
star_type: StarType::as_key(s.star_type).to_string(),
scope: Scope::as_key(s.scope).to_string(),
brightness: Some(
s.brightness
.map(|b| translate_brightness(b, lang).to_string())
.unwrap_or_default(),
),
brightness_key: s.brightness.map(|b| b.as_key().to_string()),
mutagen: if is_mutagen_candidate(s.key) {
Some(
s.mutagen
.map(|m| translate_mutagen(m, lang).to_string())
.unwrap_or_default(),
)
} else {
None
},
mutagen_key: s.mutagen.map(|m| m.as_key().to_string()),
}
}
fn bare_star_dto(s: &Star) -> StarDto {
StarDto {
key: s.key.as_key().to_string(),
name: s.name.clone(),
star_type: StarType::as_key(s.star_type).to_string(),
scope: Scope::as_key(s.scope).to_string(),
brightness: None,
brightness_key: None,
mutagen: None,
mutagen_key: None,
}
}
fn pillar(p: (HeavenlyStem, EarthlyBranch), lang: Language) -> [String; 2] {
[
translate_heavenly_stem(p.0, lang).to_string(),
translate_earthly_branch(p.1, lang).to_string(),
]
}
fn pillar_keys(p: (HeavenlyStem, EarthlyBranch)) -> [String; 2] {
[p.0.as_key().to_string(), p.1.as_key().to_string()]
}
fn palace_dto(p: &PalaceData, lang: Language) -> PalaceDto {
PalaceDto {
index: p.index,
name: translate_palace(p.name, lang).to_string(),
name_key: p.name.as_key().to_string(),
is_body_palace: p.is_body_palace,
is_original_palace: p.is_original_palace,
heavenly_stem: translate_heavenly_stem(p.heavenly_stem, lang).to_string(),
heavenly_stem_key: p.heavenly_stem.as_key().to_string(),
earthly_branch: translate_earthly_branch(p.earthly_branch, lang).to_string(),
earthly_branch_key: p.earthly_branch.as_key().to_string(),
major_stars: p
.major_stars
.iter()
.map(|s| primary_star_dto(s, lang))
.collect(),
minor_stars: p
.minor_stars
.iter()
.map(|s| primary_star_dto(s, lang))
.collect(),
adjective_stars: p.adjective_stars.iter().map(bare_star_dto).collect(),
changsheng12: translate_star(p.changsheng12, lang).to_string(),
changsheng12_key: p.changsheng12.as_key().to_string(),
boshi12: translate_star(p.boshi12, lang).to_string(),
boshi12_key: p.boshi12.as_key().to_string(),
jiangqian12: translate_star(p.jiangqian12, lang).to_string(),
jiangqian12_key: p.jiangqian12.as_key().to_string(),
suiqian12: translate_star(p.suiqian12, lang).to_string(),
suiqian12_key: p.suiqian12.as_key().to_string(),
decadal: DecadalDto {
range: [p.decadal.range.0, p.decadal.range.1],
heavenly_stem: translate_heavenly_stem(p.decadal.heavenly_stem, lang).to_string(),
heavenly_stem_key: p.decadal.heavenly_stem.as_key().to_string(),
earthly_branch: translate_earthly_branch(p.decadal.earthly_branch, lang).to_string(),
earthly_branch_key: p.decadal.earthly_branch.as_key().to_string(),
},
ages: p.ages.clone(),
mutagen_star_keys: {
let stars = p.mutagen_stars(&MUTAGEN);
std::array::from_fn(|i| stars[i].as_key().to_string())
},
}
}
impl Astrolabe {
pub fn to_dto(&self) -> AstrolabeDto {
let lang = self.language;
AstrolabeDto {
gender: translate_gender(self.gender, lang).to_string(),
solar_date: self.solar_date.clone(),
lunar_date: self.lunar_date.clone(),
chinese_date: self.chinese_date.clone(),
raw_dates: RawDatesDto {
lunar_date: RawLunarDateDto {
lunar_year: self.raw_dates.lunar_date.lunar_year,
lunar_month: self.raw_dates.lunar_date.lunar_month,
lunar_day: self.raw_dates.lunar_date.lunar_day,
is_leap: self.raw_dates.lunar_date.is_leap,
},
chinese_date: RawChineseDateDto {
yearly: pillar(self.raw_dates.chinese_date.yearly, Language::ZhCN),
monthly: pillar(self.raw_dates.chinese_date.monthly, Language::ZhCN),
daily: pillar(self.raw_dates.chinese_date.daily, Language::ZhCN),
hourly: pillar(self.raw_dates.chinese_date.hourly, Language::ZhCN),
yearly_keys: pillar_keys(self.raw_dates.chinese_date.yearly),
monthly_keys: pillar_keys(self.raw_dates.chinese_date.monthly),
daily_keys: pillar_keys(self.raw_dates.chinese_date.daily),
hourly_keys: pillar_keys(self.raw_dates.chinese_date.hourly),
},
},
time: self.time.clone(),
time_range: self.time_range.clone(),
sign: self.sign.clone(),
zodiac: self.zodiac.clone(),
earthly_branch_of_soul_palace: translate_earthly_branch(
self.earthly_branch_of_soul_palace,
lang,
)
.to_string(),
earthly_branch_of_soul_palace_key: self
.earthly_branch_of_soul_palace
.as_key()
.to_string(),
earthly_branch_of_body_palace: translate_earthly_branch(
self.earthly_branch_of_body_palace,
lang,
)
.to_string(),
earthly_branch_of_body_palace_key: self
.earthly_branch_of_body_palace
.as_key()
.to_string(),
soul: translate_star(self.soul, lang).to_string(),
soul_key: self.soul.as_key().to_string(),
body: translate_star(self.body, lang).to_string(),
body_key: self.body.as_key().to_string(),
five_elements_class: translate_five_elements_class(self.five_elements_class, lang)
.to_string(),
five_elements_class_key: self.five_elements_class.as_key().to_string(),
palaces: self.palaces.iter().map(|p| palace_dto(p, lang)).collect(),
gender_key: match self.gender {
Gender::Male => "male",
Gender::Female => "female",
}
.to_string(),
time_index: self.time_index,
fix_leap: self.fix_leap,
language: lang.as_code().to_string(),
config: self.config.clone().into(),
}
}
}
fn scope_dto(item: &HoroscopeItem, lang: Language) -> HoroscopeScopeDto {
HoroscopeScopeDto {
index: item.index,
name: item.name.clone(),
heavenly_stem: translate_heavenly_stem(item.heavenly_stem, lang).to_string(),
heavenly_stem_key: item.heavenly_stem.as_key().to_string(),
earthly_branch: translate_earthly_branch(item.earthly_branch, lang).to_string(),
earthly_branch_key: item.earthly_branch.as_key().to_string(),
palace_names: item
.palace_names
.iter()
.map(|p| translate_palace(*p, lang).to_string())
.collect(),
palace_name_keys: item
.palace_names
.iter()
.map(|p| p.as_key().to_string())
.collect(),
mutagen: item
.mutagen
.iter()
.map(|k| translate_star(*k, lang).to_string())
.collect(),
mutagen_keys: item
.mutagen
.iter()
.map(|k| k.as_key().to_string())
.collect(),
stars: item.stars.as_ref().map(|groups| {
groups
.iter()
.map(|g| g.iter().map(bare_star_dto).collect())
.collect()
}),
}
}
impl HoroscopeData {
pub fn to_dto(&self, lang: Language) -> HoroscopeDto {
HoroscopeDto {
lunar_date: self.lunar_date.clone(),
solar_date: self.solar_date.clone(),
decadal: scope_dto(&self.decadal, lang),
age: AgeDto {
base: scope_dto(&self.age.base, lang),
nominal_age: self.age.nominal_age,
},
yearly: YearlyDto {
base: scope_dto(&self.yearly.base, lang),
yearly_dec_star: YearlyDecStarDto {
suiqian12: self
.yearly
.yearly_dec_star
.suiqian12
.iter()
.map(|k| translate_star(*k, lang).to_string())
.collect(),
suiqian12_keys: self
.yearly
.yearly_dec_star
.suiqian12
.iter()
.map(|k| k.as_key().to_string())
.collect(),
jiangqian12: self
.yearly
.yearly_dec_star
.jiangqian12
.iter()
.map(|k| translate_star(*k, lang).to_string())
.collect(),
jiangqian12_keys: self
.yearly
.yearly_dec_star
.jiangqian12
.iter()
.map(|k| k.as_key().to_string())
.collect(),
},
},
monthly: scope_dto(&self.monthly, lang),
daily: scope_dto(&self.daily, lang),
hourly: scope_dto(&self.hourly, lang),
}
}
}