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