rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use std::cell::RefCell;

use chrono::{DateTime, FixedOffset, Utc};

thread_local! {
    static CURRENT_TIMEZONE: RefCell<String> = RefCell::new("UTC".to_string());
}

pub fn activate_timezone(tz_name: &str) {
    CURRENT_TIMEZONE.with(|timezone| {
        *timezone.borrow_mut() = tz_name.to_owned();
    });
}

pub fn deactivate_timezone() {
    CURRENT_TIMEZONE.with(|timezone| {
        *timezone.borrow_mut() = "UTC".to_owned();
    });
}

#[must_use]
pub fn get_current_timezone_name() -> String {
    CURRENT_TIMEZONE.with(|timezone| timezone.borrow().clone())
}

#[must_use]
pub fn now() -> DateTime<Utc> {
    Utc::now()
}

#[must_use]
pub fn localtime(dt: &DateTime<Utc>, offset_hours: i32) -> DateTime<FixedOffset> {
    dt.with_timezone(&fixed_offset(offset_hours))
}

#[must_use]
pub fn is_aware(_dt: &DateTime<Utc>) -> bool {
    true
}

#[must_use]
pub fn is_naive(dt_str: &str) -> bool {
    !dt_str.contains('+') && !dt_str.ends_with('Z')
}

#[must_use]
pub fn make_aware(naive_str: &str, offset_hours: i32) -> String {
    format!("{naive_str}{}", format_offset(offset_hours))
}

fn fixed_offset(offset_hours: i32) -> FixedOffset {
    let clamped = offset_hours.clamp(-23, 23) * 3600;
    FixedOffset::east_opt(clamped)
        .unwrap_or_else(|| FixedOffset::east_opt(0).expect("zero offset must be valid"))
}

fn format_offset(offset_hours: i32) -> String {
    let clamped = offset_hours.clamp(-23, 23);
    let sign = if clamped < 0 { '-' } else { '+' };
    let hours = clamped.unsigned_abs();

    format!("{sign}{hours:02}:00")
}

#[cfg(test)]
mod tests {
    use super::*;
    use chrono::{TimeZone, Timelike, Utc};

    #[test]
    fn test_timezone_activate_deactivate() {
        deactivate_timezone();
        activate_timezone("Europe/Berlin");
        assert_eq!(get_current_timezone_name(), "Europe/Berlin");

        deactivate_timezone();
        assert_eq!(get_current_timezone_name(), "UTC");
    }

    #[test]
    fn test_localtime_offset() {
        let dt = Utc.with_ymd_and_hms(2024, 3, 19, 12, 0, 0).unwrap();
        let local = localtime(&dt, 2);

        assert_eq!(local.offset().local_minus_utc(), 2 * 3600);
        assert_eq!(local.hour(), 14);
    }

    #[test]
    fn test_is_naive_detection() {
        assert!(is_naive("2024-03-19T12:00:00"));
        assert!(!is_naive("2024-03-19T12:00:00+02:00"));
        assert!(!is_naive("2024-03-19T12:00:00Z"));
    }

    #[test]
    fn test_make_aware() {
        assert_eq!(
            make_aware("2024-03-19T12:00:00", 2),
            "2024-03-19T12:00:00+02:00"
        );
        assert_eq!(
            make_aware("2024-03-19T12:00:00", -5),
            "2024-03-19T12:00:00-05:00"
        );
    }

    #[test]
    fn aware_datetimes_are_always_aware() {
        let dt = Utc.with_ymd_and_hms(2024, 3, 19, 12, 0, 0).unwrap();
        assert!(is_aware(&dt));
    }

    #[test]
    fn now_returns_utc_datetime() {
        assert_eq!(now().timezone(), Utc);
    }
}