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