use pyo3::create_exception;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use serde::de::DeserializeOwned;
use crate::bridge::{self, HoroscopeInput, LunarChartInput, QueryInput, SolarChartInput};
use crate::error::BridgeError;
create_exception!(
_x_iztro,
IztroError,
PyValueError,
"排盘出错。`args[0]` 为面向人的描述,`code` 属性为机器可读的分类标识\n (invalid_date / invalid_time_index / invalid_argument / internal)。\n 继承 ValueError,既有的 `except ValueError` 依然能捕获。"
);
fn to_py_err(err: &BridgeError) -> PyErr {
let py_err = IztroError::new_err(err.message.clone());
Python::with_gil(|py| {
let _ = py_err.value(py).setattr("code", err.code);
});
py_err
}
fn from_python<T: DeserializeOwned>(input: &Bound<'_, PyAny>) -> PyResult<T> {
pythonize::depythonize(input).map_err(|e| {
to_py_err(&BridgeError::invalid_argument(format!(
"invalid input: {e}"
)))
})
}
fn to_python(py: Python<'_>, value: &serde_json::Value) -> PyResult<PyObject> {
pythonize::pythonize(py, value)
.map(|bound| bound.unbind())
.map_err(|e| {
to_py_err(&BridgeError::internal(format!(
"failed to convert to Python object: {e}"
)))
})
}
fn run<T: DeserializeOwned>(
input: &Bound<'_, PyAny>,
compute: impl FnOnce(&T) -> Result<serde_json::Value, BridgeError>,
) -> PyResult<serde_json::Value> {
let parsed: T = from_python(input)?;
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| compute(&parsed))) {
Ok(result) => result.map_err(|e| to_py_err(&e)),
Err(panic) => Err(to_py_err(&BridgeError::internal(
crate::dto::panic_message(panic.as_ref()),
))),
}
}
#[pyfunction]
fn by_solar(py: Python<'_>, input: &Bound<'_, PyAny>) -> PyResult<PyObject> {
to_python(py, &run(input, |i: &SolarChartInput| bridge::by_solar(i))?)
}
#[pyfunction]
fn by_lunar(py: Python<'_>, input: &Bound<'_, PyAny>) -> PyResult<PyObject> {
to_python(py, &run(input, |i: &LunarChartInput| bridge::by_lunar(i))?)
}
#[pyfunction]
fn get_horoscope(py: Python<'_>, input: &Bound<'_, PyAny>) -> PyResult<PyObject> {
to_python(py, &run(input, |i: &HoroscopeInput| bridge::horoscope(i))?)
}
#[pyfunction]
fn query(py: Python<'_>, input: &Bound<'_, PyAny>) -> PyResult<PyObject> {
to_python(py, &run(input, |i: &QueryInput| bridge::query(i))?)
}
#[pyfunction]
fn by_solar_json(input: &Bound<'_, PyAny>) -> PyResult<String> {
let value = run(input, |i: &SolarChartInput| bridge::by_solar(i))?;
serde_json::to_string(&value).map_err(|e| to_py_err(&BridgeError::internal(e.to_string())))
}
#[pyfunction]
fn by_lunar_json(input: &Bound<'_, PyAny>) -> PyResult<String> {
let value = run(input, |i: &LunarChartInput| bridge::by_lunar(i))?;
serde_json::to_string(&value).map_err(|e| to_py_err(&BridgeError::internal(e.to_string())))
}
#[pymodule]
fn _x_iztro(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(by_solar, m)?)?;
m.add_function(wrap_pyfunction!(by_lunar, m)?)?;
m.add_function(wrap_pyfunction!(get_horoscope, m)?)?;
m.add_function(wrap_pyfunction!(query, m)?)?;
m.add_function(wrap_pyfunction!(by_solar_json, m)?)?;
m.add_function(wrap_pyfunction!(by_lunar_json, m)?)?;
m.add("IztroError", m.py().get_type::<IztroError>())?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}