rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use chrono::{DateTime, Duration, NaiveDate, NaiveTime, SecondsFormat, Utc};

use crate::core::validators::ValidationError;

use super::FieldType;

pub fn validate_temporal_flags(auto_now: bool, auto_now_add: bool) -> Result<(), ValidationError> {
    if auto_now && auto_now_add {
        return Err(ValidationError::new(
            "auto_now and auto_now_add are mutually exclusive.",
            "invalid",
        ));
    }

    Ok(())
}

pub fn parse_datetime_field(value: &str) -> Result<DateTime<Utc>, ValidationError> {
    DateTime::parse_from_rfc3339(value)
        .map(|value| value.with_timezone(&Utc))
        .map_err(|_| {
            ValidationError::new("Enter a valid RFC 3339 datetime.", "invalid")
                .with_param("value", value)
        })
}

pub fn parse_date_field(value: &str) -> Result<NaiveDate, ValidationError> {
    NaiveDate::parse_from_str(value, "%Y-%m-%d").map_err(|_| {
        ValidationError::new("Enter a valid ISO date.", "invalid").with_param("value", value)
    })
}

pub fn parse_time_field(value: &str) -> Result<NaiveTime, ValidationError> {
    NaiveTime::parse_from_str(value, "%H:%M:%S")
        .or_else(|_| NaiveTime::parse_from_str(value, "%H:%M"))
        .map_err(|_| {
            ValidationError::new("Enter a valid ISO time.", "invalid").with_param("value", value)
        })
}

fn parse_duration_field(value: &str) -> Result<Duration, ValidationError> {
    if let Ok(microseconds) = value.trim().parse::<i64>() {
        return Ok(Duration::microseconds(microseconds));
    }

    let (days, time_part) = if let Some((days_part, rest)) = value.split_once(' ') {
        (
            days_part.parse::<i64>().map_err(|_| {
                ValidationError::new("Enter a valid duration.", "invalid")
                    .with_param("value", value)
            })?,
            rest,
        )
    } else {
        (0, value)
    };

    let parts = time_part.split(':').collect::<Vec<_>>();
    let (hours, minutes, seconds) = match parts.as_slice() {
        [hours, minutes, seconds] => (
            hours.parse::<i64>().ok(),
            minutes.parse::<i64>().ok(),
            seconds.parse::<i64>().ok(),
        ),
        _ => (None, None, None),
    };

    match (hours, minutes, seconds) {
        (Some(hours), Some(minutes), Some(seconds)) => Ok(Duration::days(days)
            + Duration::hours(hours)
            + Duration::minutes(minutes)
            + Duration::seconds(seconds)),
        _ => {
            Err(ValidationError::new("Enter a valid duration.", "invalid")
                .with_param("value", value))
        }
    }
}

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

    match field {
        FieldType::DateTime { .. } => Some("timestamp with time zone".to_string()),
        FieldType::Date { .. } => Some("date".to_string()),
        FieldType::Time => Some("time".to_string()),
        FieldType::Duration => Some("bigint".to_string()),
        _ => None,
    }
}

pub fn get_prep_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
    match field {
        FieldType::DateTime { .. } => {
            Ok(parse_datetime_field(value)?.to_rfc3339_opts(SecondsFormat::AutoSi, true))
        }
        FieldType::Date { .. } => Ok(parse_date_field(value)?.format("%Y-%m-%d").to_string()),
        FieldType::Time => Ok(parse_time_field(value)?.format("%H:%M:%S").to_string()),
        FieldType::Duration => Ok(parse_duration_field(value)?
            .num_microseconds()
            .expect("duration microseconds should fit into i64")
            .to_string()),
        _ => Err(ValidationError::new(
            "Field type is not handled by datetime preparation.",
            "invalid",
        )),
    }
}

pub fn from_db_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
    match field {
        FieldType::DateTime { .. } => {
            Ok(parse_datetime_field(value)?.to_rfc3339_opts(SecondsFormat::AutoSi, true))
        }
        FieldType::Date { .. } => Ok(parse_date_field(value)?.format("%Y-%m-%d").to_string()),
        FieldType::Time => Ok(parse_time_field(value)?.format("%H:%M:%S").to_string()),
        FieldType::Duration => Ok(parse_duration_field(value)?
            .num_microseconds()
            .expect("duration microseconds should fit into i64")
            .to_string()),
        _ => Err(ValidationError::new(
            "Field type is not handled by datetime conversion.",
            "invalid",
        )),
    }
}

#[must_use]
pub fn formfield(field: &FieldType) -> Option<&'static str> {
    match field {
        FieldType::DateTime { .. } => Some("DateTimeField"),
        FieldType::Date { .. } => Some("DateField"),
        FieldType::Time => Some("TimeField"),
        FieldType::Duration => Some("DurationField"),
        _ => None,
    }
}

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

    #[test]
    fn db_type_for_postgres_uses_timestamp_with_timezone() {
        let field = FieldType::DateTime {
            auto_now: false,
            auto_now_add: false,
        };
        assert_eq!(
            db_type(&field, "postgres").as_deref(),
            Some("timestamp with time zone")
        );
    }

    #[test]
    fn get_prep_value_normalizes_datetime() {
        let field = FieldType::DateTime {
            auto_now: false,
            auto_now_add: false,
        };
        let prepared = get_prep_value(&field, "2024-01-02T03:04:05Z")
            .expect("datetime preparation should parse RFC3339 values");
        assert_eq!(prepared, "2024-01-02T03:04:05Z");
    }

    #[test]
    fn from_db_value_parses_date() {
        let field = FieldType::Date {
            auto_now: false,
            auto_now_add: false,
        };
        let parsed = from_db_value(&field, "2024-05-06").expect("database date should parse");
        assert_eq!(parsed, "2024-05-06");
    }

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