use chrono::{DateTime, NaiveDate, NaiveTime, Utc};
use clarax::prelude::*;
use rust_decimal::Decimal;
use std::str::FromStr;
use uuid::Uuid;
use crate::error::DjangoError;
use crate::field_types::{DjangoFieldType, FieldDescriptor};
use clarax_core::types::FieldValue;
pub fn extract_field_descriptors(
py: Python<'_>,
model_class: &Bound<'_, PyAny>,
) -> Result<Vec<FieldDescriptor>, DjangoError> {
let meta = model_class
.getattr("_meta")
.map_err(|_| DjangoError::Python("model class has no _meta attribute".into()))?;
let fields = meta
.call_method0("get_fields")
.map_err(|e| DjangoError::Python(format!("_meta.get_fields() failed: {e}")))?;
let mut descriptors = Vec::new();
let fields_list: Vec<Bound<'_, PyAny>> = fields
.extract()
.map_err(|e| DjangoError::Python(format!("cannot iterate _meta.get_fields(): {e}")))?;
for field in &fields_list {
let is_relation = field
.getattr("is_relation")
.and_then(|v| v.extract::<bool>())
.unwrap_or(true);
if is_relation {
continue;
}
let name: String = field
.getattr("name")
.and_then(|v| v.extract())
.map_err(|e| DjangoError::Python(format!("field missing 'name': {e}")))?;
let internal_type: String = field
.call_method0("get_internal_type")
.and_then(|v| v.extract())
.unwrap_or_default();
let nullable: bool = field
.getattr("null")
.and_then(|v| v.extract())
.unwrap_or(false);
let has_default: bool = field
.call_method0("has_default")
.and_then(|v| v.extract())
.unwrap_or(false);
if let Some(field_type) = map_django_internal_type(py, field, &internal_type) {
descriptors.push(FieldDescriptor {
name,
field_type,
nullable,
has_default,
});
}
}
Ok(descriptors)
}
fn map_django_internal_type(
_py: Python<'_>,
field: &Bound<'_, PyAny>,
internal_type: &str,
) -> Option<DjangoFieldType> {
match internal_type {
"CharField" => {
let max_length = extract_usize_attr(field, "max_length").unwrap_or(255);
Some(DjangoFieldType::CharField { max_length })
}
"TextField" => Some(DjangoFieldType::TextField),
"IntegerField" | "SmallIntegerField" | "PositiveIntegerField"
| "PositiveSmallIntegerField" => Some(DjangoFieldType::IntegerField),
"BigIntegerField" | "PositiveBigIntegerField" => Some(DjangoFieldType::BigIntegerField),
"FloatField" => Some(DjangoFieldType::FloatField),
"DecimalField" => {
let max_digits = extract_u32_attr(field, "max_digits").unwrap_or(10);
let decimal_places = extract_u32_attr(field, "decimal_places").unwrap_or(2);
Some(DjangoFieldType::DecimalField {
max_digits,
decimal_places,
})
}
"BooleanField" | "NullBooleanField" => Some(DjangoFieldType::BooleanField),
"DateField" => Some(DjangoFieldType::DateField),
"TimeField" => Some(DjangoFieldType::TimeField),
"DateTimeField" => Some(DjangoFieldType::DateTimeField),
"UUIDField" => Some(DjangoFieldType::UuidField),
"JSONField" => Some(DjangoFieldType::JsonField),
"BinaryField" => {
let max_length = extract_usize_attr(field, "max_length");
Some(DjangoFieldType::BinaryField { max_length })
}
"EmailField" => {
let max_length = extract_usize_attr(field, "max_length").unwrap_or(254);
Some(DjangoFieldType::EmailField { max_length })
}
"URLField" => {
let max_length = extract_usize_attr(field, "max_length").unwrap_or(200);
Some(DjangoFieldType::UrlField { max_length })
}
"SlugField" => {
let max_length = extract_usize_attr(field, "max_length").unwrap_or(50);
Some(DjangoFieldType::SlugField { max_length })
}
_ => None,
}
}
fn extract_usize_attr(obj: &Bound<'_, PyAny>, attr: &str) -> Option<usize> {
obj.getattr(attr).ok()?.extract::<usize>().ok()
}
fn extract_u32_attr(obj: &Bound<'_, PyAny>, attr: &str) -> Option<u32> {
obj.getattr(attr).ok()?.extract::<u32>().ok()
}
pub fn convert_python_value_to_field(
py_value: &Bound<'_, PyAny>,
descriptor: &FieldDescriptor,
) -> Result<FieldValue, DjangoError> {
if py_value.is_none() {
return Ok(FieldValue::Null);
}
match &descriptor.field_type {
DjangoFieldType::CharField { .. }
| DjangoFieldType::TextField
| DjangoFieldType::EmailField { .. }
| DjangoFieldType::UrlField { .. }
| DjangoFieldType::SlugField { .. } => {
let val: String = py_value.extract().map_err(|_| DjangoError::TypeConversion {
expected: "str".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
Ok(FieldValue::Text(val))
}
DjangoFieldType::IntegerField => {
let val: i64 = py_value.extract().map_err(|_| DjangoError::TypeConversion {
expected: "int".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
Ok(FieldValue::Integer(val))
}
DjangoFieldType::BigIntegerField => {
let val: i64 = py_value.extract().map_err(|_| DjangoError::TypeConversion {
expected: "int (i64)".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
Ok(FieldValue::Integer(val))
}
DjangoFieldType::FloatField => {
let val: f64 = py_value.extract().map_err(|_| DjangoError::TypeConversion {
expected: "float".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
Ok(FieldValue::Float(val))
}
DjangoFieldType::DecimalField { .. } => {
let str_val: String = py_value
.call_method0("__str__")
.and_then(|s| s.extract())
.map_err(|_| DjangoError::TypeConversion {
expected: "Decimal (via str)".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
let decimal = Decimal::from_str(&str_val).map_err(|_| DjangoError::Serialization {
field: descriptor.name.clone(),
message: format!("cannot parse '{str_val}' as Decimal"),
})?;
Ok(FieldValue::Decimal(decimal))
}
DjangoFieldType::BooleanField => {
let val: bool = py_value.extract().map_err(|_| DjangoError::TypeConversion {
expected: "bool".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
Ok(FieldValue::Boolean(val))
}
DjangoFieldType::DateField => {
let iso: String = py_value
.call_method0("isoformat")
.and_then(|s| s.extract())
.map_err(|_| DjangoError::TypeConversion {
expected: "date".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
let date = NaiveDate::parse_from_str(&iso, "%Y-%m-%d").map_err(|_| {
DjangoError::Serialization {
field: descriptor.name.clone(),
message: format!("cannot parse '{iso}' as date"),
}
})?;
Ok(FieldValue::Date(date))
}
DjangoFieldType::TimeField => {
let iso: String = py_value
.call_method0("isoformat")
.and_then(|s| s.extract())
.map_err(|_| DjangoError::TypeConversion {
expected: "time".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
let time = NaiveTime::parse_from_str(&iso, "%H:%M:%S%.f").map_err(|_| {
DjangoError::Serialization {
field: descriptor.name.clone(),
message: format!("cannot parse '{iso}' as time"),
}
})?;
Ok(FieldValue::Time(time))
}
DjangoFieldType::DateTimeField => {
let iso: String = py_value
.call_method0("isoformat")
.and_then(|s| s.extract())
.map_err(|_| DjangoError::TypeConversion {
expected: "datetime".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
let dt = iso
.parse::<DateTime<Utc>>()
.or_else(|_| {
chrono::NaiveDateTime::parse_from_str(&iso, "%Y-%m-%dT%H:%M:%S%.f")
.map(|naive| naive.and_utc())
})
.map_err(|_| DjangoError::Serialization {
field: descriptor.name.clone(),
message: format!("cannot parse '{iso}' as datetime"),
})?;
Ok(FieldValue::DateTime(dt))
}
DjangoFieldType::UuidField => {
let str_val: String =
py_value
.call_method0("__str__")
.and_then(|s| s.extract())
.map_err(|_| DjangoError::TypeConversion {
expected: "UUID".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
let uuid = Uuid::parse_str(&str_val).map_err(|_| DjangoError::Serialization {
field: descriptor.name.clone(),
message: format!("cannot parse '{str_val}' as UUID"),
})?;
Ok(FieldValue::Uuid(uuid))
}
DjangoFieldType::JsonField => {
let py = py_value.py();
let json_mod = py.import("json").map_err(|e| {
DjangoError::Python(format!("cannot import json module: {e}"))
})?;
let dumped = json_mod
.call_method1("dumps", (py_value,))
.map_err(|e| DjangoError::Serialization {
field: descriptor.name.clone(),
message: format!("json.dumps failed: {e}"),
})?;
let json_str: String = dumped.extract::<String>().map_err(|e| {
DjangoError::Python(format!("json.dumps returned non-string: {e}"))
})?;
let value: serde_json::Value =
serde_json::from_str(&json_str).map_err(|e| DjangoError::Serialization {
field: descriptor.name.clone(),
message: format!("invalid JSON: {e}"),
})?;
Ok(FieldValue::Json(value))
}
DjangoFieldType::BinaryField { .. } => {
let bytes: Vec<u8> =
py_value
.extract()
.map_err(|_| DjangoError::TypeConversion {
expected: "bytes".into(),
actual: py_value.get_type().qualname().map(|n| n.to_string()).unwrap_or_else(|_| "unknown".into()),
})?;
Ok(FieldValue::Binary(bytes))
}
}
}