1use bnto_core::context::ProcessContext;
8use bnto_core::errors::BntoError;
9use bnto_core::processor::{FileData, NodeInput, NodeOutput, NodeProcessor, OutputFile};
10use bnto_core::progress::ProgressReporter;
11
12pub struct ConvertFormat;
14
15impl ConvertFormat {
16 pub fn new() -> Self {
17 Self
18 }
19}
20
21impl Default for ConvertFormat {
22 fn default() -> Self {
23 Self::new()
24 }
25}
26
27impl NodeProcessor for ConvertFormat {
30 fn name(&self) -> &str {
31 "spreadsheet-convert"
32 }
33
34 fn metadata(&self) -> bnto_core::NodeMetadata {
35 use bnto_core::metadata::*;
36 NodeMetadata {
37 node_type: "spreadsheet-convert".to_string(),
38 name: "CSV to JSON".to_string(),
39 description: "Convert CSV rows to a JSON array of objects".to_string(),
40 category: NodeCategory::Spreadsheet,
41 accepts: vec!["text/csv".to_string()],
42 platforms: vec!["browser".to_string()],
43 parameters: build_convert_parameters(),
44 input_cardinality: InputCardinality::PerFile,
45 requires: vec![],
46 }
47 }
48
49 fn process(
50 &self,
51 input: NodeInput,
52 progress: &ProgressReporter,
53 _ctx: &dyn ProcessContext,
54 ) -> Result<NodeOutput, BntoError> {
55 progress.report(0, "Parsing CSV...");
56 let data = input
57 .data
58 .into_bytes()
59 .map_err(|e| BntoError::ProcessingFailed(format!("Failed to read input: {e}")))?;
60 let config = ConvertConfig::from_params(&input.params);
61 let csv_text = parse_and_strip_bom(&data)?;
62
63 progress.report(10, "Reading headers...");
64 let delimiter = resolve_delimiter(&config.delimiter);
65 let headers = read_headers(csv_text, delimiter)?;
66
67 progress.report(20, "Converting rows to JSON...");
68 let rows = read_rows(csv_text, delimiter, &headers)?;
69
70 progress.report(70, "Serializing JSON...");
71 let json_bytes = serialize_json(&rows, config.pretty)?;
72
73 progress.report(90, "Building result...");
74 let metadata = build_convert_metadata(headers.len(), rows.len(), json_bytes.len());
75 let output_filename = generate_output_filename(&input.filename);
76
77 progress.report(100, "Done!");
78 Ok(NodeOutput {
79 files: vec![OutputFile {
80 data: FileData::Bytes(json_bytes),
81 filename: output_filename,
82 mime_type: "application/json".to_string(),
83 metadata: serde_json::Map::new(),
84 }],
85 metadata,
86 })
87 }
88}
89
90struct ConvertConfig {
93 delimiter: String,
94 pretty: bool,
95}
96
97impl ConvertConfig {
98 fn from_params(params: &serde_json::Map<String, serde_json::Value>) -> Self {
99 Self {
100 delimiter: params
101 .get("delimiter")
102 .and_then(|v| v.as_str())
103 .unwrap_or(",")
104 .to_string(),
105 pretty: params
106 .get("pretty")
107 .and_then(|v| v.as_bool())
108 .unwrap_or(false),
109 }
110 }
111}
112
113fn resolve_delimiter(delimiter: &str) -> u8 {
115 match delimiter {
116 "comma" | "," => b',',
117 "semicolon" | ";" => b';',
118 "tab" | "\t" => b'\t',
119 "pipe" | "|" => b'|',
120 other if other.len() == 1 => other.as_bytes()[0],
121 _ => b',',
122 }
123}
124
125fn build_convert_parameters() -> Vec<bnto_core::metadata::ParameterDef> {
128 use bnto_core::metadata::*;
129 vec![
130 ParameterDef {
131 name: "delimiter".to_string(),
132 label: "Delimiter".to_string(),
133 description: "Column separator character".to_string(),
134 param_type: ParameterType::Enum {
135 options: vec![
136 OptionEntry {
137 value: "comma".to_string(),
138 label: "Comma".to_string(),
139 },
140 OptionEntry {
141 value: "semicolon".to_string(),
142 label: "Semicolon".to_string(),
143 },
144 OptionEntry {
145 value: "tab".to_string(),
146 label: "Tab".to_string(),
147 },
148 OptionEntry {
149 value: "pipe".to_string(),
150 label: "Pipe".to_string(),
151 },
152 ],
153 },
154 default: Some(serde_json::json!("comma")),
155 ..Default::default()
156 },
157 ParameterDef {
158 name: "pretty".to_string(),
159 label: "Pretty Print".to_string(),
160 description: "Format output JSON with indentation".to_string(),
161 param_type: ParameterType::Boolean,
162 default: Some(serde_json::json!(false)),
163 ..Default::default()
164 },
165 ]
166}
167
168fn parse_and_strip_bom(data: &[u8]) -> Result<&str, BntoError> {
172 let data = if data.starts_with(&[0xEF, 0xBB, 0xBF]) {
174 &data[3..]
175 } else {
176 data
177 };
178
179 let text = std::str::from_utf8(data).map_err(|e| {
180 BntoError::InvalidInput(format!(
181 "File is not valid UTF-8 text (is this really a CSV?): {e}"
182 ))
183 })?;
184
185 if text.trim().is_empty() {
186 return Err(BntoError::InvalidInput(
187 "CSV file is empty — no data to convert".to_string(),
188 ));
189 }
190
191 Ok(text)
192}
193
194fn read_headers(csv_text: &str, delimiter: u8) -> Result<Vec<String>, BntoError> {
196 let mut reader = csv::ReaderBuilder::new()
197 .has_headers(true)
198 .delimiter(delimiter)
199 .flexible(true)
200 .from_reader(csv_text.as_bytes());
201
202 let headers = reader
203 .headers()
204 .map_err(|e| BntoError::ProcessingFailed(format!("Failed to read CSV headers: {e}")))?
205 .clone();
206
207 let names: Vec<String> = headers.iter().map(|h| h.trim().to_string()).collect();
208
209 if names.is_empty() {
210 return Err(BntoError::InvalidInput(
211 "CSV has no columns — cannot convert to JSON".to_string(),
212 ));
213 }
214
215 Ok(names)
216}
217
218fn read_rows(
220 csv_text: &str,
221 delimiter: u8,
222 headers: &[String],
223) -> Result<Vec<serde_json::Map<String, serde_json::Value>>, BntoError> {
224 let mut reader = csv::ReaderBuilder::new()
225 .has_headers(true)
226 .delimiter(delimiter)
227 .flexible(true)
228 .from_reader(csv_text.as_bytes());
229
230 let mut rows = Vec::new();
231
232 for result in reader.records() {
233 let record = result
234 .map_err(|e| BntoError::ProcessingFailed(format!("Failed to read CSV row: {e}")))?;
235
236 let mut obj = serde_json::Map::new();
237 for (i, header) in headers.iter().enumerate() {
238 let value = record.get(i).unwrap_or("").to_string();
239 obj.insert(header.clone(), serde_json::Value::String(value));
240 }
241 rows.push(obj);
242 }
243
244 Ok(rows)
245}
246
247fn serialize_json(
250 rows: &[serde_json::Map<String, serde_json::Value>],
251 pretty: bool,
252) -> Result<Vec<u8>, BntoError> {
253 let json_value = serde_json::Value::Array(
254 rows.iter()
255 .map(|obj| serde_json::Value::Object(obj.clone()))
256 .collect(),
257 );
258
259 let bytes = if pretty {
260 serde_json::to_vec_pretty(&json_value)
261 } else {
262 serde_json::to_vec(&json_value)
263 }
264 .map_err(|e| BntoError::ProcessingFailed(format!("Failed to serialize JSON: {e}")))?;
265
266 Ok(bytes)
267}
268
269fn build_convert_metadata(
272 column_count: usize,
273 row_count: usize,
274 output_size: usize,
275) -> serde_json::Map<String, serde_json::Value> {
276 let mut metadata = serde_json::Map::new();
277 metadata.insert(
278 "columnCount".to_string(),
279 serde_json::Value::Number(column_count.into()),
280 );
281 metadata.insert(
282 "rowCount".to_string(),
283 serde_json::Value::Number(row_count.into()),
284 );
285 metadata.insert(
286 "outputSize".to_string(),
287 serde_json::Value::Number(output_size.into()),
288 );
289 metadata
290}
291
292fn generate_output_filename(original: &str) -> String {
296 if let Some(dot_pos) = original.rfind('.') {
297 let stem = &original[..dot_pos];
298 format!("{stem}.json")
299 } else {
300 format!("{original}.json")
301 }
302}
303
304#[cfg(test)]
309mod tests {
310 use super::*;
311 use bnto_core::NoopContext;
312
313 fn make_input(csv_text: &str) -> NodeInput {
316 NodeInput {
317 data: FileData::Bytes(csv_text.as_bytes().to_vec()),
318 filename: "test.csv".to_string(),
319 mime_type: Some("text/csv".to_string()),
320 params: serde_json::Map::new(),
321 }
322 }
323
324 fn make_input_with_params(
325 csv_text: &str,
326 params: serde_json::Map<String, serde_json::Value>,
327 ) -> NodeInput {
328 NodeInput {
329 data: FileData::Bytes(csv_text.as_bytes().to_vec()),
330 filename: "test.csv".to_string(),
331 mime_type: Some("text/csv".to_string()),
332 params,
333 }
334 }
335
336 fn make_input_bytes(data: Vec<u8>) -> NodeInput {
337 NodeInput {
338 data: FileData::Bytes(data),
339 filename: "test.csv".to_string(),
340 mime_type: Some("text/csv".to_string()),
341 params: serde_json::Map::new(),
342 }
343 }
344
345 fn output_json(output: &NodeOutput) -> serde_json::Value {
346 let bytes = output.files[0]
347 .data
348 .clone()
349 .into_bytes()
350 .expect("Should read bytes");
351 serde_json::from_slice(&bytes).expect("Output should be valid JSON")
352 }
353
354 fn process_ok(input: NodeInput) -> NodeOutput {
355 let processor = ConvertFormat::new();
356 let progress = ProgressReporter::new_noop();
357 processor
358 .process(input, &progress, &NoopContext)
359 .expect("should succeed")
360 }
361
362 fn process_err(input: NodeInput) -> BntoError {
363 let processor = ConvertFormat::new();
364 let progress = ProgressReporter::new_noop();
365 let result = processor.process(input, &progress, &NoopContext);
366 assert!(result.is_err(), "expected an error");
367 match result {
368 Err(e) => e,
369 Ok(_) => unreachable!(),
370 }
371 }
372
373 #[test]
376 fn test_name() {
377 assert_eq!(ConvertFormat::new().name(), "spreadsheet-convert");
378 }
379
380 #[test]
381 fn test_default() {
382 #[allow(clippy::default_constructed_unit_structs)]
383 let _p = ConvertFormat::default();
384 }
385
386 #[test]
387 fn test_metadata_node_type() {
388 let meta = ConvertFormat::new().metadata();
389 assert_eq!(meta.node_type, "spreadsheet-convert");
390 }
391
392 #[test]
393 fn test_metadata_has_delimiter_and_pretty_params() {
394 let meta = ConvertFormat::new().metadata();
395 let names: Vec<&str> = meta.parameters.iter().map(|p| p.name.as_str()).collect();
396 assert!(names.contains(&"delimiter"));
397 assert!(names.contains(&"pretty"));
398 }
399
400 #[test]
403 fn test_simple_csv_to_json() {
404 let csv = "name,age,city\nAlice,30,NYC\nBob,25,LA\n";
405 let output = process_ok(make_input(csv));
406 let json = output_json(&output);
407
408 let arr = json.as_array().expect("should be array");
409 assert_eq!(arr.len(), 2);
410 assert_eq!(arr[0]["name"], "Alice");
411 assert_eq!(arr[0]["age"], "30");
412 assert_eq!(arr[0]["city"], "NYC");
413 assert_eq!(arr[1]["name"], "Bob");
414 }
415
416 #[test]
417 fn test_values_are_strings() {
418 let csv = "id,price,active\n1,19.99,true\n";
419 let output = process_ok(make_input(csv));
420 let json = output_json(&output);
421
422 let row = &json[0];
423 assert!(row["id"].is_string(), "id should be a string");
424 assert!(row["price"].is_string(), "price should be a string");
425 assert!(row["active"].is_string(), "active should be a string");
426 assert_eq!(row["id"], "1");
427 assert_eq!(row["price"], "19.99");
428 assert_eq!(row["active"], "true");
429 }
430
431 #[test]
432 fn test_headers_only_produces_empty_array() {
433 let csv = "name,age,city\n";
434 let output = process_ok(make_input(csv));
435 let json = output_json(&output);
436
437 let arr = json.as_array().expect("should be array");
438 assert_eq!(arr.len(), 0);
439 }
440
441 #[test]
442 fn test_single_row() {
443 let csv = "x\n42\n";
444 let output = process_ok(make_input(csv));
445 let json = output_json(&output);
446
447 assert_eq!(json.as_array().unwrap().len(), 1);
448 assert_eq!(json[0]["x"], "42");
449 }
450
451 #[test]
452 fn test_single_column() {
453 let csv = "email\nalice@example.com\nbob@example.com\n";
454 let output = process_ok(make_input(csv));
455 let json = output_json(&output);
456
457 assert_eq!(json[0]["email"], "alice@example.com");
458 assert_eq!(json[1]["email"], "bob@example.com");
459 }
460
461 #[test]
464 fn test_empty_cells_become_empty_strings() {
465 let csv = "name,age,city\nAlice,,NYC\n,25,\n";
466 let output = process_ok(make_input(csv));
467 let json = output_json(&output);
468
469 assert_eq!(json[0]["age"], "");
470 assert_eq!(json[1]["name"], "");
471 assert_eq!(json[1]["city"], "");
472 }
473
474 #[test]
475 fn test_short_row_gets_empty_string_for_missing_columns() {
476 let csv = "name,age,city\nAlice\n";
477 let output = process_ok(make_input(csv));
478 let json = output_json(&output);
479
480 assert_eq!(json[0]["name"], "Alice");
481 assert_eq!(json[0]["age"], "");
482 assert_eq!(json[0]["city"], "");
483 }
484
485 #[test]
488 fn test_quoted_fields_with_commas() {
489 let csv = "name,bio\n\"Alice\",\"Loves coding, hiking\"\n";
490 let output = process_ok(make_input(csv));
491 let json = output_json(&output);
492
493 assert_eq!(json[0]["bio"], "Loves coding, hiking");
494 }
495
496 #[test]
497 fn test_quoted_fields_with_newlines() {
498 let csv = "name,bio\n\"Alice\",\"Line one\nLine two\"\n";
499 let output = process_ok(make_input(csv));
500 let json = output_json(&output);
501
502 assert_eq!(json[0]["bio"], "Line one\nLine two");
503 }
504
505 #[test]
506 fn test_escaped_quotes() {
507 let csv = "name,bio\n\"Alice\",\"Says \"\"hello\"\"\"\n";
508 let output = process_ok(make_input(csv));
509 let json = output_json(&output);
510
511 assert_eq!(json[0]["bio"], "Says \"hello\"");
512 }
513
514 #[test]
517 fn test_unicode_characters() {
518 let csv = "name,city\nMüller,München\n田中太郎,東京\n";
519 let output = process_ok(make_input(csv));
520 let json = output_json(&output);
521
522 assert_eq!(json[0]["name"], "Müller");
523 assert_eq!(json[0]["city"], "München");
524 assert_eq!(json[1]["name"], "田中太郎");
525 }
526
527 #[test]
528 fn test_emoji() {
529 let csv = "name,note\nEmma 🎉,rocket 🚀\n";
530 let output = process_ok(make_input(csv));
531 let json = output_json(&output);
532
533 assert_eq!(json[0]["name"], "Emma 🎉");
534 assert_eq!(json[0]["note"], "rocket 🚀");
535 }
536
537 #[test]
540 fn test_utf8_bom_stripped() {
541 let csv_with_bom = b"\xEF\xBB\xBFname,age\nAlice,30\n";
542 let output = process_ok(make_input_bytes(csv_with_bom.to_vec()));
543 let json = output_json(&output);
544
545 assert_eq!(json[0]["name"], "Alice");
547 assert!(json[0].get("\u{FEFF}name").is_none());
548 }
549
550 #[test]
553 fn test_crlf_line_endings() {
554 let csv = "name,age\r\nAlice,30\r\nBob,25\r\n";
555 let output = process_ok(make_input(csv));
556 let json = output_json(&output);
557
558 assert_eq!(json.as_array().unwrap().len(), 2);
559 assert_eq!(json[0]["name"], "Alice");
560 assert_eq!(json[1]["name"], "Bob");
561 }
562
563 #[test]
566 fn test_semicolon_delimiter() {
567 let csv = "name;age;city\nAlice;30;NYC\n";
568 let mut params = serde_json::Map::new();
569 params.insert("delimiter".into(), serde_json::json!("semicolon"));
570 let output = process_ok(make_input_with_params(csv, params));
571 let json = output_json(&output);
572
573 assert_eq!(json[0]["name"], "Alice");
574 assert_eq!(json[0]["age"], "30");
575 assert_eq!(json[0]["city"], "NYC");
576 }
577
578 #[test]
579 fn test_tab_delimiter() {
580 let csv = "name\tage\nAlice\t30\n";
581 let mut params = serde_json::Map::new();
582 params.insert("delimiter".into(), serde_json::json!("tab"));
583 let output = process_ok(make_input_with_params(csv, params));
584 let json = output_json(&output);
585
586 assert_eq!(json[0]["name"], "Alice");
587 assert_eq!(json[0]["age"], "30");
588 }
589
590 #[test]
591 fn test_pipe_delimiter() {
592 let csv = "name|age\nAlice|30\n";
593 let mut params = serde_json::Map::new();
594 params.insert("delimiter".into(), serde_json::json!("pipe"));
595 let output = process_ok(make_input_with_params(csv, params));
596 let json = output_json(&output);
597
598 assert_eq!(json[0]["name"], "Alice");
599 }
600
601 #[test]
602 fn test_default_delimiter_is_comma() {
603 let csv = "name,age\nAlice,30\n";
604 let output = process_ok(make_input(csv));
605 let json = output_json(&output);
606
607 assert_eq!(json[0]["name"], "Alice");
608 }
609
610 #[test]
613 fn test_compact_output_by_default() {
614 let csv = "name\nAlice\n";
615 let output = process_ok(make_input(csv));
616 let bytes = output.files[0].data.clone().into_bytes().unwrap();
617 let text = String::from_utf8(bytes).unwrap();
618
619 assert!(!text.contains('\n'), "Compact output should be single line");
621 }
622
623 #[test]
624 fn test_pretty_output() {
625 let csv = "name\nAlice\n";
626 let mut params = serde_json::Map::new();
627 params.insert("pretty".into(), serde_json::json!(true));
628 let output = process_ok(make_input_with_params(csv, params));
629 let bytes = output.files[0].data.clone().into_bytes().unwrap();
630 let text = String::from_utf8(bytes).unwrap();
631
632 assert!(text.contains('\n'), "Pretty output should have newlines");
633 assert!(text.contains(" "), "Pretty output should have indentation");
634 }
635
636 #[test]
637 fn test_pretty_changes_output() {
638 let csv = "name,age\nAlice,30\nBob,25\n";
639
640 let compact = process_ok(make_input(csv));
641 let compact_size = compact.files[0].data.len().unwrap();
642
643 let mut params = serde_json::Map::new();
644 params.insert("pretty".into(), serde_json::json!(true));
645 let pretty = process_ok(make_input_with_params(csv, params));
646 let pretty_size = pretty.files[0].data.len().unwrap();
647
648 assert!(
649 pretty_size > compact_size,
650 "Pretty should be larger: {pretty_size} vs {compact_size}"
651 );
652 }
653
654 #[test]
657 fn test_empty_input() {
658 let err = process_err(make_input(""));
659 assert!(err.to_string().contains("empty"));
660 }
661
662 #[test]
663 fn test_whitespace_only_input() {
664 let err = process_err(make_input(" \n\n "));
665 assert!(err.to_string().contains("empty"));
666 }
667
668 #[test]
669 fn test_non_utf8_input() {
670 let err = process_err(make_input_bytes(vec![0xFF, 0xFE, 0x00, 0x41]));
671 assert!(err.to_string().contains("UTF-8"));
672 }
673
674 #[test]
677 fn test_output_filename_csv_to_json() {
678 assert_eq!(generate_output_filename("data.csv"), "data.json");
679 }
680
681 #[test]
682 fn test_output_filename_no_extension() {
683 assert_eq!(generate_output_filename("data"), "data.json");
684 }
685
686 #[test]
687 fn test_output_filename_multiple_dots() {
688 assert_eq!(generate_output_filename("my.data.csv"), "my.data.json");
689 }
690
691 #[test]
692 fn test_output_file_has_json_extension() {
693 let csv = "name\nAlice\n";
694 let output = process_ok(make_input(csv));
695 assert_eq!(output.files[0].filename, "test.json");
696 }
697
698 #[test]
699 fn test_output_mime_type() {
700 let csv = "name\nAlice\n";
701 let output = process_ok(make_input(csv));
702 assert_eq!(output.files[0].mime_type, "application/json");
703 }
704
705 #[test]
708 fn test_metadata_fields() {
709 let csv = "name,age,city\nAlice,30,NYC\nBob,25,LA\n";
710 let output = process_ok(make_input(csv));
711
712 assert_eq!(output.metadata["columnCount"], 3);
713 assert_eq!(output.metadata["rowCount"], 2);
714 assert!(output.metadata["outputSize"].as_u64().unwrap() > 0);
715 }
716
717 #[test]
720 fn test_fixture_simple() {
721 let data = include_bytes!("../../../../test-fixtures/csv/simple.csv");
722 let output = process_ok(make_input_bytes(data.to_vec()));
723 let json = output_json(&output);
724 let arr = json.as_array().unwrap();
725
726 assert_eq!(arr.len(), 5);
727 assert_eq!(arr[0]["name"], "Alice");
728 }
729
730 #[test]
731 fn test_fixture_crlf() {
732 let data = include_bytes!("../../../../test-fixtures/csv/crlf.csv");
733 let output = process_ok(make_input_bytes(data.to_vec()));
734 let json = output_json(&output);
735
736 assert_eq!(json.as_array().unwrap().len(), 3);
737 assert_eq!(json[0]["name"], "Alice");
738 }
739
740 #[test]
741 fn test_fixture_bom() {
742 let data = include_bytes!("../../../../test-fixtures/csv/bom.csv");
743 let output = process_ok(make_input_bytes(data.to_vec()));
744 let json = output_json(&output);
745
746 assert_eq!(json[0]["name"], "Alice");
748 }
749
750 #[test]
751 fn test_fixture_unicode() {
752 let data = include_bytes!("../../../../test-fixtures/csv/unicode.csv");
753 let output = process_ok(make_input_bytes(data.to_vec()));
754 let json = output_json(&output);
755
756 assert_eq!(json[0]["name"], "Müller");
757 assert_eq!(json[1]["name"], "田中太郎");
758 assert!(json[3]["name"].as_str().unwrap().contains('🎉'));
759 }
760
761 #[test]
762 fn test_fixture_quoted_fields() {
763 let data = include_bytes!("../../../../test-fixtures/csv/quoted-fields.csv");
764 let output = process_ok(make_input_bytes(data.to_vec()));
765 let json = output_json(&output);
766
767 assert_eq!(json[0]["bio"], "Loves coding, hiking");
768 assert!(json[2]["bio"].as_str().unwrap().contains('\n'));
769 }
770
771 #[test]
772 fn test_fixture_semicolon() {
773 let data = include_bytes!("../../../../test-fixtures/csv/semicolon.csv");
774 let mut params = serde_json::Map::new();
775 params.insert("delimiter".into(), serde_json::json!("semicolon"));
776 let input = make_input_with_params(std::str::from_utf8(data).unwrap(), params);
777 let output = process_ok(input);
778 let json = output_json(&output);
779
780 assert_eq!(json[0]["name"], "Alice");
781 assert_eq!(json[0]["age"], "30");
782 }
783
784 #[test]
785 fn test_fixture_empty_cells() {
786 let data = include_bytes!("../../../../test-fixtures/csv/empty-cells.csv");
787 let output = process_ok(make_input_bytes(data.to_vec()));
788 let json = output_json(&output);
789
790 assert_eq!(json[0]["city"], "");
792 assert_eq!(json[1]["age"], "");
794 }
795
796 #[test]
797 fn test_fixture_numeric_values_stay_strings() {
798 let data = include_bytes!("../../../../test-fixtures/csv/numeric-values.csv");
799 let output = process_ok(make_input_bytes(data.to_vec()));
800 let json = output_json(&output);
801
802 assert!(json[0]["price"].is_string());
804 assert!(json[0]["active"].is_string());
805 assert_eq!(json[0]["price"], "19.99");
806 assert_eq!(json[0]["active"], "true");
807 }
808
809 #[test]
812 fn test_large_csv_1000_rows() {
813 let mut csv = String::from("id,name,value\n");
814 for i in 0..1000 {
815 csv.push_str(&format!("{i},item_{i},{}\n", i * 10));
816 }
817 let output = process_ok(make_input(&csv));
818 let json = output_json(&output);
819
820 assert_eq!(json.as_array().unwrap().len(), 1000);
821 assert_eq!(output.metadata["rowCount"], 1000);
822 assert_eq!(output.metadata["columnCount"], 3);
823 }
824
825 #[test]
828 fn test_resolve_delimiter_names() {
829 assert_eq!(resolve_delimiter("comma"), b',');
830 assert_eq!(resolve_delimiter("semicolon"), b';');
831 assert_eq!(resolve_delimiter("tab"), b'\t');
832 assert_eq!(resolve_delimiter("pipe"), b'|');
833 }
834
835 #[test]
836 fn test_resolve_delimiter_literals() {
837 assert_eq!(resolve_delimiter(","), b',');
838 assert_eq!(resolve_delimiter(";"), b';');
839 assert_eq!(resolve_delimiter("|"), b'|');
840 }
841
842 #[test]
843 fn test_resolve_delimiter_unknown_defaults_to_comma() {
844 assert_eq!(resolve_delimiter("unknown"), b',');
845 assert_eq!(resolve_delimiter(""), b',');
846 }
847}