shape_runtime/stdlib_io/
path_ops.rs1use shape_value::ValueWord;
7use std::path::Path;
8use std::sync::Arc;
9
10pub fn io_join(
12 args: &[ValueWord],
13 _ctx: &crate::module_exports::ModuleContext,
14) -> Result<ValueWord, String> {
15 if args.is_empty() {
16 return Err("io.join() requires at least one path argument".to_string());
17 }
18
19 let mut result = std::path::PathBuf::new();
20 for arg in args {
21 if let Some(s) = arg.as_str() {
22 result.push(s);
23 } else if let Some(view) = arg.as_any_array() {
24 let arr = view.to_generic();
25 for item in arr.iter() {
26 if let Some(s) = item.as_str() {
27 result.push(s);
28 }
29 }
30 } else {
31 return Err("io.join() arguments must be strings".to_string());
32 }
33 }
34 Ok(ValueWord::from_string(Arc::new(
35 result.to_string_lossy().to_string(),
36 )))
37}
38
39pub fn io_dirname(
41 args: &[ValueWord],
42 _ctx: &crate::module_exports::ModuleContext,
43) -> Result<ValueWord, String> {
44 let path = args
45 .first()
46 .and_then(|a| a.as_str())
47 .ok_or_else(|| "io.dirname() requires a string path".to_string())?;
48
49 let parent = Path::new(path)
50 .parent()
51 .map(|p| p.to_string_lossy().to_string())
52 .unwrap_or_default();
53 Ok(ValueWord::from_string(Arc::new(parent)))
54}
55
56pub fn io_basename(
58 args: &[ValueWord],
59 _ctx: &crate::module_exports::ModuleContext,
60) -> Result<ValueWord, String> {
61 let path = args
62 .first()
63 .and_then(|a| a.as_str())
64 .ok_or_else(|| "io.basename() requires a string path".to_string())?;
65
66 let name = Path::new(path)
67 .file_name()
68 .map(|n| n.to_string_lossy().to_string())
69 .unwrap_or_default();
70 Ok(ValueWord::from_string(Arc::new(name)))
71}
72
73pub fn io_extension(
75 args: &[ValueWord],
76 _ctx: &crate::module_exports::ModuleContext,
77) -> Result<ValueWord, String> {
78 let path = args
79 .first()
80 .and_then(|a| a.as_str())
81 .ok_or_else(|| "io.extension() requires a string path".to_string())?;
82
83 let ext = Path::new(path)
84 .extension()
85 .map(|e| e.to_string_lossy().to_string())
86 .unwrap_or_default();
87 Ok(ValueWord::from_string(Arc::new(ext)))
88}
89
90pub fn io_resolve(
92 args: &[ValueWord],
93 _ctx: &crate::module_exports::ModuleContext,
94) -> Result<ValueWord, String> {
95 let path = args
96 .first()
97 .and_then(|a| a.as_str())
98 .ok_or_else(|| "io.resolve() requires a string path".to_string())?;
99
100 let resolved =
101 std::fs::canonicalize(path).map_err(|e| format!("io.resolve(\"{}\"): {}", path, e))?;
102 Ok(ValueWord::from_string(Arc::new(
103 resolved.to_string_lossy().to_string(),
104 )))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
112 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
113 crate::module_exports::ModuleContext {
114 schemas: registry,
115 invoke_callable: None,
116 raw_invoker: None,
117 function_hashes: None,
118 vm_state: None,
119 granted_permissions: None,
120 scope_constraints: None,
121 set_pending_resume: None,
122 set_pending_frame_resume: None,
123 }
124 }
125
126 #[test]
127 fn test_join_two_parts() {
128 let ctx = test_ctx();
129 let result = io_join(
130 &[
131 ValueWord::from_string(Arc::new("/home".to_string())),
132 ValueWord::from_string(Arc::new("user".to_string())),
133 ],
134 &ctx,
135 )
136 .unwrap();
137 assert_eq!(result.as_str().unwrap(), "/home/user");
138 }
139
140 #[test]
141 fn test_join_three_parts() {
142 let ctx = test_ctx();
143 let result = io_join(
144 &[
145 ValueWord::from_string(Arc::new("/a".to_string())),
146 ValueWord::from_string(Arc::new("b".to_string())),
147 ValueWord::from_string(Arc::new("c.txt".to_string())),
148 ],
149 &ctx,
150 )
151 .unwrap();
152 assert_eq!(result.as_str().unwrap(), "/a/b/c.txt");
153 }
154
155 #[test]
156 fn test_dirname() {
157 let ctx = test_ctx();
158 let result = io_dirname(
159 &[ValueWord::from_string(Arc::new(
160 "/home/user/file.txt".to_string(),
161 ))],
162 &ctx,
163 )
164 .unwrap();
165 assert_eq!(result.as_str().unwrap(), "/home/user");
166 }
167
168 #[test]
169 fn test_basename() {
170 let ctx = test_ctx();
171 let result = io_basename(
172 &[ValueWord::from_string(Arc::new(
173 "/home/user/file.txt".to_string(),
174 ))],
175 &ctx,
176 )
177 .unwrap();
178 assert_eq!(result.as_str().unwrap(), "file.txt");
179 }
180
181 #[test]
182 fn test_extension() {
183 let ctx = test_ctx();
184 let result = io_extension(
185 &[ValueWord::from_string(Arc::new(
186 "/home/user/file.txt".to_string(),
187 ))],
188 &ctx,
189 )
190 .unwrap();
191 assert_eq!(result.as_str().unwrap(), "txt");
192 }
193
194 #[test]
195 fn test_extension_none() {
196 let ctx = test_ctx();
197 let result = io_extension(
198 &[ValueWord::from_string(Arc::new(
199 "/home/user/Makefile".to_string(),
200 ))],
201 &ctx,
202 )
203 .unwrap();
204 assert_eq!(result.as_str().unwrap(), "");
205 }
206
207 #[test]
208 fn test_resolve_tmp() {
209 let ctx = test_ctx();
210 let result = io_resolve(
211 &[ValueWord::from_string(Arc::new("/tmp".to_string()))],
212 &ctx,
213 )
214 .unwrap();
215 let resolved = result.as_str().unwrap();
216 assert!(resolved.starts_with('/'));
218 }
219}