use serde::Deserialize;
use serde_json::{Value, json};
use crate::data::constants::*;
use crate::data::earthly_branches::get_earthly_branch_info;
use crate::data::heavenly_stems::get_heavenly_stem_info;
use crate::data::stars::{STARS_WITH_INFO, StarKey, get_star_info};
use crate::data::types::*;
use crate::dto::parse_config_value;
use crate::error::BridgeError;
use crate::i18n::lookup::{key_of, key_of_in, translate_key};
use crate::models::astrolabe::Astrolabe;
use crate::models::star::Star;
use crate::star::decorative::{get_changsheng12_start_index, get_jiangqian12_start_index};
use crate::star::query::{self as star_query, StarParam};
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct SolarChartInput {
pub solar_date: String,
pub time_index: u8,
pub gender: String,
pub fix_leap: bool,
pub language: String,
pub config: Option<Value>,
pub from_stem: Option<String>,
pub from_branch: Option<String>,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct LunarChartInput {
pub lunar_date: String,
pub time_index: u8,
pub gender: String,
pub is_leap_month: bool,
pub fix_leap: bool,
pub language: String,
pub config: Option<Value>,
pub from_stem: Option<String>,
pub from_branch: Option<String>,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct HoroscopeInput {
pub solar_date: String,
pub time_index: u8,
pub gender: String,
pub fix_leap: bool,
pub language: String,
pub config: Option<Value>,
pub target_date: String,
pub target_time_index: u8,
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "camelCase", default)]
pub struct QueryInput {
pub kind: String,
pub solar_date: String,
pub lunar_date: String,
pub time_index: u8,
pub is_leap_month: bool,
pub fix_leap: bool,
pub language: String,
pub config: Option<Value>,
pub gender: String,
pub index: i32,
#[serde(default = "default_max")]
pub max: i32,
pub hour: u8,
pub star_key: String,
pub stem_key: String,
pub branch_key: String,
pub scope: String,
pub month_index: usize,
pub soul_index: usize,
pub lunar_month: u32,
pub lunar_day: u32,
pub is_leap: bool,
pub five_elements_class: String,
pub pillars: Vec<Vec<String>>,
pub from_stem: Option<String>,
pub from_branch: Option<String>,
pub key: String,
pub text: String,
pub key_filter: String,
pub target_date: String,
pub target_time_index: u8,
}
fn default_max() -> i32 {
12
}
fn serialize_failed(e: serde_json::Error) -> BridgeError {
BridgeError::internal(format!("failed to serialize result: {e}"))
}
fn parse_gender(s: &str) -> Result<Gender, BridgeError> {
match s.to_ascii_lowercase().as_str() {
"male" => Ok(Gender::Male),
"female" => Ok(Gender::Female),
_ => Err(BridgeError::invalid_argument(format!(
"invalid gender '{s}': expected 'male' or 'female'"
))),
}
}
fn parse_language(s: &str) -> Result<Language, BridgeError> {
Language::from_code(s).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid language '{s}': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN"
))
})
}
fn parse_language_or_default(s: &str) -> Result<Language, BridgeError> {
if s.is_empty() {
Ok(Language::ZhCN)
} else {
parse_language(s)
}
}
fn parse_config(v: &Option<Value>) -> Result<Config, BridgeError> {
match v {
None => Ok(Config::default()),
Some(Value::Null) => Ok(Config::default()),
Some(value) => parse_config_value(value),
}
}
fn parse_star_key(key: &str) -> Result<StarKey, BridgeError> {
StarKey::from_key(key)
.ok_or_else(|| BridgeError::invalid_argument(format!("unknown star key '{key}'")))
}
fn parse_stem_key(key: &str) -> Result<HeavenlyStem, BridgeError> {
HeavenlyStem::from_key(key)
.ok_or_else(|| BridgeError::invalid_argument(format!("unknown heavenly stem key '{key}'")))
}
fn parse_branch_key(key: &str) -> Result<EarthlyBranch, BridgeError> {
EarthlyBranch::from_key(key)
.ok_or_else(|| BridgeError::invalid_argument(format!("unknown earthly branch key '{key}'")))
}
fn parse_scope_key(key: &str) -> Result<Scope, BridgeError> {
Scope::from_key(key)
.ok_or_else(|| BridgeError::invalid_argument(format!("unknown scope '{key}'")))
}
fn parse_pillars(
pillars: &[Vec<String>],
) -> Result<[(HeavenlyStem, EarthlyBranch); 4], BridgeError> {
if pillars.len() != 4 {
return Err(BridgeError::invalid_argument(format!(
"invalid pillars: expected 4 entries (yearly, monthly, daily, hourly), got {}",
pillars.len()
)));
}
let mut out = [(HeavenlyStem::Jia, EarthlyBranch::Zi); 4];
for (i, pillar) in pillars.iter().enumerate() {
if pillar.len() != 2 {
return Err(BridgeError::invalid_argument(format!(
"invalid pillars[{i}]: expected 2 entries (stem, branch), got {}",
pillar.len()
)));
}
out[i] = (parse_stem_key(&pillar[0])?, parse_branch_key(&pillar[1])?);
}
Ok(out)
}
fn parse_from(
stem: &Option<String>,
branch: &Option<String>,
) -> Result<Option<(HeavenlyStem, EarthlyBranch)>, BridgeError> {
match (stem, branch) {
(None, None) => Ok(None),
(Some(s), Some(b)) => Ok(Some((
HeavenlyStem::from_key(s).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid fromStem '{s}': unknown heavenly stem"
))
})?,
EarthlyBranch::from_key(b).ok_or_else(|| {
BridgeError::invalid_argument(format!(
"invalid fromBranch '{b}': unknown earthly branch"
))
})?,
))),
_ => Err(BridgeError::invalid_argument(
"invalid rearrange target: fromStem and fromBranch must be given together",
)),
}
}
fn apply_rearrange(
astrolabe: Astrolabe,
stem: &Option<String>,
branch: &Option<String>,
) -> Result<Astrolabe, BridgeError> {
match parse_from(stem, branch)? {
None => Ok(astrolabe),
Some((s, b)) => Ok(astrolabe.rearranged(s, b)),
}
}
pub fn by_solar(input: &SolarChartInput) -> Result<Value, BridgeError> {
let astrolabe = crate::by_solar(
&input.solar_date,
input.time_index,
parse_gender(&input.gender)?,
input.fix_leap,
parse_language(&input.language)?,
parse_config(&input.config)?,
)?;
let astrolabe = apply_rearrange(astrolabe, &input.from_stem, &input.from_branch)?;
serde_json::to_value(astrolabe.to_dto()).map_err(serialize_failed)
}
pub fn by_lunar(input: &LunarChartInput) -> Result<Value, BridgeError> {
let astrolabe = crate::by_lunar(
&input.lunar_date,
input.time_index,
parse_gender(&input.gender)?,
LeapMonth::from_flags(input.is_leap_month, input.fix_leap),
parse_language(&input.language)?,
parse_config(&input.config)?,
)?;
let astrolabe = apply_rearrange(astrolabe, &input.from_stem, &input.from_branch)?;
serde_json::to_value(astrolabe.to_dto()).map_err(serialize_failed)
}
pub fn horoscope(input: &HoroscopeInput) -> Result<Value, BridgeError> {
let language = parse_language(&input.language)?;
let astrolabe = crate::by_solar(
&input.solar_date,
input.time_index,
parse_gender(&input.gender)?,
input.fix_leap,
language,
parse_config(&input.config)?,
)?;
let horoscope = crate::get_horoscope(
&astrolabe,
&input.target_date,
input.target_time_index,
language,
)?;
serde_json::to_value(horoscope.to_dto(language)).map_err(serialize_failed)
}
pub fn query(input: &QueryInput) -> Result<Value, BridgeError> {
match input.kind.as_str() {
"zodiacBySolar" | "signBySolar" | "signByLunar" | "majorStarBySolar"
| "majorStarByLunar" | "astrolabeToPrompt" | "horoscopeToPrompt" => translated(input),
"fixIndex"
| "earthlyBranchToPalaceIndex"
| "timeToIndex"
| "getAgeIndex"
| "getBrightness"
| "getMutagen"
| "getMutagensByHeavenlyStem"
| "getSoulAndBody"
| "getFiveElementsClass"
| "getPalaceNames"
| "getHoroscope"
| "fixLunarMonthIndex"
| "fixLunarDayIndex"
| "translateChineseDate" => util(input),
k if k.starts_with("get") && is_star_kind(k) => star(input),
"starsInfo" | "heavenlyStems" | "earthlyBranches" | "constants" => {
Ok(data(input.kind.as_str()))
}
"translate" => Ok(json!(translate_key(
&input.key,
parse_language_or_default(&input.language)?
))),
"allKeys" => Ok(json!(crate::i18n::lookup::all_keys())),
"keyOf" => Ok(json!(if input.key_filter.is_empty() {
key_of(&input.text)
} else {
key_of_in(&input.text, &input.key_filter)
})),
other => Err(BridgeError::invalid_argument(format!(
"unknown query kind '{other}'"
))),
}
}
fn is_star_kind(kind: &str) -> bool {
matches!(
kind,
"getStartIndex"
| "getLuYangTuoMaIndex"
| "getKuiYueIndex"
| "getChangQuIndex"
| "getKongJieIndex"
| "getTimelyStarIndex"
| "getLuanXiIndex"
| "getDailyStarIndex"
| "getMonthlyStarIndex"
| "getYearlyStarIndex"
| "getMajorStar"
| "getMinorStar"
| "getAdjectiveStar"
| "getChangsheng12"
| "getBoShi12"
| "getYearly12"
| "getHoroscopeStar"
| "getChangesheng12StartIndex"
| "getJiangqian12StartIndex"
| "getChangQuIndexByHeavenlyStem"
| "getHuoLingIndex"
| "getZuoYouIndex"
| "getHuagaiXianchiIndex"
| "getGuGuaIndex"
| "getJieshaAdjIndex"
| "getDahaoIndex"
| "getNianjieIndex"
| "getTianshiTianshangIndex"
)
}
fn translated(input: &QueryInput) -> Result<Value, BridgeError> {
let language = parse_language(&input.language)?;
let config = parse_config(&input.config)?;
let text = match input.kind.as_str() {
"zodiacBySolar" => crate::get_zodiac_by_solar_date(&input.solar_date, language, config),
"signBySolar" => crate::get_sign_by_solar_date(&input.solar_date, language),
"signByLunar" => {
crate::get_sign_by_lunar_date(&input.lunar_date, input.is_leap_month, language)
}
"majorStarBySolar" => crate::get_major_star_by_solar_date(
&input.solar_date,
input.time_index,
input.fix_leap,
language,
config,
),
"majorStarByLunar" => crate::get_major_star_by_lunar_date(
&input.lunar_date,
input.time_index,
LeapMonth::from_flags(input.is_leap_month, input.fix_leap),
language,
config,
),
"astrolabeToPrompt" => crate::by_solar(
&input.solar_date,
input.time_index,
parse_gender(&input.gender)?,
input.fix_leap,
language,
config,
)
.map(|a| crate::astrolabe_to_prompt(&a, language)),
"horoscopeToPrompt" => crate::by_solar(
&input.solar_date,
input.time_index,
parse_gender(&input.gender)?,
input.fix_leap,
language,
config,
)
.and_then(|a| {
crate::get_horoscope(&a, &input.target_date, input.target_time_index, language)
.map(|h| crate::horoscope_to_prompt(&a, &h, language))
}),
other => {
return Err(BridgeError::invalid_argument(format!(
"unknown query kind '{other}'"
)));
}
};
Ok(json!(text?))
}
fn util(input: &QueryInput) -> Result<Value, BridgeError> {
let config = parse_config(&input.config)?;
let value = match input.kind.as_str() {
"fixIndex" => {
if input.max <= 0 {
return Err(BridgeError::invalid_argument(format!(
"invalid max '{}': expected a positive integer",
input.max
)));
}
json!(crate::utils::fix_index(input.index, input.max))
}
"earthlyBranchToPalaceIndex" => json!(crate::utils::earthly_branch_to_palace_index(
parse_branch_key(&input.branch_key)?
)),
"timeToIndex" => {
if input.hour > 23 {
return Err(BridgeError::invalid_argument(format!(
"invalid hour '{}': expected 0-23",
input.hour
)));
}
json!(crate::utils::time_to_index(input.hour))
}
"getAgeIndex" => json!(crate::utils::get_age_index(parse_branch_key(
&input.branch_key
)?)),
"getBrightness" => json!(
crate::utils::get_brightness(parse_star_key(&input.star_key)?, input.index, &config)
.map(|b| b.as_key())
),
"getMutagen" => json!(
crate::utils::get_mutagen(
parse_star_key(&input.star_key)?,
parse_stem_key(&input.stem_key)?,
&config
)
.map(|m| m.as_key())
),
"getMutagensByHeavenlyStem" => json!(
crate::utils::get_mutagens_by_heavenly_stem(parse_stem_key(&input.stem_key)?, &config)
.iter()
.map(|s| s.as_key())
.collect::<Vec<_>>()
),
"getSoulAndBody" => {
if input.time_index > 12 {
return Err(BridgeError::invalid_argument(format!(
"invalid timeIndex '{}': expected 0-12",
input.time_index
)));
}
let r = crate::get_soul_and_body(
input.month_index,
input.time_index,
parse_stem_key(&input.stem_key)?,
);
json!({
"soulIndex": r.soul_index,
"bodyIndex": r.body_index,
"heavenlyStemOfSoul": r.heavenly_stem_of_soul.as_key(),
"earthlyBranchOfSoul": r.earthly_branch_of_soul.as_key(),
})
}
"getFiveElementsClass" => json!(
crate::get_five_elements_class(
parse_stem_key(&input.stem_key)?,
parse_branch_key(&input.branch_key)?
)
.as_key()
),
"getPalaceNames" => json!(
crate::get_palace_names(input.soul_index)
.iter()
.map(|p| p.as_key())
.collect::<Vec<_>>()
),
"getHoroscope" => {
let (decadals, ages) = crate::astro::palace::get_decadals_and_ages(
input.soul_index,
parse_five_elements_class(&input.five_elements_class)?,
parse_gender(&input.gender)?,
parse_stem_key(&input.stem_key)?,
parse_branch_key(&input.branch_key)?,
);
let language = parse_language_or_default(&input.language)?;
json!({
"decadals": decadals.iter().map(|d| json!({
"range": d.range,
"heavenlyStem": crate::i18n::translate_heavenly_stem(d.heavenly_stem, language),
"heavenlyStemKey": d.heavenly_stem.as_key(),
"earthlyBranch": crate::i18n::translate_earthly_branch(d.earthly_branch, language),
"earthlyBranchKey": d.earthly_branch.as_key(),
})).collect::<Vec<_>>(),
"ages": ages,
})
}
"fixLunarMonthIndex" => json!(crate::astro::builder::fix_lunar_month_index(
input.lunar_month,
input.lunar_day,
input.is_leap,
input.time_index,
input.fix_leap,
)),
"fixLunarDayIndex" => json!(crate::astro::builder::fix_lunar_day_index(
input.lunar_day,
input.time_index
)),
"translateChineseDate" => json!(crate::utils::translate_chinese_date(
parse_pillars(&input.pillars)?,
parse_language(&input.language)?
)),
other => {
return Err(BridgeError::invalid_argument(format!(
"unknown query kind '{other}'"
)));
}
};
Ok(value)
}
fn parse_five_elements_class(key: &str) -> Result<FiveElementsClass, BridgeError> {
FiveElementsClass::from_key(key).ok_or_else(|| {
BridgeError::invalid_argument(format!("unknown five elements class '{key}'"))
})
}
fn star_groups(groups: &[Vec<Star>; 12]) -> Value {
json!(
groups
.iter()
.map(|palace| palace
.iter()
.map(|s| json!({
"key": s.key.as_key(),
"name": s.name,
"type": s.star_type.as_key(),
"scope": s.scope.as_key(),
}))
.collect::<Vec<_>>())
.collect::<Vec<_>>()
)
}
fn shen_keys(shen: &[StarKey; 12]) -> Value {
json!(shen.iter().map(|k| k.as_key()).collect::<Vec<_>>())
}
fn star(input: &QueryInput) -> Result<Value, BridgeError> {
let config = parse_config(&input.config)?;
let language = parse_language_or_default(&input.language)?;
match input.kind.as_str() {
"getChangesheng12StartIndex" => {
return Ok(json!(get_changsheng12_start_index(
parse_five_elements_class(&input.five_elements_class)?
)));
}
"getJiangqian12StartIndex" => {
return Ok(json!(get_jiangqian12_start_index(parse_branch_key(
&input.branch_key
)?)));
}
"getHoroscopeStar" => {
return Ok(star_groups(&crate::astro::horoscope::get_horoscope_stars(
parse_stem_key(&input.stem_key)?,
parse_branch_key(&input.branch_key)?,
parse_scope_key(&input.scope)?,
language,
)));
}
"getChangQuIndexByHeavenlyStem" => {
let r =
crate::star::location::get_chang_qu_index_by_stem(parse_stem_key(&input.stem_key)?);
return Ok(json!({ "changIndex": r.chang, "quIndex": r.qu }));
}
"getZuoYouIndex" => {
let r = crate::star::location::get_zuo_you_index(input.lunar_month);
return Ok(json!({ "zuoIndex": r.zuo, "youIndex": r.you }));
}
"getHuoLingIndex" => {
let r = crate::star::location::get_huo_ling_index(
parse_branch_key(&input.branch_key)?,
input.time_index,
);
return Ok(json!({ "huoIndex": r.huo, "lingIndex": r.ling }));
}
"getHuagaiXianchiIndex" => {
let r = crate::star::location::get_huagai_xianchi_index(parse_branch_key(
&input.branch_key,
)?);
return Ok(json!({ "huagaiIndex": r.huagai, "xianchiIndex": r.xianchi }));
}
"getGuGuaIndex" => {
let r = crate::star::location::get_gu_gua_index(parse_branch_key(&input.branch_key)?);
return Ok(json!({ "guchenIndex": r.guchen, "guasuIndex": r.guasu }));
}
"getJieshaAdjIndex" => {
return Ok(json!(crate::star::location::get_jiesha_adj_index(
parse_branch_key(&input.branch_key)?
)));
}
"getDahaoIndex" => {
return Ok(json!(crate::star::location::get_dahao_index(
parse_branch_key(&input.branch_key)?
)));
}
"getNianjieIndex" => {
return Ok(json!(crate::star::location::get_nianjie_index(
parse_branch_key(&input.branch_key)?
)));
}
"getTianshiTianshangIndex" => {
let (shang, shi) = crate::star::location::get_tianshang_tianshi_index(
parse_gender(&input.gender)?,
parse_branch_key(&input.branch_key)?,
input.soul_index,
config.algorithm,
);
return Ok(json!({ "tianshangIndex": shang, "tianshiIndex": shi }));
}
_ => {}
}
let param = StarParam {
solar_date: &input.solar_date,
time_index: input.time_index,
gender: parse_gender(&input.gender)?,
fix_leap: input.fix_leap,
from: parse_from(&input.from_stem, &input.from_branch)?,
language,
config: &config,
};
let value = match input.kind.as_str() {
"getStartIndex" => {
let r = star_query::get_start_index(¶m)?;
json!({ "ziweiIndex": r.ziwei, "tianfuIndex": r.tianfu })
}
"getLuYangTuoMaIndex" => {
let r = star_query::get_lu_yang_tuo_ma_index(¶m)?;
json!({ "luIndex": r.lu, "yangIndex": r.yang, "tuoIndex": r.tuo, "maIndex": r.ma })
}
"getKuiYueIndex" => {
let r = star_query::get_kui_yue_index(¶m)?;
json!({ "kuiIndex": r.kui, "yueIndex": r.yue })
}
"getChangQuIndex" => {
let r = star_query::get_chang_qu_index(¶m)?;
json!({ "changIndex": r.chang, "quIndex": r.qu })
}
"getKongJieIndex" => {
let r = star_query::get_kong_jie_index(¶m)?;
json!({ "kongIndex": r.kong, "jieIndex": r.jie })
}
"getTimelyStarIndex" => {
let r = star_query::get_timely_star_index(¶m)?;
json!({ "taifuIndex": r.taifu, "fenggaoIndex": r.fenggao })
}
"getLuanXiIndex" => {
let r = star_query::get_luan_xi_index(¶m)?;
json!({ "hongluanIndex": r.hongluan, "tianxiIndex": r.tianxi })
}
"getDailyStarIndex" => {
let r = star_query::get_daily_star_index(¶m)?;
json!({
"santaiIndex": r.santai,
"bazuoIndex": r.bazuo,
"enguangIndex": r.enguang,
"tianguiIndex": r.tiangui,
})
}
"getMonthlyStarIndex" => {
let r = star_query::get_monthly_star_index(¶m)?;
json!({
"yuejieIndex": r.jieshen,
"tianyaoIndex": r.tianyao,
"tianxingIndex": r.tianxing,
"yinshaIndex": r.yinsha,
"tianyueIndex": r.tianyue,
"tianwuIndex": r.tianwu,
})
}
"getYearlyStarIndex" => {
let r = star_query::get_yearly_star_index(¶m)?;
json!({
"xianchiIndex": r.xianchi,
"huagaiIndex": r.huagai,
"guchenIndex": r.guchen,
"guasuIndex": r.guasu,
"tiancaiIndex": r.tiancai,
"tianshouIndex": r.tianshou,
"tianchuIndex": r.tianchu,
"posuiIndex": r.posui,
"feilianIndex": r.feilian,
"longchiIndex": r.longchi,
"fenggeIndex": r.fengge,
"tiankuIndex": r.tianku,
"tianxuIndex": r.tianxu,
"tianguanIndex": r.tianguan,
"tianfuIndex": r.tianfu,
"tiandeIndex": r.tiande,
"yuedeIndex": r.yuede,
"tiankongIndex": r.tiankong,
"jieluIndex": r.jielu,
"kongwangIndex": r.kongwang,
"xunkongIndex": r.xunkong,
"tianshangIndex": r.tianshang,
"tianshiIndex": r.tianshi,
"jiekongIndex": r.jiekong,
"jieshaAdjIndex": r.jiesha,
"nianjieIndex": r.nianjie,
"dahaoAdjIndex": r.dahao,
})
}
"getMajorStar" => star_groups(&star_query::get_major_stars(¶m)?),
"getMinorStar" => star_groups(&star_query::get_minor_stars(¶m)?),
"getAdjectiveStar" => star_groups(&star_query::get_adjective_stars(¶m)?),
"getChangsheng12" => shen_keys(&star_query::get_changsheng12(¶m)?),
"getBoShi12" => shen_keys(&star_query::get_boshi12(¶m)?),
"getYearly12" => {
let (suiqian12, jiangqian12) = star_query::get_yearly12(¶m)?;
json!({ "suiqian12": shen_keys(&suiqian12), "jiangqian12": shen_keys(&jiangqian12) })
}
other => {
return Err(BridgeError::invalid_argument(format!(
"unknown query kind '{other}'"
)));
}
};
Ok(value)
}
fn data(kind: &str) -> Value {
match kind {
"starsInfo" => json!(
STARS_WITH_INFO
.iter()
.map(|star| {
let info = get_star_info(*star).expect("STARS_WITH_INFO 内必有记录");
(
star.as_key().to_string(),
json!({
"brightness": info.brightness.iter()
.map(|b| b.map(|b| b.as_key()))
.collect::<Vec<_>>(),
"fiveElements": info.five_elements.map(|f| f.as_str()),
"yinYang": info.yin_yang.map(|y| y.as_str()),
}),
)
})
.collect::<serde_json::Map<_, _>>()
),
"heavenlyStems" => json!(
HEAVENLY_STEMS
.iter()
.map(|stem| {
let info = get_heavenly_stem_info(*stem);
(
stem.as_key().to_string(),
json!({
"yinYang": info.yin_yang.as_str(),
"fiveElements": info.five_elements.as_str(),
"crash": info.crash.map(|c| c.as_key()),
"mutagen": info.mutagen.iter().map(|s| s.as_key()).collect::<Vec<_>>(),
}),
)
})
.collect::<serde_json::Map<_, _>>()
),
"earthlyBranches" => json!(
EARTHLY_BRANCHES
.iter()
.map(|branch| {
let info = get_earthly_branch_info(*branch);
(
branch.as_key().to_string(),
json!({
"yinYang": info.yin_yang.as_str(),
"fiveElements": info.five_elements.as_str(),
"crash": info.crash.as_key(),
"soul": info.soul.as_key(),
"body": info.body.as_key(),
"inside": info.inside,
"outside": info.outside,
"healthTip": info.health_tip,
}),
)
})
.collect::<serde_json::Map<_, _>>()
),
_ => json!({
"LANGUAGES": LANGUAGES,
"HEAVENLY_STEMS": HEAVENLY_STEMS.iter().map(|s| s.as_key()).collect::<Vec<_>>(),
"EARTHLY_BRANCHES": EARTHLY_BRANCHES.iter().map(|b| b.as_key()).collect::<Vec<_>>(),
"ZODIAC": ZODIAC,
"SIGNS": SIGNS,
"PALACES": PALACES.iter().map(|p| p.as_key()).collect::<Vec<_>>(),
"GENDER": {
"male": Gender::Male.yin_yang().as_str(),
"female": Gender::Female.yin_yang().as_str(),
},
"CHINESE_TIME": CHINESE_TIME,
"TIME_RANGE": TIME_RANGES,
"TIGER_RULE": rule_map(&TIGER_RULE),
"RAT_RULE": rule_map(&RAT_RULE),
"MUTAGEN": crate::data::stars::MUTAGEN.iter().map(|m| m.as_key()).collect::<Vec<_>>(),
"FIVE_ELEMENTS_CLASS": FIVE_ELEMENTS_CLASSES
.iter()
.map(|c| (c.as_key().to_string(), json!(*c as u8)))
.collect::<serde_json::Map<_, _>>(),
}),
}
}
fn rule_map(rule: &[HeavenlyStem; 10]) -> Value {
json!(
HEAVENLY_STEMS
.iter()
.zip(rule.iter())
.map(|(from, to)| (from.as_key().to_string(), json!(to.as_key())))
.collect::<serde_json::Map<_, _>>()
)
}