mars_agents/source/
path.rs1use std::path::{Path, PathBuf};
7
8use crate::error::MarsError;
9use crate::source::ResolvedRef;
10use crate::types::SourceName;
11
12pub fn fetch_path(
19 path: &Path,
20 project_root: &Path,
21 source_name: &str,
22) -> Result<ResolvedRef, MarsError> {
23 let resolved = if path.is_absolute() {
24 path.to_path_buf()
25 } else {
26 project_root.join(path)
27 };
28
29 let resolved = canonicalize_path(&resolved, source_name)?;
31
32 if !resolved.exists() {
34 return Err(MarsError::Source {
35 source_name: source_name.to_string(),
36 message: format!("path does not exist: {}", resolved.display()),
37 });
38 }
39
40 if !resolved.is_dir() {
41 return Err(MarsError::Source {
42 source_name: source_name.to_string(),
43 message: format!("path is not a directory: {}", resolved.display()),
44 });
45 }
46
47 Ok(ResolvedRef {
48 source_name: SourceName::from(source_name),
49 version: None,
50 version_tag: None,
51 commit: None,
52 tree_path: resolved,
53 })
54}
55
56fn canonicalize_path(path: &Path, source_name: &str) -> Result<PathBuf, MarsError> {
58 dunce::canonicalize(path).map_err(|e| MarsError::Source {
59 source_name: source_name.to_string(),
60 message: format!("failed to resolve path `{}`: {e}", path.display()),
61 })
62}
63
64#[cfg(test)]
65mod tests {
66 use super::*;
67 use tempfile::TempDir;
68
69 #[test]
70 fn fetch_absolute_path() {
71 let dir = TempDir::new().unwrap();
72 let source_dir = dir.path().join("my-agents");
73 std::fs::create_dir_all(&source_dir).unwrap();
74
75 let resolved = fetch_path(&source_dir, dir.path(), "local-source").unwrap();
76
77 assert_eq!(resolved.source_name, "local-source");
78 assert!(resolved.version.is_none());
79 assert!(resolved.version_tag.is_none());
80 assert!(resolved.commit.is_none());
81 assert_eq!(
82 dunce::canonicalize(&resolved.tree_path).unwrap(),
83 dunce::canonicalize(&source_dir).unwrap()
84 );
85 }
86
87 #[test]
88 fn fetch_relative_path() {
89 let dir = TempDir::new().unwrap();
90 let project_root = dir.path().join("project");
91 let source_dir = dir.path().join("project").join("local-agents");
92 std::fs::create_dir_all(&project_root).unwrap();
93 std::fs::create_dir_all(&source_dir).unwrap();
94
95 let resolved = fetch_path(Path::new("local-agents"), &project_root, "local").unwrap();
96
97 assert_eq!(
98 dunce::canonicalize(&resolved.tree_path).unwrap(),
99 dunce::canonicalize(&source_dir).unwrap()
100 );
101 }
102
103 #[test]
104 fn fetch_relative_path_with_dotdot() {
105 let dir = TempDir::new().unwrap();
106 let project_root = dir.path().join("project");
107 let source_dir = dir.path().join("external-agents");
108 std::fs::create_dir_all(&project_root).unwrap();
109 std::fs::create_dir_all(&source_dir).unwrap();
110
111 let resolved =
112 fetch_path(Path::new("../external-agents"), &project_root, "external").unwrap();
113
114 assert_eq!(
115 dunce::canonicalize(&resolved.tree_path).unwrap(),
116 dunce::canonicalize(&source_dir).unwrap()
117 );
118 }
119
120 #[test]
121 fn fetch_nonexistent_path_returns_error() {
122 let dir = TempDir::new().unwrap();
123 let result = fetch_path(&dir.path().join("nonexistent"), dir.path(), "bad-source");
124
125 assert!(result.is_err());
126 let err = result.unwrap_err().to_string();
127 assert!(
128 err.contains("bad-source"),
129 "error should mention source name: {err}"
130 );
131 assert!(
132 err.contains("nonexistent"),
133 "error should mention the path: {err}"
134 );
135 }
136
137 #[test]
138 fn fetch_file_not_directory_returns_error() {
139 let dir = TempDir::new().unwrap();
140 let file_path = dir.path().join("not-a-dir.txt");
141 std::fs::write(&file_path, "content").unwrap();
142
143 let result = fetch_path(&file_path, dir.path(), "file-source");
144
145 assert!(result.is_err());
146 let err = result.unwrap_err().to_string();
147 assert!(
148 err.contains("not a directory"),
149 "error should mention 'not a directory': {err}"
150 );
151 }
152}