1use ahash::HashMap;
2use async_trait::async_trait;
3use kcl_error::KclError;
4use kcl_error::KclErrorDetails;
5use kcl_error::SourceRange;
6
7use crate::TypedPath;
8
9#[derive(Clone, Eq, PartialEq)]
10pub struct InMemoryFiles {
11 files: HashMap<String, Vec<u8>>,
12}
13
14impl InMemoryFiles {
15 pub fn new(files: HashMap<String, Vec<u8>>) -> Self {
16 Self { files }
17 }
18}
19
20impl std::fmt::Debug for InMemoryFiles {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 let filenames: Vec<_> = self.files.keys().collect();
23 f.debug_struct("InMemoryFiles").field("files", &filenames).finish()
24 }
25}
26
27#[async_trait]
28impl crate::fs::FileSystem for InMemoryFiles {
29 async fn read(&self, path: &TypedPath, source_range: SourceRange) -> Result<Vec<u8>, KclError> {
30 let path_str = path.to_string();
31 let bytes = self.files.get(&path_str).ok_or_else(|| {
32 KclError::new_io(KclErrorDetails::new(
33 format!("No such file {} found", path.display()),
34 vec![source_range],
35 ))
36 })?;
37 Ok(bytes.to_owned())
38 }
39
40 async fn read_to_string(&self, path: &TypedPath, source_range: SourceRange) -> Result<String, KclError> {
41 let bytes = self.read(path, source_range).await?;
42 String::from_utf8(bytes).map_err(|err| {
43 KclError::new_io(KclErrorDetails::new(
44 format!("File {} had invalid UTF-8 bytes: {err}", path.display()),
45 vec![source_range],
46 ))
47 })
48 }
49
50 async fn exists(&self, path: &TypedPath, _source_range: SourceRange) -> Result<bool, crate::errors::KclError> {
51 Ok(self.files.contains_key(&path.to_string()))
52 }
53
54 async fn get_all_files(
55 &self,
56 base_path: &TypedPath,
57 _source_range: SourceRange,
58 ) -> Result<Vec<TypedPath>, crate::errors::KclError> {
59 let all_files = self
60 .files
61 .keys()
62 .map(|filename| TypedPath::new(filename))
63 .filter(|file| file.starts_with(base_path))
64 .collect();
65 Ok(all_files)
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use ahash::HashMap;
72 use kcl_error::SourceRange;
73
74 use super::InMemoryFiles;
75 use crate::TypedPath;
76 use crate::fs::FileSystem;
77
78 fn files(entries: &[(&str, &[u8])]) -> InMemoryFiles {
79 InMemoryFiles::new(
80 entries
81 .iter()
82 .map(|(path, bytes)| ((*path).to_owned(), (*bytes).to_vec()))
83 .collect::<HashMap<_, _>>(),
84 )
85 }
86
87 #[tokio::test]
88 async fn reads_existing_file_as_bytes_and_string() {
89 let fs = files(&[("/project/main.kcl", b"part = 1")]);
90 let path = TypedPath::new("/project/main.kcl");
91
92 assert_eq!(
93 fs.read(&path, SourceRange::default()).await.unwrap(),
94 b"part = 1".to_vec()
95 );
96 assert_eq!(
97 fs.read_to_string(&path, SourceRange::default()).await.unwrap(),
98 "part = 1"
99 );
100 }
101
102 #[tokio::test]
103 async fn reports_file_existence() {
104 let fs = files(&[("/project/main.kcl", b"part = 1")]);
105
106 assert!(
107 fs.exists(&TypedPath::new("/project/main.kcl"), SourceRange::default())
108 .await
109 .unwrap()
110 );
111 assert!(
112 !fs.exists(&TypedPath::new("/project/missing.kcl"), SourceRange::default())
113 .await
114 .unwrap()
115 );
116 }
117
118 #[tokio::test]
119 async fn read_errors_when_file_is_missing() {
120 let fs = files(&[]);
121
122 let err = fs
123 .read(&TypedPath::new("/project/missing.kcl"), SourceRange::default())
124 .await
125 .unwrap_err();
126
127 assert!(err.to_string().contains("No such file /project/missing.kcl found"));
128 }
129
130 #[tokio::test]
131 async fn read_to_string_errors_when_file_is_not_utf8() {
132 let fs = files(&[("/project/binary.kcl", &[0xff, 0xfe])]);
133
134 let err = fs
135 .read_to_string(&TypedPath::new("/project/binary.kcl"), SourceRange::default())
136 .await
137 .unwrap_err();
138
139 assert!(
140 err.to_string()
141 .contains("File /project/binary.kcl had invalid UTF-8 bytes")
142 );
143 }
144
145 #[tokio::test]
146 async fn get_all_files_returns_files_under_base_path() {
147 let fs = files(&[
148 ("/project/main.kcl", b"main"),
149 ("/project/sub/part.kcl", b"part"),
150 ("/projectile/not-project.kcl", b"outside"),
151 ("/other/file.kcl", b"other"),
152 ]);
153
154 let mut files = fs
155 .get_all_files(&TypedPath::new("/project"), SourceRange::default())
156 .await
157 .unwrap()
158 .into_iter()
159 .map(|path| path.to_string())
160 .collect::<Vec<_>>();
161 files.sort();
162
163 assert_eq!(files, vec!["/project/main.kcl", "/project/sub/part.kcl"]);
164 }
165}