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/// All methods require `! [Disk]`.
14use std::collections::HashMap;
15
16use crate::value::{RuntimeError, Value};
17
18pub fn register(global: &mut HashMap<String, Value>) {
19    let mut members = HashMap::new();
20    for method in &[
21        "readText",
22        "writeText",
23        "appendText",
24        "exists",
25        "delete",
26        "deleteDir",
27        "listDir",
28        "makeDir",
29    ] {
30        members.insert(
31            method.to_string(),
32            Value::Builtin(format!("Disk.{}", method)),
33        );
34    }
35    global.insert(
36        "Disk".to_string(),
37        Value::Namespace {
38            name: "Disk".to_string(),
39            members,
40        },
41    );
42}
43
44pub fn effects(name: &str) -> &'static [&'static str] {
45    match name {
46        "Disk.readText" | "Disk.writeText" | "Disk.appendText" | "Disk.exists" | "Disk.delete"
47        | "Disk.deleteDir" | "Disk.listDir" | "Disk.makeDir" => &["Disk"],
48        _ => &[],
49    }
50}
51
52/// Returns `Some(result)` when `name` is owned by this service, `None` otherwise.
53pub fn call(name: &str, args: &[Value]) -> Option<Result<Value, RuntimeError>> {
54    match name {
55        "Disk.readText" => Some(read_text(&args)),
56        "Disk.writeText" => Some(write_text(&args)),
57        "Disk.appendText" => Some(append_text(&args)),
58        "Disk.exists" => Some(exists(&args)),
59        "Disk.delete" => Some(delete(&args)),
60        "Disk.deleteDir" => Some(delete_dir(&args)),
61        "Disk.listDir" => Some(list_dir(&args)),
62        "Disk.makeDir" => Some(make_dir(&args)),
63        _ => None,
64    }
65}
66
67// ─── Implementations ──────────────────────────────────────────────────────────
68
69fn read_text(args: &[Value]) -> Result<Value, RuntimeError> {
70    let path = one_str_arg("Disk.readText", args)?;
71    match std::fs::read_to_string(&path) {
72        Ok(text) => Ok(Value::Ok(Box::new(Value::Str(text)))),
73        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
74    }
75}
76
77fn write_text(args: &[Value]) -> Result<Value, RuntimeError> {
78    let (path, content) = two_str_args("Disk.writeText", args)?;
79    match std::fs::write(&path, &content) {
80        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
81        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
82    }
83}
84
85fn append_text(args: &[Value]) -> Result<Value, RuntimeError> {
86    use std::io::Write;
87    let (path, content) = two_str_args("Disk.appendText", args)?;
88    match std::fs::OpenOptions::new()
89        .create(true)
90        .append(true)
91        .open(&path)
92    {
93        Ok(mut f) => match f.write_all(content.as_bytes()) {
94            Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
95            Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
96        },
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(std::path::Path::new(&path).exists()))
104}
105
106fn delete(args: &[Value]) -> Result<Value, RuntimeError> {
107    let path = one_str_arg("Disk.delete", args)?;
108    let p = std::path::Path::new(&path);
109    if p.is_dir() {
110        return Ok(Value::Err(Box::new(Value::Str(
111            "Disk.delete: path is a directory — use Disk.deleteDir to remove directories"
112                .to_string(),
113        ))));
114    }
115    match std::fs::remove_file(p) {
116        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
117        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
118    }
119}
120
121fn delete_dir(args: &[Value]) -> Result<Value, RuntimeError> {
122    let path = one_str_arg("Disk.deleteDir", args)?;
123    let p = std::path::Path::new(&path);
124    if !p.is_dir() {
125        return Ok(Value::Err(Box::new(Value::Str(
126            "Disk.deleteDir: path is not a directory — use Disk.delete to remove files".to_string(),
127        ))));
128    }
129    match std::fs::remove_dir_all(p) {
130        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
131        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
132    }
133}
134
135fn list_dir(args: &[Value]) -> Result<Value, RuntimeError> {
136    let path = one_str_arg("Disk.listDir", args)?;
137    match std::fs::read_dir(&path) {
138        Ok(entries) => {
139            let mut names = Vec::new();
140            for entry in entries {
141                match entry {
142                    Ok(e) => names.push(Value::Str(e.file_name().to_string_lossy().into_owned())),
143                    Err(e) => return Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
144                }
145            }
146            Ok(Value::Ok(Box::new(Value::List(names))))
147        }
148        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
149    }
150}
151
152fn make_dir(args: &[Value]) -> Result<Value, RuntimeError> {
153    let path = one_str_arg("Disk.makeDir", args)?;
154    match std::fs::create_dir_all(&path) {
155        Ok(_) => Ok(Value::Ok(Box::new(Value::Unit))),
156        Err(e) => Ok(Value::Err(Box::new(Value::Str(e.to_string())))),
157    }
158}
159
160// ─── Argument helpers ─────────────────────────────────────────────────────────
161
162fn one_str_arg(fn_name: &str, args: &[Value]) -> Result<String, RuntimeError> {
163    match args {
164        [Value::Str(s)] => Ok(s.clone()),
165        [_] => Err(RuntimeError::Error(format!(
166            "{}: path must be a String",
167            fn_name
168        ))),
169        _ => Err(RuntimeError::Error(format!(
170            "{}() takes 1 argument (path), got {}",
171            fn_name,
172            args.len()
173        ))),
174    }
175}
176
177fn two_str_args(fn_name: &str, args: &[Value]) -> Result<(String, String), RuntimeError> {
178    match args {
179        [Value::Str(a), Value::Str(b)] => Ok((a.clone(), b.clone())),
180        [_, _] => Err(RuntimeError::Error(format!(
181            "{}: both arguments must be Strings",
182            fn_name
183        ))),
184        _ => Err(RuntimeError::Error(format!(
185            "{}() takes 2 arguments (path, content), got {}",
186            fn_name,
187            args.len()
188        ))),
189    }
190}