rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use crate::core::validators::ValidationError;

use super::FieldType;

pub fn validate_decimal_field(
    value: &str,
    max_digits: usize,
    decimal_places: usize,
) -> Result<(), ValidationError> {
    let normalized = value.trim().trim_start_matches(['+', '-']);
    let parts: Vec<_> = normalized.split('.').collect();

    let is_valid_decimal = match parts.as_slice() {
        [whole] => !whole.is_empty() && whole.chars().all(|ch| ch.is_ascii_digit()),
        [whole, fractional] => {
            !whole.is_empty()
                && whole.chars().all(|ch| ch.is_ascii_digit())
                && fractional.chars().all(|ch| ch.is_ascii_digit())
        }
        _ => false,
    };

    if !is_valid_decimal {
        return Err(ValidationError::new(
            "Enter a valid decimal number.",
            "invalid",
        ));
    }

    let whole_digits = parts[0].chars().count();
    let fractional_digits = parts
        .get(1)
        .map_or(0, |fractional| fractional.chars().count());
    let total_digits = whole_digits + fractional_digits;

    if fractional_digits > decimal_places {
        return Err(ValidationError::new(
            format!("Ensure that there are no more than {decimal_places} decimal places."),
            "max_decimal_places",
        ));
    }

    if total_digits > max_digits {
        return Err(ValidationError::new(
            format!("Ensure that there are no more than {max_digits} digits in total."),
            "max_digits",
        ));
    }

    Ok(())
}

fn parse_float(value: &str) -> Result<f64, ValidationError> {
    value.trim().parse::<f64>().map_err(|_| {
        ValidationError::new("Enter a valid number.", "invalid").with_param("value", value)
    })
}

#[must_use]
pub fn db_type(field: &FieldType, vendor: &str) -> Option<String> {
    match field {
        FieldType::Float => Some(match vendor {
            "postgres" => "double precision".to_string(),
            "sqlite" => "real".to_string(),
            "mysql" => "double".to_string(),
            _ => "double precision".to_string(),
        }),
        FieldType::Decimal {
            max_digits,
            decimal_places,
        } => Some(format!("numeric({max_digits},{decimal_places})")),
        _ => None,
    }
}

pub fn get_prep_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
    match field {
        FieldType::Float => Ok(parse_float(value)?.to_string()),
        FieldType::Decimal {
            max_digits,
            decimal_places,
        } => {
            validate_decimal_field(value, *max_digits as usize, *decimal_places as usize)?;
            Ok(value.trim().to_string())
        }
        _ => Err(ValidationError::new(
            "Field type is not handled by float preparation.",
            "invalid",
        )),
    }
}

pub fn from_db_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
    match field {
        FieldType::Float => Ok(parse_float(value)?.to_string()),
        FieldType::Decimal {
            max_digits,
            decimal_places,
        } => {
            validate_decimal_field(value, *max_digits as usize, *decimal_places as usize)?;
            Ok(value.trim().to_string())
        }
        _ => Err(ValidationError::new(
            "Field type is not handled by float conversion.",
            "invalid",
        )),
    }
}

#[must_use]
pub fn formfield(field: &FieldType) -> Option<&'static str> {
    match field {
        FieldType::Float => Some("FloatField"),
        FieldType::Decimal { .. } => Some("DecimalField"),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::{FieldType, db_type, formfield, from_db_value, get_prep_value};

    #[test]
    fn db_type_for_postgres_uses_double_precision_for_float() {
        assert_eq!(
            db_type(&FieldType::Float, "postgres").as_deref(),
            Some("double precision")
        );
    }

    #[test]
    fn get_prep_value_normalizes_float_input() {
        let prepared = get_prep_value(&FieldType::Float, "3.5")
            .expect("float preparation should parse valid numbers");
        assert_eq!(prepared, "3.5");
    }

    #[test]
    fn from_db_value_parses_decimal_round_trip() {
        let field = FieldType::Decimal {
            max_digits: 5,
            decimal_places: 2,
        };
        let parsed = from_db_value(&field, "12.34").expect("decimal values should parse");
        assert_eq!(parsed, "12.34");
    }

    #[test]
    fn formfield_returns_correct_type() {
        let field = FieldType::Decimal {
            max_digits: 6,
            decimal_places: 2,
        };
        assert_eq!(formfield(&field), Some("DecimalField"));
    }
}