rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use std::ops::Deref;
use std::sync::OnceLock;

/// Django's `cached_property` is intentionally omitted here.
///
/// Rust does not have Python's descriptor protocol, and idiomatic cached
/// initialization is modeled with `OnceLock` fields on the owning type.
pub struct LazyObject<T> {
    init: Box<dyn Fn() -> T + Send + Sync>,
    value: OnceLock<T>,
}

impl<T: Send + Sync> LazyObject<T> {
    /// Create a lazily initialized value wrapper.
    #[must_use]
    pub fn new(init: impl Fn() -> T + Send + Sync + 'static) -> Self {
        Self {
            init: Box::new(init),
            value: OnceLock::new(),
        }
    }

    /// Return the wrapped value, initializing it on first access.
    #[must_use]
    pub fn get(&self) -> &T {
        self.value.get_or_init(|| (self.init)())
    }
}

impl<T: Send + Sync> Deref for LazyObject<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        self.get()
    }
}

/// Create a lazily initialized value.
#[must_use]
pub fn lazy<T: Send + Sync>(init: impl Fn() -> T + Send + Sync + 'static) -> LazyObject<T> {
    LazyObject::new(init)
}

/// Split a string on the first occurrence of `sep`, matching Python's
/// `str.partition()` behavior.
///
/// # Panics
///
/// Panics if `sep` is empty, matching Python's `ValueError` for an empty
/// separator.
#[must_use]
pub fn partition<'a>(s: &'a str, sep: &str) -> (&'a str, &'a str, &'a str) {
    assert!(!sep.is_empty(), "empty separator");

    match s.find(sep) {
        Some(index) => {
            let tail_index = index + sep.len();
            (&s[..index], &s[index..tail_index], &s[tail_index..])
        }
        None => (s, "", ""),
    }
}

#[cfg(test)]
mod tests {
    use std::sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
    };
    use std::thread;

    use super::{LazyObject, lazy, partition};

    fn assert_send_sync<T: Send + Sync>() {}

    #[test]
    fn test_lazy_object_defers_initialization_until_first_access() {
        let calls = Arc::new(AtomicUsize::new(0));
        let init_calls = Arc::clone(&calls);
        let lazy_value = LazyObject::new(move || {
            init_calls.fetch_add(1, Ordering::SeqCst);
            String::from("ready")
        });

        assert_eq!(calls.load(Ordering::SeqCst), 0);
        assert_eq!(lazy_value.get(), "ready");
        assert_eq!(calls.load(Ordering::SeqCst), 1);
        assert_eq!(lazy_value.get(), "ready");
        assert_eq!(calls.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn test_lazy_object_is_thread_safe_and_initializes_once() {
        let calls = Arc::new(AtomicUsize::new(0));
        let init_calls = Arc::clone(&calls);
        let lazy_value: Arc<LazyObject<usize>> = Arc::new(LazyObject::new(move || {
            init_calls.fetch_add(1, Ordering::SeqCst);
            42
        }));

        let handles: Vec<_> = (0..8)
            .map(|_| {
                let lazy_value = Arc::clone(&lazy_value);
                thread::spawn(move || *lazy_value.get())
            })
            .collect();

        let values: Vec<_> = handles
            .into_iter()
            .map(|handle| handle.join().expect("thread should finish"))
            .collect();

        assert_eq!(values, vec![42; 8]);
        assert_eq!(calls.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn test_lazy_object_deref_forwards_access() {
        let lazy_value = LazyObject::new(|| String::from("hello"));

        assert_eq!(lazy_value.len(), 5);
        assert_eq!(&*lazy_value, "hello");
    }

    #[test]
    fn test_lazy_object_is_send_and_sync() {
        assert_send_sync::<LazyObject<String>>();
    }

    #[test]
    fn test_lazy_convenience_constructor() {
        let lazy_value = lazy(|| 7_u32);

        assert_eq!(*lazy_value.get(), 7);
    }

    #[test]
    fn test_lazy_object() {
        let calls = Arc::new(AtomicUsize::new(0));
        let init_calls = Arc::clone(&calls);
        let lazy_value = lazy(move || {
            init_calls.fetch_add(1, Ordering::SeqCst);
            vec![0, 1, 2]
        });

        assert_eq!(calls.load(Ordering::SeqCst), 0);
        assert_eq!(lazy_value.len(), 3);
        assert_eq!(lazy_value[1], 1);
        assert_eq!(calls.load(Ordering::SeqCst), 1);
        assert_eq!(lazy_value.first(), Some(&0));
        assert_eq!(calls.load(Ordering::SeqCst), 1);
    }

    #[test]
    fn test_partition() {
        let cases = [
            ("alpha-beta-gamma", "-", ("alpha", "-", "beta-gamma")),
            ("alphabetagamma", "-", ("alphabetagamma", "", "")),
            ("", ":", ("", "", "")),
            ("key::value", "::", ("key", "::", "value")),
        ];

        for (input, separator, expected) in cases {
            assert_eq!(partition(input, separator), expected);
        }
    }

    #[test]
    fn test_partition_with_separator_present() {
        assert_eq!(
            partition("alpha-beta-gamma", "-"),
            ("alpha", "-", "beta-gamma")
        );
    }

    #[test]
    fn test_partition_without_separator_present() {
        assert_eq!(partition("alphabetagamma", "-"), ("alphabetagamma", "", ""));
    }

    #[test]
    fn test_partition_with_empty_input() {
        assert_eq!(partition("", ":"), ("", "", ""));
    }

    #[test]
    #[should_panic(expected = "empty separator")]
    fn test_partition_rejects_empty_separator() {
        let _ = partition("abc", "");
    }
}