Skip to main content

doge_runtime/stdlib/
fetch.rs

1//! `fetch` — the file-I/O stdlib module. Every operation takes a Str path (and,
2//! where relevant, Str text), and every OS failure — a missing file, a permission
3//! problem, non-text bytes — is a catchable IOError rather than a panic.
4
5use std::fs;
6use std::io::Write;
7
8use crate::error::{DogeError, DogeResult};
9use crate::stdlib::str_arg;
10use crate::value::Value;
11
12/// `fetch.read(path)` — the file's whole contents as a Str. A missing file or one
13/// whose bytes are not valid text is a catchable IOError.
14pub fn fetch_read(path: &Value) -> DogeResult {
15    let path = str_arg("fetch", "read", path)?;
16    match fs::read_to_string(path) {
17        Ok(text) => Ok(Value::str(text)),
18        Err(err) => Err(DogeError::io_error(format!("cannot read {path}: {err}"))),
19    }
20}
21
22/// `fetch.write(path, text)` — replace the file's contents with `text`, creating
23/// it if needed. Returns `none`.
24pub fn fetch_write(path: &Value, text: &Value) -> DogeResult {
25    let path = str_arg("fetch", "write", path)?;
26    let text = str_arg("fetch", "write", text)?;
27    match fs::write(path, text) {
28        Ok(()) => Ok(Value::None),
29        Err(err) => Err(DogeError::io_error(format!("cannot write {path}: {err}"))),
30    }
31}
32
33/// `fetch.append(path, text)` — add `text` to the end of the file, creating it if
34/// needed. Returns `none`.
35pub fn fetch_append(path: &Value, text: &Value) -> DogeResult {
36    let path = str_arg("fetch", "append", path)?;
37    let text = str_arg("fetch", "append", text)?;
38    let result = fs::OpenOptions::new()
39        .create(true)
40        .append(true)
41        .open(path)
42        .and_then(|mut file| file.write_all(text.as_bytes()));
43    match result {
44        Ok(()) => Ok(Value::None),
45        Err(err) => Err(DogeError::io_error(format!(
46            "cannot append to {path}: {err}"
47        ))),
48    }
49}
50
51/// `fetch.exists(path)` — whether a file or directory exists at `path`.
52pub fn fetch_exists(path: &Value) -> DogeResult {
53    let path = str_arg("fetch", "exists", path)?;
54    Ok(Value::Bool(fs::metadata(path).is_ok()))
55}
56
57/// `fetch.delete(path)` — remove the file at `path`. A missing file is a catchable
58/// IOError. Returns `none`.
59pub fn fetch_delete(path: &Value) -> DogeResult {
60    let path = str_arg("fetch", "delete", path)?;
61    match fs::remove_file(path) {
62        Ok(()) => Ok(Value::None),
63        Err(err) => Err(DogeError::io_error(format!("cannot delete {path}: {err}"))),
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::error::ErrorKind;
71    use std::path::PathBuf;
72
73    /// A unique scratch path under the OS temp dir, salted with the process id and
74    /// a caller-supplied tag so parallel tests never collide.
75    fn scratch(tag: &str) -> PathBuf {
76        std::env::temp_dir().join(format!("doge_fetch_{}_{tag}", std::process::id()))
77    }
78
79    #[test]
80    fn write_append_read_round_trip() {
81        let path = scratch("round_trip");
82        let p = Value::str(path.to_string_lossy());
83        fetch_write(&p, &Value::str("much ")).unwrap();
84        fetch_append(&p, &Value::str("wow")).unwrap();
85        assert!(matches!(fetch_read(&p).unwrap(), Value::Str(s) if &*s == "much wow"));
86        assert!(matches!(fetch_exists(&p).unwrap(), Value::Bool(true)));
87        fetch_delete(&p).unwrap();
88        assert!(matches!(fetch_exists(&p).unwrap(), Value::Bool(false)));
89    }
90
91    #[test]
92    fn reading_a_missing_file_is_a_catchable_io_error() {
93        let path = scratch("missing");
94        let err = fetch_read(&Value::str(path.to_string_lossy())).unwrap_err();
95        assert_eq!(err.kind, ErrorKind::IOError);
96    }
97
98    #[test]
99    fn non_str_path_is_a_type_error() {
100        assert_eq!(
101            fetch_read(&Value::Int(1)).unwrap_err().kind,
102            ErrorKind::TypeError
103        );
104    }
105}