use cpex_core::cmf::MessagePayload;
use cpex_core::context::PluginContextTable;
use cpex_core::extensions::Extensions;
use cpex_core::hooks::payload::PluginPayload;
use pyo3::exceptions::PyValueError;
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
use serde_json::{Map, Value};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GenericPayload {
pub value: Value,
}
cpex_core::impl_plugin_payload!(GenericPayload);
pub fn pyobj_to_json_value(
_py: Python<'_>,
obj: &Bound<'_, PyAny>,
depth: usize,
) -> PyResult<Value> {
if depth > 128 {
return Err(PyValueError::new_err(
"cpex: value nesting exceeds maximum depth of 128 levels",
));
}
if obj.is_none() {
return Ok(Value::Null);
}
if let Ok(b) = obj.cast::<PyBool>() {
return Ok(Value::Bool(b.is_true()));
}
if let Ok(i) = obj.cast::<PyInt>() {
let n: i64 = i.extract()?;
return Ok(Value::Number(n.into()));
}
if let Ok(f) = obj.cast::<PyFloat>() {
let v: f64 = f.extract()?;
let n = serde_json::Number::from_f64(v).ok_or_else(|| {
PyValueError::new_err(format!("cpex: float value {v} is not a valid JSON number"))
})?;
return Ok(Value::Number(n));
}
if let Ok(s) = obj.cast::<PyString>() {
let text: String = s.extract()?;
return Ok(Value::String(text));
}
if let Ok(lst) = obj.cast::<PyList>() {
let mut out = Vec::with_capacity(lst.len());
for item in lst.iter() {
out.push(pyobj_to_json_value(_py, &item, depth + 1)?);
}
return Ok(Value::Array(out));
}
if let Ok(d) = obj.cast::<PyDict>() {
let mut map = Map::with_capacity(d.len());
for (k, v) in d.iter() {
let key: String = k.extract().map_err(|_| {
PyValueError::new_err("cpex: dict keys must be strings; got a non-string key")
})?;
map.insert(key, pyobj_to_json_value(_py, &v, depth + 1)?);
}
return Ok(Value::Object(map));
}
let type_name = obj
.get_type()
.qualname()
.and_then(|s| s.extract::<String>())
.unwrap_or_else(|_| "unknown".to_string());
Err(PyValueError::new_err(format!(
"cpex: cannot convert Python object of type '{type_name}' to a JSON value"
)))
}
pub fn json_value_to_pyobj<'py>(py: Python<'py>, v: &Value) -> PyResult<Bound<'py, PyAny>> {
match v {
Value::Null => Ok(py.None().into_bound(py)),
Value::Bool(b) => Ok(b.into_pyobject(py)?.to_owned().into_any()),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(i.into_pyobject(py)?.into_any())
} else if let Some(f) = n.as_f64() {
Ok(f.into_pyobject(py)?.into_any())
} else {
Err(PyValueError::new_err(format!(
"cpex: JSON number {n} is out of range for Python"
)))
}
},
Value::String(s) => Ok(s.into_pyobject(py)?.into_any()),
Value::Array(arr) => {
let lst = PyList::empty(py);
for item in arr {
lst.append(json_value_to_pyobj(py, item)?)?;
}
Ok(lst.into_any())
},
Value::Object(map) => {
let d = PyDict::new(py);
for (k, val) in map {
d.set_item(k, json_value_to_pyobj(py, val)?)?;
}
Ok(d.into_any())
},
}
}
pub fn resolve_payload(hook_name: &str, value: Value) -> PyResult<Box<dyn PluginPayload>> {
if hook_name.starts_with("cmf.") {
let msg: MessagePayload = serde_json::from_value(value).map_err(|e| {
PyValueError::new_err(format!(
"cpex: payload for '{hook_name}' is not a valid MessagePayload: {e}"
))
})?;
Ok(Box::new(msg))
} else {
Ok(Box::new(GenericPayload { value }))
}
}
pub fn serialize_payload(payload: &dyn PluginPayload) -> Option<Value> {
if let Some(mp) = payload.as_any().downcast_ref::<MessagePayload>() {
return serde_json::to_value(mp).ok();
}
if let Some(gp) = payload.as_any().downcast_ref::<GenericPayload>() {
return serde_json::to_value(&gp.value).ok();
}
None
}
pub fn extensions_from_value(value: Value) -> PyResult<Extensions> {
serde_json::from_value(value)
.map_err(|e| PyValueError::new_err(format!("cpex: extensions conversion failed: {e}")))
}
pub fn context_table_from_value(value: Value) -> PyResult<Option<PluginContextTable>> {
if value.is_null() {
return Ok(None);
}
let table: PluginContextTable = serde_json::from_value(value).map_err(|e| {
PyValueError::new_err(format!("cpex: context_table conversion failed: {e}"))
})?;
Ok(Some(table))
}