Skip to main content

cljrs_stdlib/
io.rs

1//! Native implementations for `clojure.rust.io`.
2
3use std::any::Any;
4use std::io::{BufRead, BufReader, BufWriter, Cursor, Read, Write};
5use std::sync::{Arc, Mutex};
6
7use cljrs_value::resource::Resource;
8use cljrs_value::{Arity, ResourceHandle, Value, ValueError, ValueResult};
9
10use crate::register_fns;
11
12// ── Concrete resource types ──────────────────────────────────────────────────
13
14/// A buffered file reader.
15#[derive(Debug)]
16pub struct IoReader {
17    inner: Mutex<Option<BufReader<std::fs::File>>>,
18}
19
20impl IoReader {
21    pub fn open(path: &str) -> ValueResult<Self> {
22        let file = std::fs::File::open(path)
23            .map_err(|e| ValueError::Other(format!("cannot open {path}: {e}")))?;
24        Ok(Self {
25            inner: Mutex::new(Some(BufReader::new(file))),
26        })
27    }
28
29    pub fn read_line(&self) -> ValueResult<Option<String>> {
30        let mut guard = self.inner.lock().unwrap();
31        let reader = guard
32            .as_mut()
33            .ok_or_else(|| ValueError::Other("reader is closed".into()))?;
34        let mut line = String::new();
35        let n = reader
36            .read_line(&mut line)
37            .map_err(|e| ValueError::Other(format!("read error: {e}")))?;
38        if n == 0 {
39            Ok(None) // EOF
40        } else {
41            Ok(Some(line))
42        }
43    }
44
45    pub fn read_all(&self) -> ValueResult<String> {
46        let mut guard = self.inner.lock().unwrap();
47        let reader = guard
48            .as_mut()
49            .ok_or_else(|| ValueError::Other("reader is closed".into()))?;
50        let mut buf = String::new();
51        reader
52            .read_to_string(&mut buf)
53            .map_err(|e| ValueError::Other(format!("read error: {e}")))?;
54        Ok(buf)
55    }
56}
57
58impl Resource for IoReader {
59    fn close(&self) -> ValueResult<()> {
60        let mut guard = self.inner.lock().unwrap();
61        *guard = None;
62        Ok(())
63    }
64
65    fn is_closed(&self) -> bool {
66        self.inner.lock().unwrap().is_none()
67    }
68
69    fn resource_type(&self) -> &'static str {
70        "reader"
71    }
72
73    fn as_any(&self) -> &dyn Any {
74        self
75    }
76}
77
78/// A buffered file writer.
79#[derive(Debug)]
80pub struct IoWriter {
81    inner: Mutex<Option<BufWriter<std::fs::File>>>,
82}
83
84impl IoWriter {
85    pub fn open(path: &str, append: bool) -> ValueResult<Self> {
86        let file = std::fs::OpenOptions::new()
87            .write(true)
88            .create(true)
89            .truncate(!append)
90            .append(append)
91            .open(path)
92            .map_err(|e| ValueError::Other(format!("cannot open {path}: {e}")))?;
93        Ok(Self {
94            inner: Mutex::new(Some(BufWriter::new(file))),
95        })
96    }
97
98    pub fn write_str(&self, s: &str) -> ValueResult<()> {
99        let mut guard = self.inner.lock().unwrap();
100        let writer = guard
101            .as_mut()
102            .ok_or_else(|| ValueError::Other("writer is closed".into()))?;
103        writer
104            .write_all(s.as_bytes())
105            .map_err(|e| ValueError::Other(format!("write error: {e}")))?;
106        Ok(())
107    }
108
109    pub fn flush(&self) -> ValueResult<()> {
110        let mut guard = self.inner.lock().unwrap();
111        if let Some(writer) = guard.as_mut() {
112            writer
113                .flush()
114                .map_err(|e| ValueError::Other(format!("flush error: {e}")))?;
115        }
116        Ok(())
117    }
118}
119
120impl Resource for IoWriter {
121    fn close(&self) -> ValueResult<()> {
122        let mut guard = self.inner.lock().unwrap();
123        // Flush before closing.
124        if let Some(ref mut w) = *guard {
125            let _ = w.flush();
126        }
127        *guard = None;
128        Ok(())
129    }
130
131    fn is_closed(&self) -> bool {
132        self.inner.lock().unwrap().is_none()
133    }
134
135    fn resource_type(&self) -> &'static str {
136        "writer"
137    }
138
139    fn as_any(&self) -> &dyn Any {
140        self
141    }
142}
143
144/// A reader backed by a String (for EDN read-string, etc.).
145#[derive(Debug)]
146pub struct StringReader {
147    inner: Mutex<Option<Cursor<String>>>,
148}
149
150impl StringReader {
151    pub fn new(s: String) -> Self {
152        Self {
153            inner: Mutex::new(Some(Cursor::new(s))),
154        }
155    }
156
157    pub fn read_line(&self) -> ValueResult<Option<String>> {
158        let mut guard = self.inner.lock().unwrap();
159        let cursor = guard
160            .as_mut()
161            .ok_or_else(|| ValueError::Other("reader is closed".into()))?;
162        let mut line = String::new();
163        let n = BufRead::read_line(cursor, &mut line)
164            .map_err(|e| ValueError::Other(format!("read error: {e}")))?;
165        if n == 0 { Ok(None) } else { Ok(Some(line)) }
166    }
167
168    pub fn read_all(&self) -> ValueResult<String> {
169        let mut guard = self.inner.lock().unwrap();
170        let cursor = guard
171            .as_mut()
172            .ok_or_else(|| ValueError::Other("reader is closed".into()))?;
173        let mut buf = String::new();
174        cursor
175            .read_to_string(&mut buf)
176            .map_err(|e| ValueError::Other(format!("read error: {e}")))?;
177        Ok(buf)
178    }
179}
180
181impl Resource for StringReader {
182    fn close(&self) -> ValueResult<()> {
183        *self.inner.lock().unwrap() = None;
184        Ok(())
185    }
186
187    fn is_closed(&self) -> bool {
188        self.inner.lock().unwrap().is_none()
189    }
190
191    fn resource_type(&self) -> &'static str {
192        "string-reader"
193    }
194
195    fn as_any(&self) -> &dyn Any {
196        self
197    }
198}
199
200// ── Native builtins ──────────────────────────────────────────────────────────
201
202pub fn register(globals: &Arc<cljrs_eval::GlobalEnv>, ns: &str) {
203    register_fns!(
204        globals,
205        ns,
206        [
207            ("reader", Arity::Fixed(1), builtin_reader),
208            ("writer", Arity::Variadic { min: 1 }, builtin_writer),
209            ("string-reader", Arity::Fixed(1), builtin_string_reader),
210            ("close", Arity::Fixed(1), builtin_close),
211            ("read-line", Arity::Fixed(1), builtin_read_line),
212            ("write", Arity::Fixed(2), builtin_write),
213            ("flush", Arity::Fixed(1), builtin_flush),
214            ("reader?", Arity::Fixed(1), builtin_reader_q),
215            ("writer?", Arity::Fixed(1), builtin_writer_q),
216            ("file", Arity::Fixed(1), builtin_file),
217            (
218                "delete-file",
219                Arity::Variadic { min: 1 },
220                builtin_delete_file
221            ),
222            ("make-parents", Arity::Fixed(1), builtin_make_parents),
223        ]
224    );
225}
226
227fn builtin_reader(args: &[Value]) -> ValueResult<Value> {
228    let path = match &args[0] {
229        Value::Str(s) => s.get().clone(),
230        v => {
231            return Err(ValueError::WrongType {
232                expected: "string",
233                got: v.type_name().to_string(),
234            });
235        }
236    };
237    let reader = IoReader::open(&path)?;
238    Ok(Value::Resource(ResourceHandle::new(reader)))
239}
240
241fn builtin_writer(args: &[Value]) -> ValueResult<Value> {
242    let path = match &args[0] {
243        Value::Str(s) => s.get().clone(),
244        v => {
245            return Err(ValueError::WrongType {
246                expected: "string",
247                got: v.type_name().to_string(),
248            });
249        }
250    };
251    // Check for :append option
252    let append = if args.len() >= 3 {
253        matches!(
254            (&args[1], &args[2]),
255            (Value::Keyword(k), Value::Bool(true)) if k.get().name.as_ref() == "append"
256        )
257    } else {
258        false
259    };
260    let writer = IoWriter::open(&path, append)?;
261    Ok(Value::Resource(ResourceHandle::new(writer)))
262}
263
264fn builtin_string_reader(args: &[Value]) -> ValueResult<Value> {
265    let s = match &args[0] {
266        Value::Str(s) => s.get().clone(),
267        v => {
268            return Err(ValueError::WrongType {
269                expected: "string",
270                got: v.type_name().to_string(),
271            });
272        }
273    };
274    Ok(Value::Resource(ResourceHandle::new(StringReader::new(s))))
275}
276
277fn builtin_close(args: &[Value]) -> ValueResult<Value> {
278    match &args[0] {
279        Value::Resource(r) => {
280            r.close()?;
281            Ok(Value::Nil)
282        }
283        v => Err(ValueError::WrongType {
284            expected: "resource",
285            got: v.type_name().to_string(),
286        }),
287    }
288}
289
290fn builtin_read_line(args: &[Value]) -> ValueResult<Value> {
291    match &args[0] {
292        Value::Resource(r) => {
293            if let Some(reader) = r.downcast::<IoReader>() {
294                match reader.read_line()? {
295                    Some(line) => Ok(Value::string(line)),
296                    None => Ok(Value::Nil),
297                }
298            } else if let Some(reader) = r.downcast::<StringReader>() {
299                match reader.read_line()? {
300                    Some(line) => Ok(Value::string(line)),
301                    None => Ok(Value::Nil),
302                }
303            } else {
304                Err(ValueError::Other("not a readable resource".into()))
305            }
306        }
307        v => Err(ValueError::WrongType {
308            expected: "reader",
309            got: v.type_name().to_string(),
310        }),
311    }
312}
313
314fn builtin_write(args: &[Value]) -> ValueResult<Value> {
315    match &args[0] {
316        Value::Resource(r) => {
317            let s = match &args[1] {
318                Value::Str(s) => s.get().clone(),
319                v => format!("{v}"),
320            };
321            if let Some(writer) = r.downcast::<IoWriter>() {
322                writer.write_str(&s)?;
323                Ok(Value::Nil)
324            } else {
325                Err(ValueError::Other("not a writable resource".into()))
326            }
327        }
328        v => Err(ValueError::WrongType {
329            expected: "writer",
330            got: v.type_name().to_string(),
331        }),
332    }
333}
334
335fn builtin_flush(args: &[Value]) -> ValueResult<Value> {
336    match &args[0] {
337        Value::Resource(r) => {
338            if let Some(writer) = r.downcast::<IoWriter>() {
339                writer.flush()?;
340                Ok(Value::Nil)
341            } else {
342                Err(ValueError::Other("not a writable resource".into()))
343            }
344        }
345        v => Err(ValueError::WrongType {
346            expected: "writer",
347            got: v.type_name().to_string(),
348        }),
349    }
350}
351
352fn builtin_reader_q(args: &[Value]) -> ValueResult<Value> {
353    Ok(Value::Bool(matches!(&args[0], Value::Resource(r) if
354        r.downcast::<IoReader>().is_some() || r.downcast::<StringReader>().is_some()
355    )))
356}
357
358fn builtin_writer_q(args: &[Value]) -> ValueResult<Value> {
359    Ok(Value::Bool(matches!(
360        &args[0],
361        Value::Resource(r) if r.downcast::<IoWriter>().is_some()
362    )))
363}
364
365fn builtin_file(args: &[Value]) -> ValueResult<Value> {
366    match &args[0] {
367        Value::Str(s) => Ok(Value::string(s.get().clone())),
368        v => Err(ValueError::WrongType {
369            expected: "string",
370            got: v.type_name().to_string(),
371        }),
372    }
373}
374
375fn builtin_delete_file(args: &[Value]) -> ValueResult<Value> {
376    let path = match &args[0] {
377        Value::Str(s) => s.get().clone(),
378        v => {
379            return Err(ValueError::WrongType {
380                expected: "string",
381                got: v.type_name().to_string(),
382            });
383        }
384    };
385    let silently = args.len() >= 2 && args[1] != Value::Nil && args[1] != Value::Bool(false);
386    match std::fs::remove_file(&path) {
387        Ok(()) => Ok(Value::Bool(true)),
388        Err(_) if silently => Ok(Value::Bool(false)),
389        Err(e) => Err(ValueError::Other(format!("cannot delete {path}: {e}"))),
390    }
391}
392
393fn builtin_make_parents(args: &[Value]) -> ValueResult<Value> {
394    let path = match &args[0] {
395        Value::Str(s) => s.get().clone(),
396        v => {
397            return Err(ValueError::WrongType {
398                expected: "string",
399                got: v.type_name().to_string(),
400            });
401        }
402    };
403    if let Some(parent) = std::path::Path::new(&path).parent() {
404        std::fs::create_dir_all(parent)
405            .map_err(|e| ValueError::Other(format!("cannot create dirs: {e}")))?;
406    }
407    Ok(Value::Bool(true))
408}