rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use crate::core::validators::{MaxValueValidator, MinValueValidator, ValidationError, Validator};

use super::FieldType;

const SMALL_INTEGER_MIN: i64 = -32_768;
const SMALL_INTEGER_MAX: i64 = 32_767;
const INTEGER_MIN: i64 = -2_147_483_648;
const INTEGER_MAX: i64 = 2_147_483_647;

pub fn validate_integer_field(
    value: i64,
    min: Option<i64>,
    max: Option<i64>,
) -> Result<(), ValidationError> {
    if let Some(min) = min {
        MinValueValidator::new(min).validate(&value)?;
    }
    if let Some(max) = max {
        MaxValueValidator::new(max).validate(&value)?;
    }
    Ok(())
}

pub fn validate_positive_integer_field(
    value: i64,
    max: Option<i64>,
) -> Result<(), ValidationError> {
    validate_integer_field(value, Some(0), max)
}

pub fn to_i32(value: i64) -> Result<i32, ValidationError> {
    i32::try_from(value).map_err(|_| {
        ValidationError::new("Value exceeds the range of a 32-bit integer.", "invalid")
            .with_param("value", value.to_string())
    })
}

pub fn to_i16(value: i64) -> Result<i16, ValidationError> {
    i16::try_from(value).map_err(|_| {
        ValidationError::new("Value exceeds the range of a 16-bit integer.", "invalid")
            .with_param("value", value.to_string())
    })
}

fn range_for(field_type: &FieldType) -> Option<(i64, i64)> {
    match field_type {
        FieldType::Auto | FieldType::Integer => Some((INTEGER_MIN, INTEGER_MAX)),
        FieldType::BigAuto | FieldType::BigInteger => Some((i64::MIN, i64::MAX)),
        FieldType::SmallAuto | FieldType::SmallInteger => {
            Some((SMALL_INTEGER_MIN, SMALL_INTEGER_MAX))
        }
        FieldType::PositiveInteger => Some((0, INTEGER_MAX)),
        FieldType::PositiveBigInteger => Some((0, i64::MAX)),
        FieldType::PositiveSmallInteger => Some((0, SMALL_INTEGER_MAX)),
        _ => None,
    }
}

pub fn validate_integer_range(value: i64, field_type: &FieldType) -> Result<(), ValidationError> {
    let Some((min, max)) = range_for(field_type) else {
        return Err(ValidationError::new(
            "Field type is not handled by integer range validation.",
            "invalid",
        ));
    };

    validate_integer_field(value, Some(min), Some(max))
}

#[must_use]
pub fn db_type(field: &FieldType, vendor: &str) -> Option<String> {
    let _ = vendor;

    match field {
        FieldType::Auto | FieldType::Integer | FieldType::PositiveInteger => {
            Some(if matches!(field, FieldType::PositiveInteger) {
                "integer CHECK (value >= 0)".to_string()
            } else {
                "integer".to_string()
            })
        }
        FieldType::BigAuto | FieldType::BigInteger | FieldType::PositiveBigInteger => {
            Some(if matches!(field, FieldType::PositiveBigInteger) {
                "bigint CHECK (value >= 0)".to_string()
            } else {
                "bigint".to_string()
            })
        }
        FieldType::SmallAuto | FieldType::SmallInteger | FieldType::PositiveSmallInteger => {
            Some(if matches!(field, FieldType::PositiveSmallInteger) {
                "smallint CHECK (value >= 0)".to_string()
            } else {
                "smallint".to_string()
            })
        }
        _ => None,
    }
}

pub fn get_prep_value(field_type: &FieldType, value: &str) -> Result<String, ValidationError> {
    let parsed = value.parse::<i64>().map_err(|_| {
        ValidationError::new("Enter a valid integer.", "invalid").with_param("value", value)
    })?;
    validate_integer_range(parsed, field_type)?;
    Ok(parsed.to_string())
}

pub fn from_db_value(value: &str) -> Result<i64, ValidationError> {
    value.parse::<i64>().map_err(|_| {
        ValidationError::new("Enter a valid integer.", "invalid").with_param("value", value)
    })
}

#[must_use]
pub fn formfield(field: &FieldType) -> Option<&'static str> {
    match field {
        FieldType::Auto
        | FieldType::BigAuto
        | FieldType::SmallAuto
        | FieldType::Integer
        | FieldType::BigInteger
        | FieldType::SmallInteger
        | FieldType::PositiveInteger
        | FieldType::PositiveBigInteger
        | FieldType::PositiveSmallInteger => Some("IntegerField"),
        _ => None,
    }
}

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

    #[test]
    fn db_type_for_postgres_uses_bigint_for_big_integer() {
        assert_eq!(
            db_type(&FieldType::BigInteger, "postgres").as_deref(),
            Some("bigint")
        );
    }

    #[test]
    fn get_prep_value_normalizes_integer_strings() {
        let prepared = get_prep_value(&FieldType::Integer, "42")
            .expect("integer preparation should parse valid numbers");
        assert_eq!(prepared, "42");
    }

    #[test]
    fn from_db_value_parses_integer() {
        let parsed = from_db_value("-17").expect("database integer should parse");
        assert_eq!(parsed, -17);
    }

    #[test]
    fn formfield_returns_correct_type() {
        assert_eq!(formfield(&FieldType::PositiveInteger), Some("IntegerField"));
    }

    #[test]
    fn validate_integer_range_rejects_small_integer_overflow() {
        let error = validate_integer_range(40_000, &FieldType::SmallInteger)
            .expect_err("small integers should enforce i16 bounds");
        assert_eq!(error.code, "max_value");
    }
}