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