shape_runtime/stdlib_io/
async_file_ops.rs1use shape_value::ValueWord;
7use std::sync::Arc;
8
9pub async fn io_read_file_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
13 let path = args
14 .first()
15 .and_then(|a| a.as_str())
16 .ok_or_else(|| "io.read_file_async() requires a string path argument".to_string())?
17 .to_string();
18
19 let contents = tokio::fs::read_to_string(&path)
20 .await
21 .map_err(|e| format!("io.read_file_async(\"{}\"): {}", path, e))?;
22
23 Ok(ValueWord::from_string(Arc::new(contents)))
24}
25
26pub async fn io_write_file_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
31 let path = args
32 .first()
33 .and_then(|a| a.as_str())
34 .ok_or_else(|| "io.write_file_async() requires a string path argument".to_string())?
35 .to_string();
36
37 let data = args
38 .get(1)
39 .and_then(|a| a.as_str())
40 .ok_or_else(|| "io.write_file_async() requires a string data argument".to_string())?
41 .to_string();
42
43 let bytes_written = data.len();
44 tokio::fs::write(&path, &data)
45 .await
46 .map_err(|e| format!("io.write_file_async(\"{}\"): {}", path, e))?;
47
48 Ok(ValueWord::from_i64(bytes_written as i64))
49}
50
51pub async fn io_append_file_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
56 use tokio::io::AsyncWriteExt;
57
58 let path = args
59 .first()
60 .and_then(|a| a.as_str())
61 .ok_or_else(|| "io.append_file_async() requires a string path argument".to_string())?
62 .to_string();
63
64 let data = args
65 .get(1)
66 .and_then(|a| a.as_str())
67 .ok_or_else(|| "io.append_file_async() requires a string data argument".to_string())?
68 .to_string();
69
70 let bytes_written = data.len();
71 let mut file = tokio::fs::OpenOptions::new()
72 .append(true)
73 .create(true)
74 .open(&path)
75 .await
76 .map_err(|e| format!("io.append_file_async(\"{}\"): {}", path, e))?;
77
78 file.write_all(data.as_bytes())
79 .await
80 .map_err(|e| format!("io.append_file_async(\"{}\"): {}", path, e))?;
81
82 file.flush()
83 .await
84 .map_err(|e| format!("io.append_file_async(\"{}\"): flush: {}", path, e))?;
85
86 Ok(ValueWord::from_i64(bytes_written as i64))
87}
88
89pub async fn io_read_bytes_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
93 let path = args
94 .first()
95 .and_then(|a| a.as_str())
96 .ok_or_else(|| "io.read_bytes_async() requires a string path argument".to_string())?
97 .to_string();
98
99 let bytes = tokio::fs::read(&path)
100 .await
101 .map_err(|e| format!("io.read_bytes_async(\"{}\"): {}", path, e))?;
102
103 let arr: Vec<ValueWord> = bytes
104 .iter()
105 .map(|&b| ValueWord::from_i64(b as i64))
106 .collect();
107 Ok(ValueWord::from_array(Arc::new(arr)))
108}
109
110pub async fn io_exists_async(args: Vec<ValueWord>) -> Result<ValueWord, String> {
114 let path = args
115 .first()
116 .and_then(|a| a.as_str())
117 .ok_or_else(|| "io.exists_async() requires a string path argument".to_string())?
118 .to_string();
119
120 let exists = tokio::fs::metadata(&path).await.is_ok();
122 Ok(ValueWord::from_bool(exists))
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[tokio::test]
130 async fn test_read_file_async() {
131 let dir = tempfile::tempdir().unwrap();
132 let path = dir.path().join("test.txt");
133 std::fs::write(&path, "async hello").unwrap();
134
135 let result = io_read_file_async(vec![ValueWord::from_string(Arc::new(
136 path.to_string_lossy().to_string(),
137 ))])
138 .await
139 .unwrap();
140
141 assert_eq!(result.as_str(), Some("async hello"));
142 }
143
144 #[tokio::test]
145 async fn test_write_file_async() {
146 let dir = tempfile::tempdir().unwrap();
147 let path = dir.path().join("test.txt");
148
149 let result = io_write_file_async(vec![
150 ValueWord::from_string(Arc::new(path.to_string_lossy().to_string())),
151 ValueWord::from_string(Arc::new("async world".to_string())),
152 ])
153 .await
154 .unwrap();
155
156 assert_eq!(result.as_i64(), Some(11)); let contents = std::fs::read_to_string(&path).unwrap();
158 assert_eq!(contents, "async world");
159 }
160
161 #[tokio::test]
162 async fn test_append_file_async() {
163 let dir = tempfile::tempdir().unwrap();
164 let path = dir.path().join("test.txt");
165 std::fs::write(&path, "first").unwrap();
166
167 let result = io_append_file_async(vec![
168 ValueWord::from_string(Arc::new(path.to_string_lossy().to_string())),
169 ValueWord::from_string(Arc::new("_second".to_string())),
170 ])
171 .await
172 .unwrap();
173
174 assert_eq!(result.as_i64(), Some(7)); let contents = std::fs::read_to_string(&path).unwrap();
176 assert_eq!(contents, "first_second");
177 }
178
179 #[tokio::test]
180 async fn test_read_bytes_async() {
181 let dir = tempfile::tempdir().unwrap();
182 let path = dir.path().join("test.bin");
183 std::fs::write(&path, &[0xAB, 0xCD, 0xEF]).unwrap();
184
185 let result = io_read_bytes_async(vec![ValueWord::from_string(Arc::new(
186 path.to_string_lossy().to_string(),
187 ))])
188 .await
189 .unwrap();
190
191 let arr = result.as_any_array().unwrap().to_generic();
192 assert_eq!(arr.len(), 3);
193 assert_eq!(arr[0].as_i64(), Some(0xAB));
194 assert_eq!(arr[1].as_i64(), Some(0xCD));
195 assert_eq!(arr[2].as_i64(), Some(0xEF));
196 }
197
198 #[tokio::test]
199 async fn test_exists_async() {
200 let result = io_exists_async(vec![ValueWord::from_string(Arc::new("/tmp".to_string()))])
201 .await
202 .unwrap();
203 assert_eq!(result.as_bool(), Some(true));
204
205 let result = io_exists_async(vec![ValueWord::from_string(Arc::new(
206 "/nonexistent_xyz_test".to_string(),
207 ))])
208 .await
209 .unwrap();
210 assert_eq!(result.as_bool(), Some(false));
211 }
212
213 #[tokio::test]
214 async fn test_read_file_async_missing() {
215 let result = io_read_file_async(vec![ValueWord::from_string(Arc::new(
216 "/nonexistent_file_xyz".to_string(),
217 ))])
218 .await;
219 assert!(result.is_err());
220 }
221
222 #[tokio::test]
223 async fn test_write_file_async_requires_args() {
224 let result = io_write_file_async(vec![]).await;
225 assert!(result.is_err());
226 }
227}