1use 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#[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) } 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#[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 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#[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
200pub 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 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}