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, file_info) = match content {
65 Some(c) => {
66 let info = FileInformation::new(&c);
68 (Some(c), Some(info))
69 }
70 None => {
71 let info = std::fs::read_to_string(&path)
73 .ok()
74 .map(|c| FileInformation::new(&c));
75 (None, info)
76 }
77 };
78 self.files.push(SourceFile {
79 path,
80 content: stored_content,
81 file_info,
82 metadata: FileMetadata { file_type: None },
83 });
84 id
85 }
86
87 pub fn add_file_with_info(&mut self, path: String, file_info: FileInformation) -> FileId {
96 let id = FileId(self.files.len());
97 self.files.push(SourceFile {
98 path,
99 content: None,
100 file_info: Some(file_info),
101 metadata: FileMetadata { file_type: None },
102 });
103 id
104 }
105
106 pub fn add_file_with_id(
115 &mut self,
116 id: FileId,
117 path: String,
118 content: Option<String>,
119 ) -> FileId {
120 if self.get_file(id).is_some() {
122 panic!("FileId {:?} already exists in SourceContext", id);
123 }
124
125 let (stored_content, file_info) = match content {
127 Some(c) => {
128 let info = FileInformation::new(&c);
129 (Some(c), Some(info))
130 }
131 None => {
132 let info = std::fs::read_to_string(&path)
133 .ok()
134 .map(|c| FileInformation::new(&c));
135 (None, info)
136 }
137 };
138
139 let index = self.files.len();
141 self.files.push(SourceFile {
142 path,
143 content: stored_content,
144 file_info,
145 metadata: FileMetadata { file_type: None },
146 });
147
148 self.file_id_map.insert(id.0, index);
150
151 id
152 }
153
154 pub fn get_file(&self, id: FileId) -> Option<&SourceFile> {
156 if let Some(&index) = self.file_id_map.get(&id.0) {
158 return self.files.get(index);
159 }
160
161 self.files.get(id.0)
163 }
164
165 pub fn without_content(&self) -> Self {
171 SourceContext {
172 files: self
173 .files
174 .iter()
175 .map(|f| SourceFile {
176 path: f.path.clone(),
177 content: f.content.clone(), file_info: None,
179 metadata: f.metadata.clone(),
180 })
181 .collect(),
182 file_id_map: self.file_id_map.clone(), }
184 }
185}
186
187impl Default for SourceContext {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn test_empty_context() {
199 let ctx = SourceContext::new();
200 assert!(ctx.get_file(FileId(0)).is_none());
201 }
202
203 #[test]
204 fn test_add_and_get_file() {
205 let mut ctx = SourceContext::new();
206 let id = ctx.add_file("test.qmd".to_string(), Some("# Hello".to_string()));
207
208 assert_eq!(id, FileId(0));
209 let file = ctx.get_file(id).unwrap();
210 assert_eq!(file.path, "test.qmd");
211 assert!(file.file_info.is_some());
212
213 let info = file.file_info.as_ref().unwrap();
215 assert_eq!(info.total_length(), 7);
216 }
217
218 #[test]
219 fn test_multiple_files() {
220 let mut ctx = SourceContext::new();
221 let id1 = ctx.add_file("first.qmd".to_string(), Some("First".to_string()));
222 let id2 = ctx.add_file("second.qmd".to_string(), Some("Second".to_string()));
223
224 assert_eq!(id1, FileId(0));
225 assert_eq!(id2, FileId(1));
226
227 let file1 = ctx.get_file(id1).unwrap();
228 let file2 = ctx.get_file(id2).unwrap();
229
230 assert_eq!(file1.path, "first.qmd");
231 assert_eq!(file2.path, "second.qmd");
232 assert!(file1.file_info.is_some());
233 assert!(file2.file_info.is_some());
234 assert_eq!(file1.file_info.as_ref().unwrap().total_length(), 5);
235 assert_eq!(file2.file_info.as_ref().unwrap().total_length(), 6);
236 }
237
238 #[test]
239 fn test_file_without_content() {
240 let mut ctx = SourceContext::new();
241 let id = ctx.add_file("no-content.qmd".to_string(), None);
242
243 let file = ctx.get_file(id).unwrap();
244 assert_eq!(file.path, "no-content.qmd");
245 assert!(file.file_info.is_none());
246 }
247
248 #[test]
249 fn test_without_content() {
250 let mut ctx = SourceContext::new();
251 ctx.add_file("test1.qmd".to_string(), Some("Content 1".to_string()));
252 ctx.add_file("test2.qmd".to_string(), Some("Content 2".to_string()));
253
254 let ctx_no_content = ctx.without_content();
255
256 let file1 = ctx_no_content.get_file(FileId(0)).unwrap();
257 let file2 = ctx_no_content.get_file(FileId(1)).unwrap();
258
259 assert_eq!(file1.path, "test1.qmd");
260 assert_eq!(file2.path, "test2.qmd");
261 assert!(file1.file_info.is_none());
262 assert!(file2.file_info.is_none());
263 }
264
265 #[test]
266 fn test_serialization() {
267 let mut ctx = SourceContext::new();
268 ctx.add_file("test.qmd".to_string(), Some("# Test".to_string()));
269
270 let json = serde_json::to_string(&ctx).unwrap();
271 let deserialized: SourceContext = serde_json::from_str(&json).unwrap();
272
273 let file = deserialized.get_file(FileId(0)).unwrap();
274 assert_eq!(file.path, "test.qmd");
275 assert!(file.file_info.is_some());
276 assert_eq!(file.file_info.as_ref().unwrap().total_length(), 6);
277 }
278
279 #[test]
280 fn test_serialization_without_content() {
281 let mut ctx = SourceContext::new();
282 ctx.add_file("test.qmd".to_string(), Some("# Test".to_string()));
283
284 let ctx_no_content = ctx.without_content();
285 let json = serde_json::to_string(&ctx_no_content).unwrap();
286
287 assert!(!json.contains("\"file_info\""));
289 }
290}