Skip to main content

vdsl_sync/domain/
file_type.rs

1//! File type classification for sync tracking.
2//!
3//! vdsl-sync is a generic distributed file storage engine.
4//! FileType classifies tracked files at the storage level only.
5//!
6//! - **Image** — generated images (PNG, JPG, etc.). First-class entity
7//!   that may embed generation origin (Recipe/Workflow) in metadata.
8//! - **Asset** — all other files (JSON, text, config, raw recipes, etc.).
9//!
10//! Domain-specific semantics (e.g., "this JSON is a ComfyUI recipe")
11//! belong in the consuming crate (vdsl-mcp), not here.
12
13use serde::{Deserialize, Serialize};
14use std::fmt;
15
16use super::error::DomainError;
17
18/// Type of file tracked by the sync engine.
19///
20/// Intentionally minimal — only distinguishes files that require
21/// different storage-level handling (e.g., content hashing strategy).
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum FileType {
25    /// Generated image (PNG, JPG, etc.).
26    /// May embed generation origin in metadata (PNG tEXt, EXIF, etc.).
27    Image,
28    /// General asset file (JSON, text, config, raw recipe, DB, etc.).
29    Asset,
30}
31
32impl FileType {
33    pub fn as_str(&self) -> &'static str {
34        match self {
35            Self::Image => "image",
36            Self::Asset => "asset",
37        }
38    }
39
40    /// Infer file type from a file extension (case-insensitive).
41    ///
42    /// Returns `Asset` for unrecognized extensions.
43    pub fn from_extension(ext: &str) -> Self {
44        match ext.to_ascii_lowercase().as_str() {
45            "png" | "jpg" | "jpeg" | "webp" | "bmp" | "gif" | "tiff" | "tif" => Self::Image,
46            _ => Self::Asset,
47        }
48    }
49}
50
51impl fmt::Display for FileType {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.write_str(self.as_str())
54    }
55}
56
57impl std::str::FromStr for FileType {
58    type Err = DomainError;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        match s {
62            "image" => Ok(Self::Image),
63            "asset" => Ok(Self::Asset),
64            other => Err(DomainError::InvalidFileType(other.to_string())),
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn roundtrip() {
75        for ft in [FileType::Image, FileType::Asset] {
76            let s = ft.as_str();
77            let parsed: FileType = s.parse().expect("should parse");
78            assert_eq!(parsed, ft);
79        }
80    }
81
82    #[test]
83    fn invalid_type() {
84        let result: Result<FileType, _> = "video".parse();
85        assert!(result.is_err());
86    }
87
88    #[test]
89    fn invalid_legacy_recipe() {
90        let result: Result<FileType, _> = "recipe".parse();
91        assert!(result.is_err(), "legacy 'recipe' must not parse");
92    }
93
94    #[test]
95    fn invalid_legacy_db() {
96        let result: Result<FileType, _> = "db".parse();
97        assert!(result.is_err(), "legacy 'db' must not parse");
98    }
99
100    #[test]
101    fn display() {
102        assert_eq!(format!("{}", FileType::Image), "image");
103        assert_eq!(format!("{}", FileType::Asset), "asset");
104    }
105
106    #[test]
107    fn from_extension_images() {
108        for ext in &[
109            "png", "jpg", "jpeg", "webp", "bmp", "gif", "tiff", "tif", "PNG", "Jpg",
110        ] {
111            assert_eq!(FileType::from_extension(ext), FileType::Image);
112        }
113    }
114
115    #[test]
116    fn from_extension_assets() {
117        for ext in &["json", "txt", "db", "sqlite", "toml", "yaml", "csv"] {
118            assert_eq!(FileType::from_extension(ext), FileType::Asset);
119        }
120    }
121
122    #[test]
123    fn serde_roundtrip() {
124        let ft = FileType::Asset;
125        let json = serde_json::to_string(&ft).unwrap();
126        assert_eq!(json, "\"asset\"");
127        let back: FileType = serde_json::from_str(&json).unwrap();
128        assert_eq!(back, ft);
129    }
130}