workspacer_crate/
read_file_string.rs

1// ---------------- [ File: workspacer-crate/src/read_file_string.rs ]
2crate::ix!();
3
4#[async_trait]
5impl ReadFileString for CrateHandle {
6    async fn read_file_string(&self, path: &Path) -> Result<String, CrateError> {
7        // The naive approach:
8        // let full_path = self.crate_path().join(path);
9
10        // The improved approach:
11        let mut full_path = path.to_path_buf();
12
13        // 1) If it's absolute, just use it
14        if !full_path.is_absolute() {
15            // 2) If it starts with our crate_path when interpreted as a string,
16            //    skip the join. E.g., if path = "workspacer-toml/src/imports.rs",
17            //    and crate_path is "workspacer-toml", we'd double up if we blindly do join.
18            let crate_str = self.crate_path().to_string_lossy().to_string();
19            let path_str = full_path.to_string_lossy().to_string();
20
21            if path_str.starts_with(&crate_str) {
22                // Already has crate_path as prefix, so leave it alone
23                debug!("Path is already under crate_path: {}", path_str);
24            } else {
25                // Otherwise, do the join
26                full_path = self.crate_path().join(path_str);
27            }
28        }
29
30        let content_result = fs::read_to_string(&full_path).await;
31        content_result.map_err(|io_err| CrateError::IoError {
32            io_error: Arc::new(io_err),
33            context: format!("Failed to read file: {}", full_path.display()),
34        })
35    }
36}
37
38#[cfg(test)]
39mod test_read_file_string {
40    use super::*;
41    use std::path::{Path, PathBuf};
42    use tempfile::tempdir;
43    use tokio::fs::{create_dir_all, File};
44    use tokio::io::AsyncWriteExt;
45    use std::io::Write;
46    
47
48    // ------------------------------------------------------------------------
49    // A minimal helper that implements `HasCargoTomlPathBuf` so we can create a CrateHandle.
50    // We'll just store a single directory as the "crate_path."
51    // ------------------------------------------------------------------------
52    #[derive(Clone)]
53    struct MockCratePath(PathBuf);
54
55    impl AsRef<Path> for MockCratePath {
56        fn as_ref(&self) -> &Path {
57            &self.0
58        }
59    }
60
61    // ------------------------------------------------------------------------
62    // Test fixture: Creates a real directory structure and a CrateHandle.
63    // ------------------------------------------------------------------------
64    async fn setup_crate_handle() -> CrateHandle {
65        // 1) Make a temp directory to simulate a crate root
66        let tmp_dir = tempdir().expect("Failed to create temp dir");
67        let crate_root = tmp_dir.path().to_path_buf();
68
69        // 2) We'll create a minimal Cargo.toml so the handle can be constructed
70        let cargo_toml_content = r#"
71            [package]
72            name = "mock_crate"
73            version = "0.1.0"
74            authors = ["Test <test@example.com>"]
75            license = "MIT"
76        "#;
77        let cargo_toml_path = crate_root.join("Cargo.toml");
78
79        {
80            let mut f = File::create(&cargo_toml_path)
81                .await
82                .expect("Failed to create Cargo.toml");
83            f.write_all(cargo_toml_content.as_bytes())
84                .await
85                .expect("Failed to write Cargo.toml");
86        }
87
88        // 3) Build a mock path object and then create the CrateHandle
89        let mock_path = MockCratePath(crate_root);
90        CrateHandle::new(&mock_path)
91            .await
92            .expect("Failed to create CrateHandle")
93    }
94
95    // ------------------------------------------------------------------------
96    // Write some test content to a file in the crate, returning the path.
97    // ------------------------------------------------------------------------
98    async fn write_file_in_crate(handle: &CrateHandle, relative_path: &str, content: &str) -> PathBuf {
99        let file_path = handle.as_ref().join(relative_path);
100        if let Some(parent) = file_path.parent() {
101            create_dir_all(parent)
102                .await
103                .expect("Failed to create parent directories");
104        }
105        let mut f = File::create(&file_path)
106            .await
107            .expect("Failed to create test file");
108        f.write_all(content.as_bytes())
109            .await
110            .expect("Failed to write test file content");
111        file_path
112    }
113
114    // ------------------------------------------------------------------------
115    // Test cases for read_file_string
116    // ------------------------------------------------------------------------
117
118    /// 1) If the path is already absolute, we just use that path (no crate_path join).
119    #[tokio::test]
120    async fn test_read_file_string_absolute_path() {
121        let handle = setup_crate_handle().await;
122        // We'll create a file outside the crate path (in another temp dir),
123        // then pass its absolute path to read_file_string.
124        let outside_tmp_dir = tempdir().expect("Failed to create second temp dir");
125        let outside_file_path = outside_tmp_dir.path().join("external_file.txt");
126
127        {
128            let mut f = std::fs::File::create(&outside_file_path)
129                .expect("Failed to create external file");
130            writeln!(f, "This is outside the crate path.").expect("Write failed");
131        }
132
133        let content = handle
134            .read_file_string(&outside_file_path)
135            .await
136            .expect("Failed to read external file with absolute path");
137        assert_eq!(
138            content.trim(),
139            "This is outside the crate path.",
140            "Should read from the absolute path directly"
141        );
142    }
143
144    /// 2) If the path is relative and already starts with the crate path as a string prefix, we do not join again.
145    ///    (In practice, this is a bit contrived, but let's test the logic.)
146    #[tokio::test]
147    async fn test_read_file_string_relative_already_prefixed() {
148        let handle = setup_crate_handle().await;
149
150        // We'll create a file *within* the crate path first
151        let relative_path_str = "nested/hello.txt";
152        let file_path_in_crate = write_file_in_crate(&handle, relative_path_str, "Hello, World").await;
153
154        // Now let's build a PathBuf that "looks" like it's already prefixed
155        // E.g., the user passes "crate_root/nested/hello.txt" but it's not absolute on some OS?
156        // This scenario might be OS or environment specific. We'll just forcibly do:
157        let path_str = file_path_in_crate.to_string_lossy().into_owned();
158        // Now we create a PathBuf from that string, but hopefully it doesn't parse as absolute
159        // if there's no leading slash or drive letter. We'll skip the advanced OS intricacies here.
160
161        // We'll see if the function detects that it "starts with crate_path" and doesn't re-join.
162        let content = handle
163            .read_file_string(Path::new(&path_str))
164            .await
165            .expect("Failed to read file with 'already prefixed' path");
166        assert_eq!(content, "Hello, World", "Should read the same content");
167    }
168
169    /// 3) If the path is relative and does NOT start with crate_path,
170    ///    we join it with crate_path.
171    #[tokio::test]
172    async fn test_read_file_string_relative_joined() {
173        let handle = setup_crate_handle().await;
174
175        // We'll create a file in "src/myfile.txt" inside the crate
176        let file_path = write_file_in_crate(&handle, "src/myfile.txt", "some data").await;
177
178        // We'll read via a relative path "src/myfile.txt"
179        let relative_path = Path::new("src/myfile.txt");
180        let content = handle
181            .read_file_string(relative_path)
182            .await
183            .expect("Failed to read file with relative path that does not match prefix");
184        assert_eq!(content, "some data");
185    }
186
187    /// 4) If the file doesn't exist, we expect an IoError with the correct context.
188    #[tokio::test]
189    async fn test_read_file_string_missing_file() {
190        let handle = setup_crate_handle().await;
191        let missing_path = Path::new("this_file_does_not_exist.txt");
192        let result = handle.read_file_string(missing_path).await;
193
194        assert!(result.is_err(), "Expected error for missing file");
195        match result {
196            Err(CrateError::IoError { context, .. }) => {
197                // Check that context includes "Failed to read file: <path>"
198                assert!(
199                    context.contains("Failed to read file"),
200                    "Error context should mention failed to read file"
201                );
202                // We could also check that it includes "this_file_does_not_exist.txt"
203                assert!(
204                    context.contains("this_file_does_not_exist.txt"),
205                    "Error context should mention the missing file"
206                );
207            }
208            other => panic!("Expected CrateError::IoError, got: {other:?}"),
209        }
210    }
211
212    /// 5) Test reading a file that is inside the crate path but specified with an absolute path
213    ///    to confirm we still read it without doubling the path.
214    #[tokio::test]
215    async fn test_read_file_string_same_crate_path_but_absolute() {
216        let handle = setup_crate_handle().await;
217        // Create a file in the crate
218        let relative_path = "docs/some_doc.txt";
219        let file_path = write_file_in_crate(&handle, relative_path, "doc content").await;
220        let absolute_path = file_path.canonicalize().expect("Failed to canonicalize path");
221
222        // Should read the file directly, no re-join
223        let content = handle
224            .read_file_string(&absolute_path)
225            .await
226            .expect("Failed to read doc content with absolute path");
227        assert_eq!(content, "doc content");
228    }
229
230    /// 6) (Optional) If we want to test edge cases like the path string partially matching the crate path
231    ///    but not from the start, we can do that. For example, if crate_path = "abc/def", and
232    ///    the user passes a path with "some/abc/def" in the middle. We'll see if it incorrectly
233    ///    tries to skip the join. The code might or might not handle this scenario. We'll do it for completeness.
234    #[tokio::test]
235    async fn test_read_file_string_partial_prefix() {
236        let handle = setup_crate_handle().await;
237        let crate_str = handle.crate_path().to_string_lossy().to_string();
238
239        // Suppose the crate path is "some_temp_dir_12345" 
240        // We create a partial prefix scenario, e.g. "xxx{crate_str}yyy"
241        let pathological_path_str = format!("xxx{}yyy", crate_str);
242
243        // We'll attempt to read from that path
244        let result = handle
245            .read_file_string(Path::new(&pathological_path_str))
246            .await;
247
248        // Because it doesn't strictly start with the crate_path, the code tries to do the join,
249        // which obviously won't exist => expect an error
250        assert!(result.is_err(), "Likely fails to find file or fails to parse path");
251    }
252}