1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use handlebars::Handlebars;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11#[derive(Debug, Clone)]
13pub struct VirtualFileSystem {
14 #[allow(dead_code)]
15 root: PathBuf,
16 files: Arc<RwLock<HashMap<PathBuf, VirtualFile>>>,
17 fixtures: Arc<RwLock<HashMap<PathBuf, FileFixture>>>,
18}
19
20impl VirtualFileSystem {
21 pub fn new(root: PathBuf) -> Self {
22 Self {
23 root,
24 files: Arc::new(RwLock::new(HashMap::new())),
25 fixtures: Arc::new(RwLock::new(HashMap::new())),
26 }
27 }
28
29 pub fn add_file(&self, path: PathBuf, file: VirtualFile) -> Result<()> {
30 let mut files = self.files.blocking_write();
31 files.insert(path, file);
32 Ok(())
33 }
34
35 pub fn get_file(&self, path: &Path) -> Option<VirtualFile> {
36 let files = self.files.blocking_read();
37 if let Some(file) = files.get(path) {
38 return Some(file.clone());
39 }
40
41 let fixtures = self.fixtures.blocking_read();
43 if let Some(fixture) = fixtures.get(path) {
44 return Some(fixture.clone().to_virtual_file());
45 }
46
47 None
48 }
49
50 pub fn remove_file(&self, path: &Path) -> Result<()> {
51 let mut files = self.files.blocking_write();
52 files.remove(path);
53 Ok(())
54 }
55
56 pub fn list_files(&self, path: &Path) -> Vec<VirtualFile> {
57 let files = self.files.blocking_read();
58 files
59 .iter()
60 .filter(|(file_path, _)| file_path.starts_with(path))
61 .map(|(_, file)| file.clone())
62 .collect()
63 }
64
65 pub fn clear(&self) -> Result<()> {
66 let mut files = self.files.blocking_write();
67 files.clear();
68 Ok(())
69 }
70
71 pub fn add_fixture(&self, fixture: FileFixture) -> Result<()> {
72 let mut fixtures = self.fixtures.blocking_write();
73 fixtures.insert(fixture.path.clone(), fixture);
74 Ok(())
75 }
76
77 pub fn load_fixtures(&self, fixtures: Vec<FileFixture>) -> Result<()> {
78 for fixture in fixtures {
79 self.add_fixture(fixture)?;
80 }
81 Ok(())
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct VirtualFile {
88 pub path: PathBuf,
89 pub content: FileContent,
90 pub metadata: FileMetadata,
91 pub created_at: DateTime<Utc>,
92 pub modified_at: DateTime<Utc>,
93}
94
95impl VirtualFile {
96 pub fn new(path: PathBuf, content: FileContent, metadata: FileMetadata) -> Self {
97 let now = Utc::now();
98 Self {
99 path,
100 content,
101 metadata,
102 created_at: now,
103 modified_at: now,
104 }
105 }
106
107 pub fn render_content(&self) -> Result<Vec<u8>> {
108 match &self.content {
109 FileContent::Static(data) => Ok(data.clone()),
110 FileContent::Template(template) => {
111 let handlebars = Handlebars::new();
113 let context = create_template_context();
114 let rendered = handlebars.render_template(template, &context)?;
115 Ok(rendered.into_bytes())
116 }
117 FileContent::Generated { size, pattern } => match pattern {
118 GenerationPattern::Random => Ok((0..*size).map(|_| rand::random::<u8>()).collect()),
119 GenerationPattern::Zeros => Ok(vec![0; *size]),
120 GenerationPattern::Ones => Ok(vec![1; *size]),
121 GenerationPattern::Incremental => Ok((0..*size).map(|i| (i % 256) as u8).collect()),
122 },
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
129pub enum FileContent {
130 Static(Vec<u8>),
131 Template(String),
132 Generated {
133 size: usize,
134 pattern: GenerationPattern,
135 },
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct FileMetadata {
141 pub permissions: String,
142 pub owner: String,
143 pub group: String,
144 pub size: u64,
145}
146
147impl Default for FileMetadata {
148 fn default() -> Self {
149 Self {
150 permissions: "644".to_string(),
151 owner: "mockforge".to_string(),
152 group: "users".to_string(),
153 size: 0,
154 }
155 }
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub enum GenerationPattern {
161 Random,
162 Zeros,
163 Ones,
164 Incremental,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct FileFixture {
170 pub path: PathBuf,
171 pub content: FileContent,
172 pub metadata: FileMetadata,
173}
174
175impl FileFixture {
176 pub fn to_virtual_file(self) -> VirtualFile {
177 VirtualFile::new(self.path, self.content, self.metadata)
178 }
179}
180
181fn create_template_context() -> Value {
183 let mut context = serde_json::Map::new();
184
185 let now = Utc::now();
187 context.insert("now".to_string(), Value::String(now.to_rfc3339()));
188 context.insert("timestamp".to_string(), Value::Number(now.timestamp().into()));
189 context.insert("date".to_string(), Value::String(now.format("%Y-%m-%d").to_string()));
190 context.insert("time".to_string(), Value::String(now.format("%H:%M:%S").to_string()));
191
192 context.insert("random_int".to_string(), Value::Number(rand::random::<i64>().into()));
194 context.insert(
195 "random_float".to_string(),
196 Value::String(format!("{:.6}", rand::random::<f64>())),
197 );
198
199 context.insert("uuid".to_string(), Value::String(uuid::Uuid::new_v4().to_string()));
201
202 let mut faker = serde_json::Map::new();
204 faker.insert("name".to_string(), Value::String("John Doe".to_string()));
205 faker.insert("email".to_string(), Value::String("john.doe@example.com".to_string()));
206 faker.insert("age".to_string(), Value::Number(30.into()));
207 context.insert("faker".to_string(), Value::Object(faker));
208
209 Value::Object(context)
210}