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("std::core::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 = 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 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 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 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 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 writer
220 .write_record(&headers)
221 .map_err(|e| format!("csv.stringify_records() failed: {}", e))?;
222
223 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 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 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 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 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 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 let (keys0, values0, _) = records[0].as_hashmap().expect("should be hashmap");
466 assert_eq!(keys0.len(), 2);
467 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 let lines: Vec<&str> = output.trim().lines().collect();
567 assert_eq!(lines.len(), 3);
568 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}