use crate::value::JValue;
use indexmap::IndexMap;
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
use std::cell::{OnceCell, RefCell};
use std::collections::HashMap;
use std::rc::Rc;
#[derive(Debug, Clone)]
pub struct LazyConvertError(pub String);
pub struct LazyPyDict {
obj: Py<PyDict>,
field_cache: RefCell<HashMap<String, JValue>>,
materialized: OnceCell<Rc<IndexMap<String, JValue>>>,
}
impl std::fmt::Debug for LazyPyDict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LazyPyDict").finish_non_exhaustive()
}
}
impl LazyPyDict {
pub fn new(obj: Py<PyDict>) -> Self {
LazyPyDict {
obj,
field_cache: RefCell::new(HashMap::new()),
materialized: OnceCell::new(),
}
}
pub fn py_object(&self) -> &Py<PyDict> {
&self.obj
}
pub fn same_object(&self, other: &LazyPyDict) -> bool {
self.obj.as_ptr() == other.obj.as_ptr()
}
pub fn get_field(&self, field: &str) -> Result<JValue, LazyConvertError> {
if let Some(m) = self.materialized.get() {
return Ok(m.get(field).cloned().unwrap_or(JValue::Undefined));
}
if let Some(v) = self.field_cache.borrow().get(field) {
return Ok(v.clone());
}
Python::attach(|py| {
let dict = self.obj.bind(py);
let val = match dict
.get_item(field)
.map_err(|e| LazyConvertError(e.to_string()))?
{
Some(v) => convert(&v, true).map_err(|e| LazyConvertError(e.to_string()))?,
None => JValue::Undefined,
};
if matches!(
val,
JValue::String(_) | JValue::Array(_) | JValue::Object(_) | JValue::LazyPyDict(_)
) {
self.field_cache
.borrow_mut()
.insert(field.to_string(), val.clone());
}
Ok(val)
})
}
pub fn contains_field(&self, field: &str) -> bool {
if let Some(m) = self.materialized.get() {
return m.contains_key(field);
}
if let Some(v) = self.field_cache.borrow().get(field) {
return !v.is_undefined();
}
Python::attach(|py| self.obj.bind(py).contains(field).unwrap_or(false))
}
pub fn is_empty(&self) -> bool {
if let Some(m) = self.materialized.get() {
return m.is_empty();
}
Python::attach(|py| self.obj.bind(py).is_empty())
}
pub fn to_object(&self) -> Result<Rc<IndexMap<String, JValue>>, LazyConvertError> {
if let Some(m) = self.materialized.get() {
return Ok(m.clone());
}
let map = Python::attach(|py| -> Result<IndexMap<String, JValue>, LazyConvertError> {
let dict = self.obj.bind(py);
let cache = self.field_cache.borrow();
let mut map = IndexMap::with_capacity(dict.len());
for (k, v) in dict.iter() {
let key: String = k
.extract::<String>()
.map_err(|e| LazyConvertError(e.to_string()))?;
let val = match cache.get(&key) {
Some(cached) => cached.clone(),
None => convert(&v, true).map_err(|e| LazyConvertError(e.to_string()))?,
};
map.insert(key, val);
}
Ok(map)
})?;
let rc = Rc::new(map);
let _ = self.materialized.set(rc.clone());
Ok(rc)
}
pub fn to_object_ref(&self) -> Option<&IndexMap<String, JValue>> {
if self.materialized.get().is_none() {
let _ = self.to_object();
}
self.materialized.get().map(|rc| &**rc)
}
}
pub fn convert(obj: &Bound<'_, PyAny>, lazy: bool) -> PyResult<JValue> {
use pyo3::exceptions::PyTypeError;
if obj.is_none() {
return Ok(JValue::Null);
}
if obj.is_instance_of::<PyBool>() {
return Ok(JValue::Bool(obj.extract::<bool>()?));
}
if obj.is_instance_of::<PyInt>() {
return Ok(JValue::Number(obj.extract::<i64>()? as f64));
}
if obj.is_instance_of::<PyFloat>() {
return Ok(JValue::Number(obj.extract::<f64>()?));
}
if obj.is_instance_of::<PyString>() {
return Ok(JValue::string(obj.extract::<String>()?));
}
if let Ok(list) = obj.cast::<PyList>() {
let mut result = Vec::with_capacity(list.len());
for item in list.iter() {
result.push(convert(&item, lazy)?);
}
return Ok(JValue::array(result));
}
if let Ok(dict) = obj.cast::<PyDict>() {
if lazy {
return Ok(JValue::LazyPyDict(Rc::new(LazyPyDict::new(
dict.clone().unbind(),
))));
}
let mut result = IndexMap::with_capacity(dict.len());
for (key, value) in dict.iter() {
let key_str = key.extract::<String>()?;
result.insert(key_str, convert(&value, false)?);
}
return Ok(JValue::object(result));
}
if let Ok(b) = obj.extract::<bool>() {
return Ok(JValue::Bool(b));
}
if let Ok(i) = obj.extract::<i64>() {
return Ok(JValue::Number(i as f64));
}
if let Ok(f) = obj.extract::<f64>() {
return Ok(JValue::Number(f));
}
if let Ok(s) = obj.extract::<String>() {
return Ok(JValue::string(s));
}
Err(PyTypeError::new_err(format!(
"Cannot convert Python object to JSON: {}",
obj.get_type().name()?
)))
}