1use std::collections::BTreeMap;
16use std::io;
17use std::path::{Path, PathBuf};
18
19pub(crate) struct FileSystem {
21 overlay: BTreeMap<PathBuf, String>,
22}
23
24impl FileSystem {
25 pub(crate) fn new() -> Self {
27 Self {
28 overlay: BTreeMap::new(),
29 }
30 }
31
32 pub(crate) fn with_overlay(overlay: BTreeMap<PathBuf, String>) -> Self {
36 Self { overlay }
37 }
38
39 pub(crate) fn from_overlay_file(path: &Path) -> io::Result<Self> {
44 let raw = std::fs::read_to_string(path)?;
45 let map: BTreeMap<PathBuf, String> = serde_json::from_str(&raw)
46 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
47 Ok(Self::with_overlay(map))
48 }
49
50 pub(crate) fn read_to_string(&self, path: &Path) -> io::Result<String> {
52 if let Some(text) = self.overlay.get(path) {
53 return Ok(text.clone());
54 }
55 std::fs::read_to_string(path)
56 }
57
58 pub(crate) fn is_overlay(&self, path: &Path) -> bool {
63 self.overlay.contains_key(path)
64 }
65}
66
67impl Default for FileSystem {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[mz_ore::test]
78 fn overlay_intercepts_read() {
79 let tmp = tempfile::tempdir().unwrap();
80 let file = tmp.path().join("a.sql");
81 std::fs::write(&file, "disk").unwrap();
82
83 let mut overlay = BTreeMap::new();
84 overlay.insert(file.clone(), "buffer".to_string());
85 let fs = FileSystem::with_overlay(overlay);
86
87 assert_eq!(fs.read_to_string(&file).unwrap(), "buffer");
88 assert!(fs.is_overlay(&file));
89 }
90
91 #[mz_ore::test]
92 fn no_overlay_reads_disk() {
93 let tmp = tempfile::tempdir().unwrap();
94 let file = tmp.path().join("a.sql");
95 std::fs::write(&file, "disk").unwrap();
96 let fs = FileSystem::new();
97 assert_eq!(fs.read_to_string(&file).unwrap(), "disk");
98 assert!(!fs.is_overlay(&file));
99 }
100
101 #[mz_ore::test]
102 fn from_overlay_file_round_trips() {
103 let tmp = tempfile::tempdir().unwrap();
104 let target = tmp.path().join("models/x.sql");
105 let overlay_json = tmp.path().join("overlay.json");
106
107 let body = format!(
109 "{{\"{}\":\"SELECT 42\"}}",
110 target.display().to_string().replace('\\', "\\\\"),
111 );
112 std::fs::write(&overlay_json, body).unwrap();
113
114 let fs = FileSystem::from_overlay_file(&overlay_json).unwrap();
115 assert_eq!(fs.read_to_string(&target).unwrap(), "SELECT 42");
116 assert!(fs.is_overlay(&target));
117 }
118
119 #[mz_ore::test]
120 fn from_overlay_file_rejects_garbage() {
121 let tmp = tempfile::tempdir().unwrap();
122 let overlay_json = tmp.path().join("overlay.json");
123 std::fs::write(&overlay_json, "not json").unwrap();
124
125 let err = FileSystem::from_overlay_file(&overlay_json)
126 .err()
127 .expect("garbage JSON must be rejected");
128 assert_eq!(err.kind(), io::ErrorKind::InvalidData);
129 }
130}