use std::ffi::{CStr, CString, c_char};
use crate::data::types::{Gender, Language};
use crate::dto::parse_config_json;
fn parse_gender(s: &str) -> Result<Gender, String> {
match s.to_lowercase().as_str() {
"male" => Ok(Gender::Male),
"female" => Ok(Gender::Female),
_ => Err(format!(
"Invalid gender '{}'. Expected 'male' or 'female'.",
s
)),
}
}
fn parse_language(s: &str) -> Result<Language, String> {
match s.to_lowercase().as_str() {
"zh_cn" => Ok(Language::ZhCN),
"zh_tw" => Ok(Language::ZhTW),
"en_us" => Ok(Language::EnUS),
"ja_jp" => Ok(Language::JaJP),
"ko_kr" => Ok(Language::KoKR),
"vi_vn" => Ok(Language::ViVN),
_ => Err(format!(
"Invalid language '{}'. Expected one of: zh_cn, zh_tw, en_us, ja_jp, ko_kr, vi_vn.",
s
)),
}
}
unsafe fn cstr_to_str<'a>(ptr: *const c_char, param_name: &str) -> Result<&'a str, String> {
if ptr.is_null() {
return Err(format!("'{}' is null", param_name));
}
unsafe { CStr::from_ptr(ptr) }
.to_str()
.map_err(|e| format!("'{}' is not valid UTF-8: {}", param_name, e))
}
unsafe fn cstr_to_opt_str<'a>(
ptr: *const c_char,
param_name: &str,
) -> Result<Option<&'a str>, String> {
if ptr.is_null() {
return Ok(None);
}
unsafe { cstr_to_str(ptr, param_name) }.map(Some)
}
fn catch(
f: impl FnOnce() -> Result<String, String> + std::panic::UnwindSafe,
) -> Result<String, String> {
std::panic::catch_unwind(f).unwrap_or_else(|panic| {
Err(format!(
"Invalid input: {}",
crate::dto::panic_message(panic.as_ref())
))
})
}
fn error_json(msg: &str) -> *mut c_char {
let json = serde_json::json!({ "error": msg }).to_string();
CString::new(json).unwrap().into_raw()
}
fn ok_json(json: String) -> *mut c_char {
CString::new(json).unwrap().into_raw()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_by_solar(
solar_date: *const c_char,
time_index: u8,
gender: *const c_char,
fix_leap: bool,
language: *const c_char,
config_json: *const c_char,
) -> *mut c_char {
let parsed = (|| -> Result<_, String> {
let solar_date = unsafe { cstr_to_str(solar_date, "solar_date")? };
let gender_str = unsafe { cstr_to_str(gender, "gender")? };
let language_str = unsafe { cstr_to_str(language, "language")? };
let config_str = unsafe { cstr_to_opt_str(config_json, "config_json")? };
Ok((
solar_date,
parse_gender(gender_str)?,
parse_language(language_str)?,
parse_config_json(config_str)?,
))
})();
let result = parsed.and_then(|(solar_date, gender, language, config)| {
catch(move || {
crate::by_solar_json(solar_date, time_index, gender, fix_leap, language, config)
.map_err(|e| e.to_string())
})
});
match result {
Ok(json) => ok_json(json),
Err(msg) => error_json(&msg),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_by_lunar(
lunar_date: *const c_char,
time_index: u8,
gender: *const c_char,
is_leap_month: bool,
fix_leap: bool,
language: *const c_char,
config_json: *const c_char,
) -> *mut c_char {
let parsed = (|| -> Result<_, String> {
let lunar_date = unsafe { cstr_to_str(lunar_date, "lunar_date")? };
let gender_str = unsafe { cstr_to_str(gender, "gender")? };
let language_str = unsafe { cstr_to_str(language, "language")? };
let config_str = unsafe { cstr_to_opt_str(config_json, "config_json")? };
Ok((
lunar_date,
parse_gender(gender_str)?,
parse_language(language_str)?,
parse_config_json(config_str)?,
))
})();
let result = parsed.and_then(|(lunar_date, gender, language, config)| {
catch(move || {
crate::by_lunar_json(
lunar_date,
time_index,
gender,
is_leap_month,
fix_leap,
language,
config,
)
.map_err(|e| e.to_string())
})
});
match result {
Ok(json) => ok_json(json),
Err(msg) => error_json(&msg),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_get_horoscope(
solar_date: *const c_char,
time_index: u8,
gender: *const c_char,
fix_leap: bool,
language: *const c_char,
config_json: *const c_char,
target_date: *const c_char,
target_time_index: u8,
) -> *mut c_char {
let parsed = (|| -> Result<_, String> {
let solar_date = unsafe { cstr_to_str(solar_date, "solar_date")? };
let gender_str = unsafe { cstr_to_str(gender, "gender")? };
let language_str = unsafe { cstr_to_str(language, "language")? };
let config_str = unsafe { cstr_to_opt_str(config_json, "config_json")? };
let target_date = unsafe { cstr_to_str(target_date, "target_date")? };
Ok((
solar_date,
parse_gender(gender_str)?,
parse_language(language_str)?,
parse_config_json(config_str)?,
target_date,
))
})();
let result = parsed.and_then(|(solar_date, gender, language, config, target_date)| {
catch(move || {
let astrolabe =
crate::by_solar(solar_date, time_index, gender, fix_leap, language, config)
.map_err(|e| e.to_string())?;
let horoscope =
crate::get_horoscope(&astrolabe, target_date, target_time_index, language)
.map_err(|e| e.to_string())?;
serde_json::to_string(&horoscope.to_dto(language))
.map_err(|e| format!("Failed to serialize horoscope: {}", e))
})
});
match result {
Ok(json) => ok_json(json),
Err(msg) => error_json(&msg),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn iztro_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}