quarto_source_map/
context.rs1use crate::file_info::FileInformation;
4use crate::types::FileId;
5use serde::{Deserialize, Serialize};
6
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SourceContext {
12 files: Vec<SourceFile>,
13 #[serde(skip_serializing_if = "HashMap::is_empty", default)]
16 file_id_map: HashMap<usize, usize>, }
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SourceFile {
22 pub path: String,
24 #[serde(skip_serializing_if = "Option::is_none")]
28 pub content: Option<String>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub file_info: Option<FileInformation>,
32 pub metadata: FileMetadata,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct FileMetadata {
39 pub file_type: Option<String>,
41}
42
43impl SourceContext {
44 pub fn new() -> Self {
46 SourceContext {
47 files: Vec::new(),
48 file_id_map: HashMap::new(),
49 }
50 }
51
52 pub fn add_file(&mut self, path: String, content: Option<String>) -> FileId {
60 let id = FileId(self.files.len());
61
62 let (stored_content, content_for_info) = match content {
65 Some(c) => {
66 (Some(c.clone()), Some(c))
68 }
69 None => {
70 (None, std::fs::read_to_string(&path).ok())
72 }
73 };
74
75 let file_info = content_for_info.as_ref().map(|c| FileInformation::new(c));
76 self.files.push(SourceFile {
77 path,
78 content: stored_content,
79 file_info,
80 metadata: FileMetadata { file_type: None },
81 });
82 id
83 }
84
85 pub fn add_file_with_info(&mut self, path: String, file_info: FileInformation) -> FileId {
94 let id = FileId(self.files.len());
95 self.files.push(SourceFile {
96 path,
97 content: None,
98 file_info: Some(file_info),
99 metadata: FileMetadata { file_type: None },
100 });
101 id
102 }
103
104 pub fn add_file_with_id(
113 &mut self,
114 id: FileId,
115 path: String,
116 content: Option<String>,
117 ) -> FileId {
118 if self.get_file(id).is_some() {
120 panic!("FileId {:?} already exists in SourceContext", id);
121 }
122
123 let (stored_content, content_for_info) = match content {
125 Some(c) => (Some(c.clone()), Some(c)),
126 None => (None, std::fs::read_to_string(&path).ok()),
127 };
128
129 let file_info = content_for_info.as_ref().map(|c| FileInformation::new(c));
130
131 let index = self.files.len();
133 self.files.push(SourceFile {
134 path,
135 content: stored_content,
136 file_info,
137 metadata: FileMetadata { file_type: None },
138 });
139
140 self.file_id_map.insert(id.0, index);
142
143 id
144 }
145
146 pub fn get_file(&self, id: FileId) -> Option<&SourceFile> {
148 if let Some(&index) = self.file_id_map.get(&id.0) {
150 return self.files.get(index);
151 }
152
153 self.files.get(id.0)
155 }
156
157 pub fn without_content(&self) -> Self {
163 SourceContext {
164 files: self
165 .files
166 .iter()
167 .map(|f| SourceFile {
168 path: f.path.clone(),
169 content: f.content.clone(), file_info: None,
171 metadata: f.metadata.clone(),
172 })
173 .collect(),
174 file_id_map: self.file_id_map.clone(), }
176 }
177}
178
179impl Default for SourceContext {
180 fn default() -> Self {
181 Self::new()
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_empty_context() {
191 let ctx = SourceContext::new();
192 assert!(ctx.get_file(FileId(0)).is_none());
193 }
194
195 #[test]
196 fn test_add_and_get_file() {
197 let mut ctx = SourceContext::new();
198 let id = ctx.add_file("test.qmd".to_string(), Some("# Hello".to_string()));
199
200 assert_eq!(id, FileId(0));
201 let file = ctx.get_file(id).unwrap();
202 assert_eq!(file.path, "test.qmd");
203 assert!(file.file_info.is_some());
204
205 let info = file.file_info.as_ref().unwrap();
207 assert_eq!(info.total_length(), 7);
208 }
209
210 #[test]
211 fn test_multiple_files() {
212 let mut ctx = SourceContext::new();
213 let id1 = ctx.add_file("first.qmd".to_string(), Some("First".to_string()));
214 let id2 = ctx.add_file("second.qmd".to_string(), Some("Second".to_string()));
215
216 assert_eq!(id1, FileId(0));
217 assert_eq!(id2, FileId(1));
218
219 let file1 = ctx.get_file(id1).unwrap();
220 let file2 = ctx.get_file(id2).unwrap();
221
222 assert_eq!(file1.path, "first.qmd");
223 assert_eq!(file2.path, "second.qmd");
224 assert!(file1.file_info.is_some());
225 assert!(file2.file_info.is_some());
226 assert_eq!(file1.file_info.as_ref().unwrap().total_length(), 5);
227 assert_eq!(file2.file_info.as_ref().unwrap().total_length(), 6);
228 }
229
230 #[test]
231 fn test_file_without_content() {
232 let mut ctx = SourceContext::new();
233 let id = ctx.add_file("no-content.qmd".to_string(), None);
234
235 let file = ctx.get_file(id).unwrap();
236 assert_eq!(file.path, "no-content.qmd");
237 assert!(file.file_info.is_none());
238 }
239
240 #[test]
241 fn test_without_content() {
242 let mut ctx = SourceContext::new();
243 ctx.add_file("test1.qmd".to_string(), Some("Content 1".to_string()));
244 ctx.add_file("test2.qmd".to_string(), Some("Content 2".to_string()));
245
246 let ctx_no_content = ctx.without_content();
247
248 let file1 = ctx_no_content.get_file(FileId(0)).unwrap();
249 let file2 = ctx_no_content.get_file(FileId(1)).unwrap();
250
251 assert_eq!(file1.path, "test1.qmd");
252 assert_eq!(file2.path, "test2.qmd");
253 assert!(file1.file_info.is_none());
254 assert!(file2.file_info.is_none());
255 }
256
257 #[test]
258 fn test_serialization() {
259 let mut ctx = SourceContext::new();
260 ctx.add_file("test.qmd".to_string(), Some("# Test".to_string()));
261
262 let json = serde_json::to_string(&ctx).unwrap();
263 let deserialized: SourceContext = serde_json::from_str(&json).unwrap();
264
265 let file = deserialized.get_file(FileId(0)).unwrap();
266 assert_eq!(file.path, "test.qmd");
267 assert!(file.file_info.is_some());
268 assert_eq!(file.file_info.as_ref().unwrap().total_length(), 6);
269 }
270
271 #[test]
272 fn test_serialization_without_content() {
273 let mut ctx = SourceContext::new();
274 ctx.add_file("test.qmd".to_string(), Some("# Test".to_string()));
275
276 let ctx_no_content = ctx.without_content();
277 let json = serde_json::to_string(&ctx_no_content).unwrap();
278
279 assert!(!json.contains("\"file_info\""));
281 }
282}