Skip to main content

aver/services/
disk.rs

1/// Disk service — file-system I/O.
2///
3/// Eight methods covering the full CRUD surface for files and directories:
4///   readText   — read a file as a UTF-8 string
5///   writeText  — write (overwrite) a file
6///   appendText — append to a file, creating it if absent
7///   exists     — check whether a path exists (returns Bool, not Result)
8///   delete     — remove a **file** (Err if given a directory — use deleteDir)
9///   deleteDir  — recursively remove a **directory** (Err if given a file)
10///   listDir    — list entry names in a directory
11///   makeDir    — create a directory and all missing parents (mkdir -p)
12///
13/// Each method requires its own exact effect (`Disk.readText`, `Disk.writeText`, etc.).
14use std::collections::HashMap;
15use std::rc::Rc;
16
17use crate::nan_value::{Arena, NanValue};
18use crate::value::{RuntimeError, Value, list_from_vec};
19
20pub fn register(global: &mut HashMap<String, Value>) {
21    let mut members = HashMap::new();
22    for method in &[
23        "readText",
24        "writeText",
25        "appendText",
26        "exists",
27        "delete",
28        "deleteDir",
29        "listDir",
30        "makeDir",
31    ] {
32        members.insert(
33            method.to_string(),
34            Value::Builtin(format!("Disk.{}", method)),
35        );
36    }
37    global.insert(
38        "Disk".to_string(),
39        Value::Namespace {
40            name: "Disk".to_string(),
41            members,
42        },
43    );
44}
45
46pub fn effects(name: &str) -> &'static [&'static str] {
47    match name {
48        "Disk.readText" => &["Disk.readText"],
49        "Disk.writeText" => &["Disk.writeText"],
50        "Disk.appendText" => &["Disk.appendText"],
51        "Disk.exists" => &["Disk.exists"],
52        "Disk.delete" => &["Disk.delete"],
53        "Disk.deleteDir" => &["Disk.deleteDir"],
54        "Disk.listDir" => &["Disk.listDir"],
55        "Disk.makeDir" => &["Disk.makeDir"],
56        _ => &[],
57    }
58}
59
60/// Returns `Some(result)` when `name` is owned by this service, `None` otherwise.
61pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
62    match name {
63        "Disk.readText" => Some(read_text(args)),
64        "Disk.writeText" => Some(write_text(args)),
65        "Disk.appendText" => Some(append_text(args)),
66        "Disk.exists" => Some(exists(args)),
67        "Disk.delete" => Some(delete(args)),
68        "Disk.deleteDir" => Some(delete_dir(args)),
69        "Disk.listDir" => Some(list_dir(args)),
70        "Disk.makeDir" => Some(make_dir(args)),
71        _ => None,
72    }
73}
74
75// ─── Implementations ──────────────────────────────────────────────────────────
76
77fn read_text(args: &[Value]) -> Result<Value, RuntimeError> {
78    let path = one_str_arg("Disk.readText", args)?;
79    match aver_rt::read_text(&path) {
80        Ok(text) => Ok(Value::Ok(Box::new(Value::Str(text)))),
81        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
82    }
83}
84
85fn write_text(args: &[Value]) -> Result<Value, RuntimeError> {
86    let (path, content) = two_str_args("Disk.writeText", args)?;
87    match aver_rt::write_text(&path, &content) {
88        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
89        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
90    }
91}
92
93fn append_text(args: &[Value]) -> Result<Value, RuntimeError> {
94    let (path, content) = two_str_args("Disk.appendText", args)?;
95    match aver_rt::append_text(&path, &content) {
96        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
97        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
98    }
99}
100
101fn exists(args: &[Value]) -> Result<Value, RuntimeError> {
102    let path = one_str_arg("Disk.exists", args)?;
103    Ok(Value::Bool(aver_rt::path_exists(&path)))
104}
105
106fn delete(args: &[Value]) -> Result<Value, RuntimeError> {
107    let path = one_str_arg("Disk.delete", args)?;
108    match aver_rt::delete_file(&path) {
109        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
110        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
111    }
112}
113
114fn delete_dir(args: &[Value]) -> Result<Value, RuntimeError> {
115    let path = one_str_arg("Disk.deleteDir", args)?;
116    match aver_rt::delete_dir(&path) {
117        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
118        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
119    }
120}
121
122fn list_dir(args: &[Value]) -> Result<Value, RuntimeError> {
123    let path = one_str_arg("Disk.listDir", args)?;
124    match aver_rt::list_dir(&path) {
125        Ok(entries) => Ok(Value::Ok(Box::new(list_from_vec(
126            entries.into_iter().map(Value::Str).collect(),
127        )))),
128        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
129    }
130}
131
132fn make_dir(args: &[Value]) -> Result<Value, RuntimeError> {
133    let path = one_str_arg("Disk.makeDir", args)?;
134    match aver_rt::make_dir(&path) {
135        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
136        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
137    }
138}
139
140// ─── Argument helpers ─────────────────────────────────────────────────────────
141
142fn one_str_arg(fn_name: &str, args: &[Value]) -> Result<String, RuntimeError> {
143    match args {
144        [Value::Str(s)] => Ok(s.clone()),
145        [_] => Err(RuntimeError::Error(format!(
146            "{}: path must be a String",
147            fn_name
148        ))),
149        _ => Err(RuntimeError::Error(format!(
150            "{}() takes 1 argument (path), got {}",
151            fn_name,
152            args.len()
153        ))),
154    }
155}
156
157fn two_str_args(fn_name: &str, args: &[Value]) -> Result<(String, String), RuntimeError> {
158    match args {
159        [Value::Str(a), Value::Str(b)] => Ok((a.clone(), b.clone())),
160        [a, b] => Err(RuntimeError::Error(format!(
161            "{}: both arguments must be Strings (got {}, {})",
162            fn_name,
163            crate::value::aver_repr(a),
164            crate::value::aver_repr(b)
165        ))),
166        _ => Err(RuntimeError::Error(format!(
167            "{}() takes 2 arguments (path, content), got {}",
168            fn_name,
169            args.len()
170        ))),
171    }
172}
173
174// ─── NanValue-native API ─────────────────────────────────────────────────────
175
176pub fn register_nv(global: &mut HashMap<String, NanValue>, arena: &mut Arena) {
177    let methods = &[
178        "readText",
179        "writeText",
180        "appendText",
181        "exists",
182        "delete",
183        "deleteDir",
184        "listDir",
185        "makeDir",
186    ];
187    let mut members: Vec<(Rc<str>, NanValue)> = Vec::with_capacity(methods.len());
188    for method in methods {
189        let idx = arena.push_builtin(&format!("Disk.{}", method));
190        members.push((Rc::from(*method), NanValue::new_builtin(idx)));
191    }
192    let ns_idx = arena.push(crate::nan_value::ArenaEntry::Namespace {
193        name: Rc::from("Disk"),
194        members,
195    });
196    global.insert("Disk".to_string(), NanValue::new_namespace(ns_idx));
197}
198
199pub fn call_nv(
200    name: &str,
201    args: &[NanValue],
202    arena: &mut Arena,
203) -> Option<Result<NanValue, RuntimeError>> {
204    match name {
205        "Disk.readText" => Some(read_text_nv(args, arena)),
206        "Disk.writeText" => Some(write_text_nv(args, arena)),
207        "Disk.appendText" => Some(append_text_nv(args, arena)),
208        "Disk.exists" => Some(exists_nv(args, arena)),
209        "Disk.delete" => Some(delete_nv(args, arena)),
210        "Disk.deleteDir" => Some(delete_dir_nv(args, arena)),
211        "Disk.listDir" => Some(list_dir_nv(args, arena)),
212        "Disk.makeDir" => Some(make_dir_nv(args, arena)),
213        _ => None,
214    }
215}
216
217fn nv_one_str(fn_name: &str, args: &[NanValue], arena: &Arena) -> Result<String, RuntimeError> {
218    if args.len() != 1 {
219        return Err(RuntimeError::Error(format!(
220            "{}() takes 1 argument (path), got {}",
221            fn_name,
222            args.len()
223        )));
224    }
225    if !args[0].is_string() {
226        return Err(RuntimeError::Error(format!(
227            "{}: path must be a String",
228            fn_name
229        )));
230    }
231    Ok(arena.get_string_value(args[0]).to_string())
232}
233
234fn nv_two_str(
235    fn_name: &str,
236    args: &[NanValue],
237    arena: &Arena,
238) -> Result<(String, String), RuntimeError> {
239    if args.len() != 2 {
240        return Err(RuntimeError::Error(format!(
241            "{}() takes 2 arguments (path, content), got {}",
242            fn_name,
243            args.len()
244        )));
245    }
246    if !args[0].is_string() || !args[1].is_string() {
247        return Err(RuntimeError::Error(format!(
248            "{}: both arguments must be Strings (got {}, {})",
249            fn_name,
250            args[0].type_name(),
251            args[1].type_name()
252        )));
253    }
254    Ok((
255        arena.get_string_value(args[0]).to_string(),
256        arena.get_string_value(args[1]).to_string(),
257    ))
258}
259
260fn nv_ok_unit(arena: &mut Arena) -> NanValue {
261    NanValue::new_ok_value(NanValue::UNIT, arena)
262}
263
264fn nv_ok_str(s: &str, arena: &mut Arena) -> NanValue {
265    let inner = NanValue::new_string_value(s, arena);
266    NanValue::new_ok_value(inner, arena)
267}
268
269fn nv_err_str(s: &str, arena: &mut Arena) -> NanValue {
270    let inner = NanValue::new_string_value(s, arena);
271    NanValue::new_err_value(inner, arena)
272}
273
274fn read_text_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
275    let path = nv_one_str("Disk.readText", args, arena)?;
276    match aver_rt::read_text(&path) {
277        Ok(text) => Ok(nv_ok_str(&text, arena)),
278        Err(e) => Ok(nv_err_str(&e.to_string(), arena)),
279    }
280}
281
282fn write_text_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
283    let (path, content) = nv_two_str("Disk.writeText", args, arena)?;
284    match aver_rt::write_text(&path, &content) {
285        Ok(_) => Ok(nv_ok_unit(arena)),
286        Err(e) => Ok(nv_err_str(&e.to_string(), arena)),
287    }
288}
289
290fn append_text_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
291    let (path, content) = nv_two_str("Disk.appendText", args, arena)?;
292    match aver_rt::append_text(&path, &content) {
293        Ok(_) => Ok(nv_ok_unit(arena)),
294        Err(e) => Ok(nv_err_str(&e.to_string(), arena)),
295    }
296}
297
298fn exists_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
299    let path = nv_one_str("Disk.exists", args, arena)?;
300    Ok(NanValue::new_bool(aver_rt::path_exists(&path)))
301}
302
303fn delete_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
304    let path = nv_one_str("Disk.delete", args, arena)?;
305    match aver_rt::delete_file(&path) {
306        Ok(_) => Ok(nv_ok_unit(arena)),
307        Err(e) => Ok(nv_err_str(&e.to_string(), arena)),
308    }
309}
310
311fn delete_dir_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
312    let path = nv_one_str("Disk.deleteDir", args, arena)?;
313    match aver_rt::delete_dir(&path) {
314        Ok(_) => Ok(nv_ok_unit(arena)),
315        Err(e) => Ok(nv_err_str(&e.to_string(), arena)),
316    }
317}
318
319fn list_dir_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
320    let path = nv_one_str("Disk.listDir", args, arena)?;
321    match aver_rt::list_dir(&path) {
322        Ok(entries) => {
323            let items: Vec<NanValue> = entries
324                .into_iter()
325                .map(|s| NanValue::new_string_value(&s, arena))
326                .collect();
327            let list_idx = arena.push_list(items);
328            let inner = NanValue::new_list(list_idx);
329            Ok(NanValue::new_ok_value(inner, arena))
330        }
331        Err(e) => Ok(nv_err_str(&e.to_string(), arena)),
332    }
333}
334
335fn make_dir_nv(args: &[NanValue], arena: &mut Arena) -> Result<NanValue, RuntimeError> {
336    let path = nv_one_str("Disk.makeDir", args, arena)?;
337    match aver_rt::make_dir(&path) {
338        Ok(_) => Ok(nv_ok_unit(arena)),
339        Err(e) => Ok(nv_err_str(&e.to_string(), arena)),
340    }
341}