rjango 0.1.1

A full-stack Rust backend framework inspired by Django
Documentation
use std::fs;
use std::path::PathBuf;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UploadedFile {
    pub name: String,
    pub size: u64,
    pub content_type: String,
    pub content: Vec<u8>,
}

impl UploadedFile {
    #[must_use]
    pub fn new(name: impl Into<String>, content_type: impl Into<String>, content: Vec<u8>) -> Self {
        Self {
            name: name.into(),
            size: content.len() as u64,
            content_type: content_type.into(),
            content,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InMemoryUploadedFile {
    pub file: UploadedFile,
}

impl InMemoryUploadedFile {
    #[must_use]
    pub fn new(name: impl Into<String>, content_type: impl Into<String>, content: Vec<u8>) -> Self {
        Self {
            file: UploadedFile::new(name, content_type, content),
        }
    }

    #[must_use]
    pub fn into_uploaded_file(self) -> UploadedFile {
        self.file
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TemporaryUploadedFile {
    pub path: PathBuf,
    pub name: String,
    pub size: u64,
    pub content_type: String,
}

impl TemporaryUploadedFile {
    #[must_use]
    pub fn new(
        path: PathBuf,
        name: impl Into<String>,
        content_type: impl Into<String>,
        size: u64,
    ) -> Self {
        Self {
            path,
            name: name.into(),
            size,
            content_type: content_type.into(),
        }
    }

    pub fn read(&self) -> std::io::Result<Vec<u8>> {
        fs::read(&self.path)
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::path::{Path, PathBuf};
    use std::process;
    use std::sync::atomic::{AtomicU64, Ordering};
    use std::time::{SystemTime, UNIX_EPOCH};

    use super::{InMemoryUploadedFile, TemporaryUploadedFile, UploadedFile};

    static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);

    struct TestDir {
        path: PathBuf,
    }

    impl TestDir {
        fn new() -> Self {
            let nanos = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .expect("clock should be after epoch")
                .as_nanos();
            let counter = UNIQUE_COUNTER.fetch_add(1, Ordering::Relaxed);
            let path = std::env::temp_dir().join(format!(
                "rjango-uploaded-file-test-{}-{nanos}-{counter}",
                process::id()
            ));
            fs::create_dir_all(&path).expect("create test directory");
            Self { path }
        }

        fn path(&self) -> &Path {
            &self.path
        }
    }

    impl Drop for TestDir {
        fn drop(&mut self) {
            let _ = fs::remove_dir_all(&self.path);
        }
    }

    #[test]
    fn uploaded_file_new_sets_metadata() {
        let uploaded = UploadedFile::new("avatar.png", "image/png", vec![1, 2, 3, 4]);

        assert_eq!(uploaded.name, "avatar.png");
        assert_eq!(uploaded.size, 4);
        assert_eq!(uploaded.content_type, "image/png");
        assert_eq!(uploaded.content, vec![1, 2, 3, 4]);
    }

    #[test]
    fn in_memory_uploaded_file_wraps_uploaded_file() {
        let uploaded = InMemoryUploadedFile::new("notes.txt", "text/plain", b"hi".to_vec())
            .into_uploaded_file();

        assert_eq!(uploaded.name, "notes.txt");
        assert_eq!(uploaded.size, 2);
        assert_eq!(uploaded.content_type, "text/plain");
    }

    #[test]
    fn temporary_uploaded_file_reads_from_disk() {
        let dir = TestDir::new();
        let path = dir.path().join("upload.bin");
        fs::write(&path, [9, 8, 7]).expect("write upload file");

        let uploaded =
            TemporaryUploadedFile::new(path.clone(), "upload.bin", "application/octet-stream", 3);

        assert_eq!(uploaded.path, path);
        assert_eq!(uploaded.read().expect("read upload file"), vec![9, 8, 7]);
    }
}