Skip to main content

shape_runtime/stdlib/
csv_module.rs

1//! Native `csv` module for CSV parsing and serialization.
2//!
3//! Exports: csv.parse, csv.parse_records, csv.stringify, csv.stringify_records,
4//!          csv.read_file, csv.is_valid
5
6use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
7use shape_value::ValueWord;
8use std::sync::Arc;
9
10/// Create the `csv` module with CSV parsing and serialization functions.
11pub fn create_csv_module() -> ModuleExports {
12    let mut module = ModuleExports::new("std::core::csv");
13    module.description = "CSV parsing and serialization".to_string();
14
15    // csv.parse(text: string) -> Array<Array<string>>
16    module.add_function_with_schema(
17        "parse",
18        |args: &[ValueWord], _ctx: &ModuleContext| {
19            let text = args
20                .first()
21                .and_then(|a| a.as_str())
22                .ok_or_else(|| "csv.parse() requires a string argument".to_string())?;
23
24            let mut reader = csv::ReaderBuilder::new()
25                .has_headers(false)
26                .from_reader(text.as_bytes());
27
28            let mut rows: Vec<ValueWord> = Vec::new();
29            for result in reader.records() {
30                let record = result.map_err(|e| format!("csv.parse() failed: {}", e))?;
31                let row: Vec<ValueWord> = record
32                    .iter()
33                    .map(|field| ValueWord::from_string(Arc::new(field.to_string())))
34                    .collect();
35                rows.push(ValueWord::from_array(Arc::new(row)));
36            }
37
38            Ok(ValueWord::from_array(Arc::new(rows)))
39        },
40        ModuleFunction {
41            description: "Parse CSV text into an array of rows (each row is an array of strings)"
42                .to_string(),
43            params: vec![ModuleParam {
44                name: "text".to_string(),
45                type_name: "string".to_string(),
46                required: true,
47                description: "CSV text to parse".to_string(),
48                ..Default::default()
49            }],
50            return_type: Some("Array<Array<string>>".to_string()),
51        },
52    );
53
54    // csv.parse_records(text: string) -> Array<HashMap<string, string>>
55    module.add_function_with_schema(
56        "parse_records",
57        |args: &[ValueWord], _ctx: &ModuleContext| {
58            let text = args
59                .first()
60                .and_then(|a| a.as_str())
61                .ok_or_else(|| "csv.parse_records() requires a string argument".to_string())?;
62
63            let mut reader = csv::ReaderBuilder::new()
64                .has_headers(true)
65                .from_reader(text.as_bytes());
66
67            let headers: Vec<String> = reader
68                .headers()
69                .map_err(|e| format!("csv.parse_records() failed to read headers: {}", e))?
70                .iter()
71                .map(|h| h.to_string())
72                .collect();
73
74            let mut records: Vec<ValueWord> = Vec::new();
75            for result in reader.records() {
76                let record = result.map_err(|e| format!("csv.parse_records() failed: {}", e))?;
77                let mut keys = Vec::with_capacity(headers.len());
78                let mut values = Vec::with_capacity(headers.len());
79                for (i, field) in record.iter().enumerate() {
80                    if i < headers.len() {
81                        keys.push(ValueWord::from_string(Arc::new(headers[i].clone())));
82                        values.push(ValueWord::from_string(Arc::new(field.to_string())));
83                    }
84                }
85                records.push(ValueWord::from_hashmap_pairs(keys, values));
86            }
87
88            Ok(ValueWord::from_array(Arc::new(records)))
89        },
90        ModuleFunction {
91            description:
92                "Parse CSV text using the header row as keys, returning an array of hashmaps"
93                    .to_string(),
94            params: vec![ModuleParam {
95                name: "text".to_string(),
96                type_name: "string".to_string(),
97                required: true,
98                description: "CSV text to parse (first row is headers)".to_string(),
99                ..Default::default()
100            }],
101            return_type: Some("Array<HashMap<string, string>>".to_string()),
102        },
103    );
104
105    // csv.stringify(data: Array<Array<string>>, delimiter?: string) -> string
106    module.add_function_with_schema(
107        "stringify",
108        |args: &[ValueWord], _ctx: &ModuleContext| {
109            let data = args
110                .first()
111                .and_then(|a| a.as_any_array())
112                .ok_or_else(|| {
113                    "csv.stringify() requires an Array<Array<string>> argument".to_string()
114                })?
115                .to_generic();
116
117            let delimiter = args
118                .get(1)
119                .and_then(|a| a.as_str())
120                .and_then(|s| s.as_bytes().first().copied())
121                .unwrap_or(b',');
122
123            let mut writer = csv::WriterBuilder::new()
124                .delimiter(delimiter)
125                .from_writer(Vec::new());
126
127            for row_val in data.iter() {
128                let row_arr = row_val.as_any_array().ok_or_else(|| {
129                    "csv.stringify() each row must be an Array<string>".to_string()
130                })?;
131                let row = row_arr.to_generic();
132                let fields: Vec<String> = row
133                    .iter()
134                    .map(|f| {
135                        f.as_str()
136                            .map(|s| s.to_string())
137                            .unwrap_or_else(|| f.to_string())
138                    })
139                    .collect();
140                writer
141                    .write_record(&fields)
142                    .map_err(|e| format!("csv.stringify() failed: {}", e))?;
143            }
144
145            let bytes = writer
146                .into_inner()
147                .map_err(|e| format!("csv.stringify() failed to flush: {}", e))?;
148            let output = String::from_utf8(bytes)
149                .map_err(|e| format!("csv.stringify() UTF-8 error: {}", e))?;
150
151            Ok(ValueWord::from_string(Arc::new(output)))
152        },
153        ModuleFunction {
154            description: "Convert an array of rows to a CSV string".to_string(),
155            params: vec![
156                ModuleParam {
157                    name: "data".to_string(),
158                    type_name: "Array<Array<string>>".to_string(),
159                    required: true,
160                    description: "Array of rows, each row is an array of field strings".to_string(),
161                    ..Default::default()
162                },
163                ModuleParam {
164                    name: "delimiter".to_string(),
165                    type_name: "string".to_string(),
166                    required: false,
167                    description: "Field delimiter character (default: comma)".to_string(),
168                    default_snippet: Some("\",\"".to_string()),
169                    ..Default::default()
170                },
171            ],
172            return_type: Some("string".to_string()),
173        },
174    );
175
176    // csv.stringify_records(data: Array<HashMap<string, string>>, headers?: Array<string>) -> string
177    module.add_function_with_schema(
178        "stringify_records",
179        |args: &[ValueWord], _ctx: &ModuleContext| {
180            let data = args
181                .first()
182                .and_then(|a| a.as_any_array())
183                .ok_or_else(|| {
184                    "csv.stringify_records() requires an Array<HashMap<string, string>> argument"
185                        .to_string()
186                })?
187                .to_generic();
188
189            // Determine headers: explicit argument or from first record's keys.
190            let explicit_headers: Option<Vec<String>> = args.get(1).and_then(|a| {
191                let arr = a.as_any_array()?.to_generic();
192                let headers: Vec<String> = arr
193                    .iter()
194                    .filter_map(|h| h.as_str().map(|s| s.to_string()))
195                    .collect();
196                if headers.is_empty() {
197                    None
198                } else {
199                    Some(headers)
200                }
201            });
202
203            let headers = if let Some(h) = explicit_headers {
204                h
205            } else if let Some(first) = data.first() {
206                let (keys, _, _) = first.as_hashmap().ok_or_else(|| {
207                    "csv.stringify_records() each element must be a HashMap".to_string()
208                })?;
209                keys.iter()
210                    .filter_map(|k| k.as_str().map(|s| s.to_string()))
211                    .collect()
212            } else {
213                return Ok(ValueWord::from_string(Arc::new(String::new())));
214            };
215
216            let mut writer = csv::WriterBuilder::new().from_writer(Vec::new());
217
218            // Write header row
219            writer
220                .write_record(&headers)
221                .map_err(|e| format!("csv.stringify_records() failed: {}", e))?;
222
223            // Write data rows
224            for record_val in data.iter() {
225                let (keys, values, _) = record_val.as_hashmap().ok_or_else(|| {
226                    "csv.stringify_records() each element must be a HashMap".to_string()
227                })?;
228
229                let mut row = Vec::with_capacity(headers.len());
230                for header in &headers {
231                    // Find the value for this header key
232                    let mut found = String::new();
233                    for (i, k) in keys.iter().enumerate() {
234                        if let Some(key_str) = k.as_str() {
235                            if key_str == header {
236                                found = values[i]
237                                    .as_str()
238                                    .map(|s| s.to_string())
239                                    .unwrap_or_else(|| values[i].to_string());
240                                break;
241                            }
242                        }
243                    }
244                    row.push(found);
245                }
246
247                writer
248                    .write_record(&row)
249                    .map_err(|e| format!("csv.stringify_records() failed: {}", e))?;
250            }
251
252            let bytes = writer
253                .into_inner()
254                .map_err(|e| format!("csv.stringify_records() failed to flush: {}", e))?;
255            let output = String::from_utf8(bytes)
256                .map_err(|e| format!("csv.stringify_records() UTF-8 error: {}", e))?;
257
258            Ok(ValueWord::from_string(Arc::new(output)))
259        },
260        ModuleFunction {
261            description: "Convert an array of hashmaps to a CSV string with headers".to_string(),
262            params: vec![
263                ModuleParam {
264                    name: "data".to_string(),
265                    type_name: "Array<HashMap<string, string>>".to_string(),
266                    required: true,
267                    description: "Array of records (hashmaps with string keys and values)"
268                        .to_string(),
269                    ..Default::default()
270                },
271                ModuleParam {
272                    name: "headers".to_string(),
273                    type_name: "Array<string>".to_string(),
274                    required: false,
275                    description: "Explicit header order (default: keys from first record)"
276                        .to_string(),
277                    ..Default::default()
278                },
279            ],
280            return_type: Some("string".to_string()),
281        },
282    );
283
284    // csv.read_file(path: string) -> Result<Array<Array<string>>>
285    module.add_function_with_schema(
286        "read_file",
287        |args: &[ValueWord], _ctx: &ModuleContext| {
288            let path = args
289                .first()
290                .and_then(|a| a.as_str())
291                .ok_or_else(|| "csv.read_file() requires a path string".to_string())?;
292
293            let text = std::fs::read_to_string(path)
294                .map_err(|e| format!("csv.read_file() failed to read '{}': {}", path, e))?;
295
296            let mut reader = csv::ReaderBuilder::new()
297                .has_headers(false)
298                .from_reader(text.as_bytes());
299
300            let mut rows: Vec<ValueWord> = Vec::new();
301            for result in reader.records() {
302                let record = result.map_err(|e| format!("csv.read_file() parse error: {}", e))?;
303                let row: Vec<ValueWord> = record
304                    .iter()
305                    .map(|field| ValueWord::from_string(Arc::new(field.to_string())))
306                    .collect();
307                rows.push(ValueWord::from_array(Arc::new(row)));
308            }
309
310            Ok(ValueWord::from_ok(ValueWord::from_array(Arc::new(rows))))
311        },
312        ModuleFunction {
313            description: "Read and parse a CSV file into an array of rows".to_string(),
314            params: vec![ModuleParam {
315                name: "path".to_string(),
316                type_name: "string".to_string(),
317                required: true,
318                description: "Path to the CSV file".to_string(),
319                ..Default::default()
320            }],
321            return_type: Some("Result<Array<Array<string>>>".to_string()),
322        },
323    );
324
325    // csv.is_valid(text: string) -> bool
326    module.add_function_with_schema(
327        "is_valid",
328        |args: &[ValueWord], _ctx: &ModuleContext| {
329            let text = args
330                .first()
331                .and_then(|a| a.as_str())
332                .ok_or_else(|| "csv.is_valid() requires a string argument".to_string())?;
333
334            let mut reader = csv::ReaderBuilder::new()
335                .has_headers(false)
336                .from_reader(text.as_bytes());
337
338            let valid = reader.records().all(|r| r.is_ok());
339            Ok(ValueWord::from_bool(valid))
340        },
341        ModuleFunction {
342            description: "Check if a string is valid CSV".to_string(),
343            params: vec![ModuleParam {
344                name: "text".to_string(),
345                type_name: "string".to_string(),
346                required: true,
347                description: "String to validate as CSV".to_string(),
348                ..Default::default()
349            }],
350            return_type: Some("bool".to_string()),
351        },
352    );
353
354    module
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
362        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
363        crate::module_exports::ModuleContext {
364            schemas: registry,
365            invoke_callable: None,
366            raw_invoker: None,
367            function_hashes: None,
368            vm_state: None,
369            granted_permissions: None,
370            scope_constraints: None,
371            set_pending_resume: None,
372            set_pending_frame_resume: None,
373        }
374    }
375
376    #[test]
377    fn test_csv_module_creation() {
378        let module = create_csv_module();
379        assert_eq!(module.name, "std::core::csv");
380        assert!(module.has_export("parse"));
381        assert!(module.has_export("parse_records"));
382        assert!(module.has_export("stringify"));
383        assert!(module.has_export("stringify_records"));
384        assert!(module.has_export("read_file"));
385        assert!(module.has_export("is_valid"));
386    }
387
388    #[test]
389    fn test_csv_parse_simple() {
390        let module = create_csv_module();
391        let parse_fn = module.get_export("parse").unwrap();
392        let ctx = test_ctx();
393        let input = ValueWord::from_string(Arc::new("a,b,c\n1,2,3\n4,5,6".to_string()));
394        let result = parse_fn(&[input], &ctx).unwrap();
395        let rows = result.as_any_array().expect("should be array").to_generic();
396        assert_eq!(rows.len(), 3);
397        // First row
398        let row0 = rows[0]
399            .as_any_array()
400            .expect("row should be array")
401            .to_generic();
402        assert_eq!(row0.len(), 3);
403        assert_eq!(row0[0].as_str(), Some("a"));
404        assert_eq!(row0[1].as_str(), Some("b"));
405        assert_eq!(row0[2].as_str(), Some("c"));
406        // Second row
407        let row1 = rows[1]
408            .as_any_array()
409            .expect("row should be array")
410            .to_generic();
411        assert_eq!(row1[0].as_str(), Some("1"));
412        assert_eq!(row1[1].as_str(), Some("2"));
413        assert_eq!(row1[2].as_str(), Some("3"));
414    }
415
416    #[test]
417    fn test_csv_parse_quoted_fields() {
418        let module = create_csv_module();
419        let parse_fn = module.get_export("parse").unwrap();
420        let ctx = test_ctx();
421        let input =
422            ValueWord::from_string(Arc::new("\"hello, world\",\"foo\"\"bar\"\n".to_string()));
423        let result = parse_fn(&[input], &ctx).unwrap();
424        let rows = result.as_any_array().expect("should be array").to_generic();
425        assert_eq!(rows.len(), 1);
426        let row0 = rows[0]
427            .as_any_array()
428            .expect("row should be array")
429            .to_generic();
430        assert_eq!(row0[0].as_str(), Some("hello, world"));
431        assert_eq!(row0[1].as_str(), Some("foo\"bar"));
432    }
433
434    #[test]
435    fn test_csv_parse_empty() {
436        let module = create_csv_module();
437        let parse_fn = module.get_export("parse").unwrap();
438        let ctx = test_ctx();
439        let input = ValueWord::from_string(Arc::new("".to_string()));
440        let result = parse_fn(&[input], &ctx).unwrap();
441        let rows = result.as_any_array().expect("should be array").to_generic();
442        assert_eq!(rows.len(), 0);
443    }
444
445    #[test]
446    fn test_csv_parse_requires_string() {
447        let module = create_csv_module();
448        let parse_fn = module.get_export("parse").unwrap();
449        let ctx = test_ctx();
450        let result = parse_fn(&[ValueWord::from_f64(42.0)], &ctx);
451        assert!(result.is_err());
452    }
453
454    #[test]
455    fn test_csv_parse_records() {
456        let module = create_csv_module();
457        let parse_fn = module.get_export("parse_records").unwrap();
458        let ctx = test_ctx();
459        let input = ValueWord::from_string(Arc::new("name,age\nAlice,30\nBob,25".to_string()));
460        let result = parse_fn(&[input], &ctx).unwrap();
461        let records = result.as_any_array().expect("should be array").to_generic();
462        assert_eq!(records.len(), 2);
463
464        // First record: {name: "Alice", age: "30"}
465        let (keys0, values0, _) = records[0].as_hashmap().expect("should be hashmap");
466        assert_eq!(keys0.len(), 2);
467        // Find "name" key
468        let mut found_name = false;
469        let mut found_age = false;
470        for (i, k) in keys0.iter().enumerate() {
471            if k.as_str() == Some("name") {
472                assert_eq!(values0[i].as_str(), Some("Alice"));
473                found_name = true;
474            }
475            if k.as_str() == Some("age") {
476                assert_eq!(values0[i].as_str(), Some("30"));
477                found_age = true;
478            }
479        }
480        assert!(found_name, "should have 'name' key");
481        assert!(found_age, "should have 'age' key");
482    }
483
484    #[test]
485    fn test_csv_parse_records_requires_string() {
486        let module = create_csv_module();
487        let parse_fn = module.get_export("parse_records").unwrap();
488        let ctx = test_ctx();
489        let result = parse_fn(&[ValueWord::from_f64(42.0)], &ctx);
490        assert!(result.is_err());
491    }
492
493    #[test]
494    fn test_csv_stringify_simple() {
495        let module = create_csv_module();
496        let stringify_fn = module.get_export("stringify").unwrap();
497        let ctx = test_ctx();
498        let data = ValueWord::from_array(Arc::new(vec![
499            ValueWord::from_array(Arc::new(vec![
500                ValueWord::from_string(Arc::new("a".to_string())),
501                ValueWord::from_string(Arc::new("b".to_string())),
502            ])),
503            ValueWord::from_array(Arc::new(vec![
504                ValueWord::from_string(Arc::new("1".to_string())),
505                ValueWord::from_string(Arc::new("2".to_string())),
506            ])),
507        ]));
508        let result = stringify_fn(&[data], &ctx).unwrap();
509        let output = result.as_str().expect("should be string");
510        assert_eq!(output, "a,b\n1,2\n");
511    }
512
513    #[test]
514    fn test_csv_stringify_with_delimiter() {
515        let module = create_csv_module();
516        let stringify_fn = module.get_export("stringify").unwrap();
517        let ctx = test_ctx();
518        let data = ValueWord::from_array(Arc::new(vec![ValueWord::from_array(Arc::new(vec![
519            ValueWord::from_string(Arc::new("a".to_string())),
520            ValueWord::from_string(Arc::new("b".to_string())),
521        ]))]));
522        let delimiter = ValueWord::from_string(Arc::new("\t".to_string()));
523        let result = stringify_fn(&[data, delimiter], &ctx).unwrap();
524        let output = result.as_str().expect("should be string");
525        assert_eq!(output, "a\tb\n");
526    }
527
528    #[test]
529    fn test_csv_stringify_requires_array() {
530        let module = create_csv_module();
531        let stringify_fn = module.get_export("stringify").unwrap();
532        let ctx = test_ctx();
533        let result = stringify_fn(&[ValueWord::from_f64(42.0)], &ctx);
534        assert!(result.is_err());
535    }
536
537    #[test]
538    fn test_csv_stringify_records() {
539        let module = create_csv_module();
540        let stringify_fn = module.get_export("stringify_records").unwrap();
541        let ctx = test_ctx();
542        let record1 = ValueWord::from_hashmap_pairs(
543            vec![
544                ValueWord::from_string(Arc::new("name".to_string())),
545                ValueWord::from_string(Arc::new("age".to_string())),
546            ],
547            vec![
548                ValueWord::from_string(Arc::new("Alice".to_string())),
549                ValueWord::from_string(Arc::new("30".to_string())),
550            ],
551        );
552        let record2 = ValueWord::from_hashmap_pairs(
553            vec![
554                ValueWord::from_string(Arc::new("name".to_string())),
555                ValueWord::from_string(Arc::new("age".to_string())),
556            ],
557            vec![
558                ValueWord::from_string(Arc::new("Bob".to_string())),
559                ValueWord::from_string(Arc::new("25".to_string())),
560            ],
561        );
562        let data = ValueWord::from_array(Arc::new(vec![record1, record2]));
563        let result = stringify_fn(&[data], &ctx).unwrap();
564        let output = result.as_str().expect("should be string");
565        // Should contain header row and two data rows
566        let lines: Vec<&str> = output.trim().lines().collect();
567        assert_eq!(lines.len(), 3);
568        // Header should be name,age (order from first record's keys)
569        assert!(lines[0].contains("name"));
570        assert!(lines[0].contains("age"));
571    }
572
573    #[test]
574    fn test_csv_stringify_records_with_explicit_headers() {
575        let module = create_csv_module();
576        let stringify_fn = module.get_export("stringify_records").unwrap();
577        let ctx = test_ctx();
578        let record = ValueWord::from_hashmap_pairs(
579            vec![
580                ValueWord::from_string(Arc::new("name".to_string())),
581                ValueWord::from_string(Arc::new("age".to_string())),
582            ],
583            vec![
584                ValueWord::from_string(Arc::new("Alice".to_string())),
585                ValueWord::from_string(Arc::new("30".to_string())),
586            ],
587        );
588        let data = ValueWord::from_array(Arc::new(vec![record]));
589        let headers = ValueWord::from_array(Arc::new(vec![
590            ValueWord::from_string(Arc::new("age".to_string())),
591            ValueWord::from_string(Arc::new("name".to_string())),
592        ]));
593        let result = stringify_fn(&[data, headers], &ctx).unwrap();
594        let output = result.as_str().expect("should be string");
595        let lines: Vec<&str> = output.trim().lines().collect();
596        assert_eq!(lines[0], "age,name");
597        assert_eq!(lines[1], "30,Alice");
598    }
599
600    #[test]
601    fn test_csv_stringify_records_empty() {
602        let module = create_csv_module();
603        let stringify_fn = module.get_export("stringify_records").unwrap();
604        let ctx = test_ctx();
605        let data = ValueWord::from_array(Arc::new(vec![]));
606        let result = stringify_fn(&[data], &ctx).unwrap();
607        let output = result.as_str().expect("should be string");
608        assert_eq!(output, "");
609    }
610
611    #[test]
612    fn test_csv_is_valid_true() {
613        let module = create_csv_module();
614        let is_valid_fn = module.get_export("is_valid").unwrap();
615        let ctx = test_ctx();
616        let input = ValueWord::from_string(Arc::new("a,b,c\n1,2,3".to_string()));
617        let result = is_valid_fn(&[input], &ctx).unwrap();
618        assert_eq!(result.as_bool(), Some(true));
619    }
620
621    #[test]
622    fn test_csv_is_valid_empty() {
623        let module = create_csv_module();
624        let is_valid_fn = module.get_export("is_valid").unwrap();
625        let ctx = test_ctx();
626        let input = ValueWord::from_string(Arc::new("".to_string()));
627        let result = is_valid_fn(&[input], &ctx).unwrap();
628        assert_eq!(result.as_bool(), Some(true));
629    }
630
631    #[test]
632    fn test_csv_is_valid_requires_string() {
633        let module = create_csv_module();
634        let is_valid_fn = module.get_export("is_valid").unwrap();
635        let ctx = test_ctx();
636        let result = is_valid_fn(&[ValueWord::from_f64(42.0)], &ctx);
637        assert!(result.is_err());
638    }
639
640    #[test]
641    fn test_csv_read_file() {
642        let module = create_csv_module();
643        let read_fn = module.get_export("read_file").unwrap();
644        let ctx = test_ctx();
645
646        let dir = tempfile::tempdir().unwrap();
647        let path = dir.path().join("test.csv");
648        std::fs::write(&path, "a,b\n1,2\n3,4").unwrap();
649
650        let result = read_fn(
651            &[ValueWord::from_string(Arc::new(
652                path.to_str().unwrap().to_string(),
653            ))],
654            &ctx,
655        )
656        .unwrap();
657        let inner = result.as_ok_inner().expect("should be Ok");
658        let rows = inner.as_any_array().expect("should be array").to_generic();
659        assert_eq!(rows.len(), 3);
660        let row0 = rows[0]
661            .as_any_array()
662            .expect("row should be array")
663            .to_generic();
664        assert_eq!(row0[0].as_str(), Some("a"));
665        assert_eq!(row0[1].as_str(), Some("b"));
666    }
667
668    #[test]
669    fn test_csv_read_file_nonexistent() {
670        let module = create_csv_module();
671        let read_fn = module.get_export("read_file").unwrap();
672        let ctx = test_ctx();
673        let result = read_fn(
674            &[ValueWord::from_string(Arc::new(
675                "/nonexistent/path/file.csv".to_string(),
676            ))],
677            &ctx,
678        );
679        assert!(result.is_err());
680    }
681
682    #[test]
683    fn test_csv_read_file_requires_string() {
684        let module = create_csv_module();
685        let read_fn = module.get_export("read_file").unwrap();
686        let ctx = test_ctx();
687        let result = read_fn(&[ValueWord::from_f64(42.0)], &ctx);
688        assert!(result.is_err());
689    }
690
691    #[test]
692    fn test_csv_schemas() {
693        let module = create_csv_module();
694
695        let parse_schema = module.get_schema("parse").unwrap();
696        assert_eq!(parse_schema.params.len(), 1);
697        assert_eq!(parse_schema.params[0].name, "text");
698        assert!(parse_schema.params[0].required);
699        assert_eq!(
700            parse_schema.return_type.as_deref(),
701            Some("Array<Array<string>>")
702        );
703
704        let parse_records_schema = module.get_schema("parse_records").unwrap();
705        assert_eq!(parse_records_schema.params.len(), 1);
706        assert_eq!(
707            parse_records_schema.return_type.as_deref(),
708            Some("Array<HashMap<string, string>>")
709        );
710
711        let stringify_schema = module.get_schema("stringify").unwrap();
712        assert_eq!(stringify_schema.params.len(), 2);
713        assert!(stringify_schema.params[0].required);
714        assert!(!stringify_schema.params[1].required);
715        assert_eq!(stringify_schema.return_type.as_deref(), Some("string"));
716
717        let stringify_records_schema = module.get_schema("stringify_records").unwrap();
718        assert_eq!(stringify_records_schema.params.len(), 2);
719        assert!(stringify_records_schema.params[0].required);
720        assert!(!stringify_records_schema.params[1].required);
721
722        let read_file_schema = module.get_schema("read_file").unwrap();
723        assert_eq!(read_file_schema.params.len(), 1);
724        assert_eq!(
725            read_file_schema.return_type.as_deref(),
726            Some("Result<Array<Array<string>>>")
727        );
728
729        let is_valid_schema = module.get_schema("is_valid").unwrap();
730        assert_eq!(is_valid_schema.params.len(), 1);
731        assert_eq!(is_valid_schema.return_type.as_deref(), Some("bool"));
732    }
733
734    #[test]
735    fn test_csv_roundtrip() {
736        let module = create_csv_module();
737        let parse_fn = module.get_export("parse").unwrap();
738        let stringify_fn = module.get_export("stringify").unwrap();
739        let ctx = test_ctx();
740
741        let csv_text = "name,age,city\nAlice,30,NYC\nBob,25,LA\n";
742        let parsed = parse_fn(
743            &[ValueWord::from_string(Arc::new(csv_text.to_string()))],
744            &ctx,
745        )
746        .unwrap();
747
748        let re_stringified = stringify_fn(&[parsed], &ctx).unwrap();
749        let output = re_stringified.as_str().expect("should be string");
750        assert_eq!(output, csv_text);
751    }
752
753    #[test]
754    fn test_csv_records_roundtrip() {
755        let module = create_csv_module();
756        let parse_records_fn = module.get_export("parse_records").unwrap();
757        let stringify_records_fn = module.get_export("stringify_records").unwrap();
758        let ctx = test_ctx();
759
760        let csv_text = "name,age\nAlice,30\nBob,25\n";
761        let parsed = parse_records_fn(
762            &[ValueWord::from_string(Arc::new(csv_text.to_string()))],
763            &ctx,
764        )
765        .unwrap();
766
767        let headers = ValueWord::from_array(Arc::new(vec![
768            ValueWord::from_string(Arc::new("name".to_string())),
769            ValueWord::from_string(Arc::new("age".to_string())),
770        ]));
771        let re_stringified = stringify_records_fn(&[parsed, headers], &ctx).unwrap();
772        let output = re_stringified.as_str().expect("should be string");
773        assert_eq!(output, csv_text);
774    }
775}