1use std::collections::BTreeMap;
14use std::io::{BufRead, Write};
15
16use lora_executor::{LoraValue, Row};
17use serde_json::Value as J;
18
19use super::format::{
20 invalid_data, row_parse_io_error, RowDecoder, RowEncoder, RowParseError, StreamingRowDecoder,
21};
22use super::value_json::{lora_value_from_json, lora_value_to_json};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum CsvType {
27 Auto,
30 String,
31 Int,
32 Long,
33 Float,
34 Double,
35 Bool,
36 Date,
37 DateTime,
38 LocalDateTime,
39 Time,
40 LocalTime,
41 Duration,
42 Point,
43 Json,
44 Array(Box<CsvType>),
47 SchemaLabel,
53 SchemaId,
54 SchemaStartId,
55 SchemaEndId,
56 SchemaType,
57}
58
59#[derive(Debug, Clone)]
60pub struct CsvHeader {
61 pub name: String,
63 pub ty: CsvType,
65}
66
67impl CsvHeader {
68 pub fn parse(raw: &str) -> Result<Self, String> {
70 let trimmed = raw.trim();
71 if let Some(rest) = trimmed.strip_prefix(':') {
73 let ty = match rest.to_ascii_uppercase().as_str() {
74 "LABEL" => CsvType::SchemaLabel,
75 "ID" => CsvType::SchemaId,
76 "START_ID" => CsvType::SchemaStartId,
77 "END_ID" => CsvType::SchemaEndId,
78 "TYPE" => CsvType::SchemaType,
79 other => return Err(format!("unknown schema marker `:{other}`")),
80 };
81 return Ok(Self {
82 name: String::new(),
83 ty,
84 });
85 }
86
87 match trimmed.split_once(':') {
88 None => Ok(Self {
89 name: trimmed.to_string(),
90 ty: CsvType::Auto,
91 }),
92 Some((name, ty_part)) => Ok(Self {
93 name: name.trim().to_string(),
94 ty: parse_type(ty_part.trim())?,
95 }),
96 }
97 }
98}
99
100fn parse_type(tag: &str) -> Result<CsvType, String> {
101 let (base, is_array) = match tag.strip_suffix("[]") {
102 Some(b) => (b.trim(), true),
103 None => (tag, false),
104 };
105 let base_ty = match base.to_ascii_lowercase().as_str() {
106 "string" | "text" => CsvType::String,
107 "int" | "integer" => CsvType::Int,
108 "long" => CsvType::Long,
109 "float" => CsvType::Float,
110 "double" => CsvType::Double,
111 "bool" | "boolean" => CsvType::Bool,
112 "date" => CsvType::Date,
113 "datetime" => CsvType::DateTime,
114 "localdatetime" => CsvType::LocalDateTime,
115 "time" => CsvType::Time,
116 "localtime" => CsvType::LocalTime,
117 "duration" => CsvType::Duration,
118 "point" => CsvType::Point,
119 "json" => CsvType::Json,
120 other => return Err(format!("unknown column type `{other}`")),
121 };
122 Ok(if is_array {
123 CsvType::Array(Box::new(base_ty))
124 } else {
125 base_ty
126 })
127}
128
129pub struct CsvEncoder<W: Write> {
130 writer: W,
131 header_written: bool,
132 columns: Vec<String>,
133 typed_headers: Option<Vec<String>>,
136}
137
138impl<W: Write> CsvEncoder<W> {
139 pub fn new(writer: W) -> Self {
140 Self {
141 writer,
142 header_written: false,
143 columns: Vec::new(),
144 typed_headers: None,
145 }
146 }
147
148 pub fn with_typed_headers(mut self, headers: Vec<String>) -> Self {
151 self.typed_headers = Some(headers);
152 self
153 }
154
155 pub fn into_inner(self) -> W {
156 self.writer
157 }
158}
159
160impl<W: Write> RowEncoder for CsvEncoder<W> {
161 fn begin(&mut self, columns: &[String]) -> std::io::Result<()> {
162 if self.header_written {
163 return Ok(());
164 }
165 let header_cells: Vec<&str> = if let Some(typed) = &self.typed_headers {
166 typed.iter().map(|s| s.as_str()).collect()
167 } else {
168 columns.iter().map(|s| s.as_str()).collect()
169 };
170 write_record(&mut self.writer, header_cells.iter().copied())?;
171 self.columns = columns.to_vec();
172 self.header_written = true;
173 Ok(())
174 }
175
176 fn write_row(&mut self, row: &Row) -> std::io::Result<()> {
177 let mut indexed: BTreeMap<String, &LoraValue> = BTreeMap::new();
178 for (_, name, value) in row.iter_named() {
179 indexed.insert(name.into_owned(), value);
180 }
181 let mut cells = Vec::with_capacity(self.columns.len());
182 for col in &self.columns {
183 let cell = match indexed.get(col.as_str()) {
184 Some(v) => encode_cell(v),
185 None => encode_cell(&LoraValue::Null),
186 };
187 cells.push(cell);
188 }
189 write_record(&mut self.writer, cells.iter().map(|s| s.as_str()))
190 }
191
192 fn write_named_row(&mut self, columns: &[(String, LoraValue)]) -> std::io::Result<()> {
193 if !self.header_written {
194 let header_columns: Vec<String> =
195 columns.iter().map(|(name, _)| name.clone()).collect();
196 self.begin(&header_columns)?;
197 }
198 debug_assert!(
207 columns
208 .iter()
209 .all(|(k, _)| self.columns.iter().any(|c| c == k)),
210 "row has keys not in the encoder's header: {:?} (header: {:?})",
211 columns.iter().map(|(k, _)| k).collect::<Vec<_>>(),
212 self.columns,
213 );
214 let lookup: BTreeMap<&str, &LoraValue> =
215 columns.iter().map(|(k, v)| (k.as_str(), v)).collect();
216 let mut cells = Vec::with_capacity(self.columns.len());
217 for col in &self.columns {
218 let v = lookup
219 .get(col.as_str())
220 .copied()
221 .cloned()
222 .unwrap_or(LoraValue::Null);
223 cells.push(encode_cell(&v));
224 }
225 write_record(&mut self.writer, cells.iter().map(|s| s.as_str()))
226 }
227
228 fn finish(&mut self) -> std::io::Result<()> {
229 self.writer.flush()
230 }
231}
232
233fn encode_cell(value: &LoraValue) -> String {
234 match value {
235 LoraValue::Null => String::new(),
236 LoraValue::Bool(b) => b.to_string(),
237 LoraValue::Int(i) => i.to_string(),
238 LoraValue::Float(f) => f.to_string(),
239 LoraValue::String(s) => s.clone(),
240 LoraValue::List(items) => {
241 if items.iter().all(is_scalar) && items.iter().all(list_element_safe_for_semicolon) {
247 items
248 .iter()
249 .map(encode_cell_scalar_only)
250 .collect::<Vec<_>>()
251 .join(";")
252 } else {
253 serde_json::to_string(&lora_value_to_json(value)).unwrap_or_default()
254 }
255 }
256 _ => serde_json::to_string(&lora_value_to_json(value)).unwrap_or_default(),
258 }
259}
260
261fn is_scalar(v: &LoraValue) -> bool {
262 matches!(
263 v,
264 LoraValue::Null
265 | LoraValue::Bool(_)
266 | LoraValue::Int(_)
267 | LoraValue::Float(_)
268 | LoraValue::String(_)
269 )
270}
271
272fn list_element_safe_for_semicolon(v: &LoraValue) -> bool {
277 match v {
278 LoraValue::String(s) => !s.contains([';', '"', '\n', '\r']),
279 _ => true,
280 }
281}
282
283fn encode_cell_scalar_only(v: &LoraValue) -> String {
284 match v {
285 LoraValue::Null => String::new(),
286 LoraValue::Bool(b) => b.to_string(),
287 LoraValue::Int(i) => i.to_string(),
288 LoraValue::Float(f) => f.to_string(),
289 LoraValue::String(s) => s.clone(),
290 _ => String::new(),
291 }
292}
293
294fn write_record<'a, I, W>(writer: &mut W, cells: I) -> std::io::Result<()>
295where
296 I: IntoIterator<Item = &'a str>,
297 W: Write,
298{
299 let mut first = true;
300 for cell in cells {
301 if !first {
302 writer.write_all(b",")?;
303 }
304 first = false;
305 write_cell(writer, cell)?;
306 }
307 writer.write_all(b"\n")
308}
309
310fn write_cell<W: Write>(writer: &mut W, value: &str) -> std::io::Result<()> {
311 let needs_quote = value
312 .chars()
313 .any(|c| c == ',' || c == '"' || c == '\n' || c == '\r');
314 if !needs_quote {
315 writer.write_all(value.as_bytes())
316 } else {
317 writer.write_all(b"\"")?;
318 for ch in value.chars() {
319 if ch == '"' {
320 writer.write_all(b"\"\"")?;
321 } else {
322 let mut buf = [0u8; 4];
323 writer.write_all(ch.encode_utf8(&mut buf).as_bytes())?;
324 }
325 }
326 writer.write_all(b"\"")
327 }
328}
329
330pub struct CsvDecoder<R: BufRead> {
331 reader: R,
332 headers: Option<Vec<CsvHeader>>,
333 column_names: Vec<String>,
336}
337
338impl<R: BufRead> CsvDecoder<R> {
339 pub fn new(reader: R) -> Self {
340 Self {
341 reader,
342 headers: None,
343 column_names: Vec::new(),
344 }
345 }
346
347 pub fn parsed_headers(&self) -> Option<&[CsvHeader]> {
350 self.headers.as_deref()
351 }
352
353 fn ensure_header(&mut self) -> std::io::Result<()> {
354 if self.headers.is_some() {
355 return Ok(());
356 }
357 let mut cells = match read_record(&mut self.reader)? {
358 Some(c) => c,
359 None => {
360 self.headers = Some(Vec::new());
361 return Ok(());
362 }
363 };
364 if let Some(first) = cells.first_mut() {
365 strip_utf8_bom(first);
366 }
367 let mut headers = Vec::with_capacity(cells.len());
368 let mut column_names = Vec::with_capacity(cells.len());
369 for (idx, raw) in cells.iter().enumerate() {
370 let h = CsvHeader::parse(raw).map_err(invalid_data)?;
371 column_names.push(synthetic_name(&h, idx));
372 headers.push(h);
373 }
374 self.headers = Some(headers);
375 self.column_names = column_names;
376 Ok(())
377 }
378}
379
380pub(crate) fn strip_utf8_bom(s: &mut String) {
386 if let Some(rest) = s.strip_prefix('\u{feff}') {
387 *s = rest.to_string();
388 }
389}
390
391pub(crate) fn synthetic_name(h: &CsvHeader, idx: usize) -> String {
392 if !h.name.is_empty() {
393 return h.name.clone();
394 }
395 match h.ty {
396 CsvType::SchemaLabel => "_label".to_string(),
397 CsvType::SchemaId => "_id".to_string(),
398 CsvType::SchemaStartId => "_start_id".to_string(),
399 CsvType::SchemaEndId => "_end_id".to_string(),
400 CsvType::SchemaType => "_type".to_string(),
401 _ => format!("col_{idx}"),
402 }
403}
404
405impl<R: BufRead> RowDecoder for CsvDecoder<R> {
406 fn header(&mut self) -> std::io::Result<Option<Vec<String>>> {
407 self.ensure_header()?;
408 Ok(Some(self.column_names.clone()))
409 }
410
411 fn next_row(&mut self) -> std::io::Result<Option<Vec<(String, LoraValue)>>> {
412 self.ensure_header()?;
413 let Some(cells) = read_record(&mut self.reader)? else {
414 return Ok(None);
415 };
416 let headers = self.headers.as_ref().unwrap();
417 if cells.len() != headers.len() {
418 return Err(invalid_data(format!(
419 "row has {} cells; expected {} (one per header)",
420 cells.len(),
421 headers.len()
422 )));
423 }
424 let mut out = Vec::with_capacity(cells.len());
425 for (idx, cell) in cells.into_iter().enumerate() {
426 let h = &headers[idx];
427 let value = parse_cell(&cell, &h.ty).map_err(invalid_data)?;
428 out.push((self.column_names[idx].clone(), value));
429 }
430 Ok(Some(out))
431 }
432}
433
434pub(crate) fn parse_cell(raw: &str, ty: &CsvType) -> Result<LoraValue, String> {
435 if raw.is_empty() {
436 return Ok(LoraValue::Null);
437 }
438 match ty {
439 CsvType::Auto | CsvType::String | CsvType::SchemaLabel | CsvType::SchemaType => {
440 Ok(LoraValue::String(raw.to_string()))
441 }
442 CsvType::Int
443 | CsvType::Long
444 | CsvType::SchemaId
445 | CsvType::SchemaStartId
446 | CsvType::SchemaEndId => raw
447 .parse::<i64>()
448 .map(LoraValue::Int)
449 .map_err(|e| format!("invalid integer `{raw}`: {e}")),
450 CsvType::Float | CsvType::Double => raw
451 .parse::<f64>()
452 .map(LoraValue::Float)
453 .map_err(|e| format!("invalid float `{raw}`: {e}")),
454 CsvType::Bool => match raw.to_ascii_lowercase().as_str() {
455 "true" | "t" | "1" | "yes" => Ok(LoraValue::Bool(true)),
456 "false" | "f" | "0" | "no" => Ok(LoraValue::Bool(false)),
457 other => Err(format!("invalid boolean `{other}`")),
458 },
459 CsvType::Date
460 | CsvType::DateTime
461 | CsvType::LocalDateTime
462 | CsvType::Time
463 | CsvType::LocalTime
464 | CsvType::Duration
465 | CsvType::Point => {
466 if raw.trim_start().starts_with('{') {
469 let v: J = serde_json::from_str(raw).map_err(|e| e.to_string())?;
470 lora_value_from_json(v)
471 } else {
472 let tag = match ty {
473 CsvType::Date => "date",
474 CsvType::DateTime => "datetime",
475 CsvType::LocalDateTime => "localdatetime",
476 CsvType::Time => "time",
477 CsvType::LocalTime => "localtime",
478 CsvType::Duration => "duration",
479 CsvType::Point => {
480 return Err("point cells must be JSON-encoded".into());
481 }
482 _ => unreachable!(),
483 };
484 let json = serde_json::json!({ "kind": tag, "iso": raw });
485 lora_value_from_json(json)
486 }
487 }
488 CsvType::Json => {
489 let v: J = serde_json::from_str(raw).map_err(|e| e.to_string())?;
490 lora_value_from_json(v)
491 }
492 CsvType::Array(inner) => {
493 if raw.trim_start().starts_with('[') {
495 let v: J = serde_json::from_str(raw).map_err(|e| e.to_string())?;
496 lora_value_from_json(v)
497 } else {
498 let mut items = Vec::new();
499 for part in raw.split(';') {
500 items.push(parse_cell(part, inner)?);
501 }
502 Ok(LoraValue::List(items))
503 }
504 }
505 }
506}
507
508fn read_record<R: BufRead>(reader: &mut R) -> std::io::Result<Option<Vec<String>>> {
510 let mut cells: Vec<Vec<u8>> = Vec::new();
511 let mut current: Vec<u8> = Vec::new();
512 let mut in_quotes = false;
513 let mut started = false;
514
515 loop {
516 let buf = reader.fill_buf()?;
517 if buf.is_empty() {
518 if !started && current.is_empty() && cells.is_empty() {
519 return Ok(None);
520 }
521 cells.push(std::mem::take(&mut current));
522 return decode_record(cells).map(Some);
523 }
524 let consumed = process_buf(buf, &mut cells, &mut current, &mut in_quotes, &mut started);
525 let (n, end_of_record) = consumed;
526 reader.consume(n);
527 if end_of_record {
528 cells.push(std::mem::take(&mut current));
529 return decode_record(cells).map(Some);
530 }
531 }
532}
533
534fn decode_record(cells: Vec<Vec<u8>>) -> std::io::Result<Vec<String>> {
535 cells
536 .into_iter()
537 .map(|cell| String::from_utf8(cell).map_err(invalid_data))
538 .collect()
539}
540
541pub(crate) fn process_buf(
548 buf: &[u8],
549 cells: &mut Vec<Vec<u8>>,
550 current: &mut Vec<u8>,
551 in_quotes: &mut bool,
552 started: &mut bool,
553) -> (usize, bool) {
554 let mut i = 0;
555 while i < buf.len() {
556 let b = buf[i];
557 *started = true;
558 if *in_quotes {
559 match b {
560 b'"' => {
561 if i + 1 < buf.len() && buf[i + 1] == b'"' {
562 current.push(b'"');
563 i += 2;
564 } else {
565 *in_quotes = false;
566 i += 1;
567 }
568 }
569 _ => {
570 current.push(b);
571 i += 1;
572 }
573 }
574 } else {
575 match b {
576 b'"' => {
577 *in_quotes = true;
578 i += 1;
579 }
580 b',' => {
581 cells.push(std::mem::take(current));
582 i += 1;
583 }
584 b'\n' => {
585 return (i + 1, true);
586 }
587 b'\r' => {
588 let consumed = if i + 1 < buf.len() && buf[i + 1] == b'\n' {
589 i + 2
590 } else {
591 i + 1
592 };
593 return (consumed, true);
594 }
595 _ => {
596 current.push(b);
597 i += 1;
598 }
599 }
600 }
601 }
602 (i, false)
603}
604
605pub struct StreamingCsvDecoder {
611 chunk_buffer: Vec<u8>,
616 cells: Vec<Vec<u8>>,
618 current: Vec<u8>,
620 in_quotes: bool,
621 started: bool,
622 header: Option<Vec<CsvHeader>>,
625 column_names: Vec<String>,
628 completed: Vec<Vec<(String, LoraValue)>>,
630 bytes_fed: u64,
631 rows_emitted: u64,
632 record_index: u64,
637 permissive: bool,
638 errors: Vec<RowParseError>,
639}
640
641impl Default for StreamingCsvDecoder {
642 fn default() -> Self {
643 Self::new()
644 }
645}
646
647impl StreamingCsvDecoder {
648 pub fn new() -> Self {
649 Self {
650 chunk_buffer: Vec::with_capacity(64 * 1024),
651 cells: Vec::new(),
652 current: Vec::new(),
653 in_quotes: false,
654 started: false,
655 header: None,
656 column_names: Vec::new(),
657 completed: Vec::new(),
658 bytes_fed: 0,
659 rows_emitted: 0,
660 record_index: 0,
661 permissive: false,
662 errors: Vec::new(),
663 }
664 }
665
666 pub fn parsed_headers(&self) -> Option<&[CsvHeader]> {
669 self.header.as_deref()
670 }
671
672 fn advance(&mut self) -> std::io::Result<()> {
674 loop {
678 let (consumed, end_of_record) = process_buf(
679 &self.chunk_buffer,
680 &mut self.cells,
681 &mut self.current,
682 &mut self.in_quotes,
683 &mut self.started,
684 );
685 if consumed > 0 {
686 self.chunk_buffer.drain(..consumed);
687 }
688 if !end_of_record {
689 break;
690 }
691 self.cells.push(std::mem::take(&mut self.current));
694 let record = decode_record(std::mem::take(&mut self.cells))?;
695 self.handle_record(record)?;
696 }
697 Ok(())
698 }
699
700 fn handle_record(&mut self, mut cells: Vec<String>) -> std::io::Result<()> {
701 if self.header.is_none() {
702 if let Some(first) = cells.first_mut() {
706 strip_utf8_bom(first);
707 }
708 let mut headers = Vec::with_capacity(cells.len());
709 let mut column_names = Vec::with_capacity(cells.len());
710 for (idx, raw) in cells.iter().enumerate() {
711 let h = CsvHeader::parse(raw).map_err(invalid_data)?;
712 column_names.push(synthetic_name(&h, idx));
713 headers.push(h);
714 }
715 self.header = Some(headers);
716 self.column_names = column_names;
717 return Ok(());
718 }
719 self.record_index += 1;
720 let header = self.header.as_ref().unwrap();
721 if cells.len() != header.len() {
722 let message = format!(
723 "row has {} cells; expected {} (one per header)",
724 cells.len(),
725 header.len()
726 );
727 return self.report_record_error(None, message, &cells);
728 }
729 let mut out = Vec::with_capacity(cells.len());
730 for (idx, cell) in cells.iter().enumerate() {
731 let h = &header[idx];
732 match parse_cell(cell, &h.ty) {
733 Ok(value) => out.push((self.column_names[idx].clone(), value)),
734 Err(message) => {
735 let column = Some(self.column_names[idx].clone());
736 return self.report_record_error(column, message, &cells);
737 }
738 }
739 }
740 self.completed.push(out);
741 self.rows_emitted += 1;
742 Ok(())
743 }
744
745 fn report_record_error(
746 &mut self,
747 column: Option<String>,
748 message: String,
749 cells: &[String],
750 ) -> std::io::Result<()> {
751 let raw = cells.join(",");
752 let err = RowParseError {
753 row: self.record_index,
754 column,
755 raw_sample: RowParseError::make_sample(&raw),
756 message,
757 };
758 if self.permissive {
759 self.errors.push(err);
760 Ok(())
761 } else {
762 Err(row_parse_io_error(err))
763 }
764 }
765}
766
767impl StreamingRowDecoder for StreamingCsvDecoder {
768 fn feed(&mut self, chunk: &[u8]) -> std::io::Result<()> {
769 if chunk.is_empty() {
770 return Ok(());
771 }
772 self.bytes_fed += chunk.len() as u64;
773 self.chunk_buffer.extend_from_slice(chunk);
774 self.advance()
775 }
776
777 fn drain(&mut self) -> std::io::Result<Vec<Vec<(String, LoraValue)>>> {
778 Ok(std::mem::take(&mut self.completed))
779 }
780
781 fn finish(&mut self) -> std::io::Result<Vec<Vec<(String, LoraValue)>>> {
782 self.advance()?;
784 if self.started && (!self.cells.is_empty() || !self.current.is_empty() || self.in_quotes) {
787 self.cells.push(std::mem::take(&mut self.current));
788 let record = decode_record(std::mem::take(&mut self.cells))?;
789 self.started = false;
790 self.in_quotes = false;
791 self.handle_record(record)?;
792 }
793 Ok(std::mem::take(&mut self.completed))
794 }
795
796 fn header(&self) -> Option<&[String]> {
797 if self.column_names.is_empty() {
798 None
799 } else {
800 Some(&self.column_names)
801 }
802 }
803
804 fn bytes_fed(&self) -> u64 {
805 self.bytes_fed
806 }
807
808 fn rows_emitted(&self) -> u64 {
809 self.rows_emitted
810 }
811
812 fn set_permissive(&mut self, on: bool) {
813 self.permissive = on;
814 }
815
816 fn take_errors(&mut self) -> Vec<RowParseError> {
817 std::mem::take(&mut self.errors)
818 }
819}
820
821#[cfg(test)]
822mod tests {
823 use super::*;
824 use std::io::Cursor;
825
826 #[test]
827 fn round_trip_simple() {
828 let mut buf = Vec::new();
829 {
830 let mut enc = CsvEncoder::new(&mut buf);
831 enc.begin(&["name".into(), "age".into()]).unwrap();
832 enc.write_named_row(&[
833 ("name".into(), LoraValue::String("alice".into())),
834 ("age".into(), LoraValue::Int(30)),
835 ])
836 .unwrap();
837 enc.write_named_row(&[
838 ("name".into(), LoraValue::String("bob".into())),
839 ("age".into(), LoraValue::Int(25)),
840 ])
841 .unwrap();
842 enc.finish().unwrap();
843 }
844 let text = std::str::from_utf8(&buf).unwrap();
845 assert_eq!(text, "name,age\nalice,30\nbob,25\n");
846
847 let mut dec = CsvDecoder::new(Cursor::new(buf));
848 let h = dec.header().unwrap().unwrap();
849 assert_eq!(h, vec!["name".to_string(), "age".to_string()]);
850
851 let r1 = dec.next_row().unwrap().unwrap();
852 assert_eq!(r1[0].1, LoraValue::String("alice".into()));
853 assert_eq!(r1[1].1, LoraValue::String("30".into()));
855 }
856
857 #[test]
858 fn typed_headers_parse_numeric_cells() {
859 let csv = "name:string,age:int\nalice,30\nbob,25\n";
860 let mut dec = CsvDecoder::new(Cursor::new(csv));
861 let h = dec.header().unwrap().unwrap();
862 assert_eq!(h, vec!["name".to_string(), "age".to_string()]);
863 let r = dec.next_row().unwrap().unwrap();
864 assert_eq!(r[0].1, LoraValue::String("alice".into()));
865 assert_eq!(r[1].1, LoraValue::Int(30));
866 }
867
868 #[test]
869 fn schema_markers_parse() {
870 let csv = ":ID,:LABEL,name:string\n1,User,alice\n2,User,bob\n";
871 let mut dec = CsvDecoder::new(Cursor::new(csv));
872 let h = dec.header().unwrap().unwrap();
873 assert_eq!(
874 h,
875 vec!["_id".to_string(), "_label".to_string(), "name".to_string()]
876 );
877 let r = dec.next_row().unwrap().unwrap();
878 assert_eq!(r[0].1, LoraValue::Int(1));
879 assert_eq!(r[1].1, LoraValue::String("User".into()));
880 }
881
882 #[test]
883 fn array_cells_split_on_semicolon() {
884 let csv = "tags:string[]\nfoo;bar;baz\n";
885 let mut dec = CsvDecoder::new(Cursor::new(csv));
886 let _ = dec.header().unwrap();
887 let r = dec.next_row().unwrap().unwrap();
888 let LoraValue::List(items) = &r[0].1 else {
889 panic!("expected list");
890 };
891 assert_eq!(items.len(), 3);
892 assert_eq!(items[0], LoraValue::String("foo".into()));
893 }
894
895 #[test]
896 fn quoted_cells_preserve_commas_and_newlines() {
897 let csv = "name:string,note:string\n\"hi, there\",\"line1\nline2\"\n";
898 let mut dec = CsvDecoder::new(Cursor::new(csv));
899 let _ = dec.header().unwrap();
900 let r = dec.next_row().unwrap().unwrap();
901 assert_eq!(r[0].1, LoraValue::String("hi, there".into()));
902 assert_eq!(r[1].1, LoraValue::String("line1\nline2".into()));
903 }
904
905 #[test]
906 fn quotes_escape_by_doubling() {
907 let csv = "v:string\n\"she said \"\"hi\"\"\"\n";
908 let mut dec = CsvDecoder::new(Cursor::new(csv));
909 let _ = dec.header().unwrap();
910 let r = dec.next_row().unwrap().unwrap();
911 assert_eq!(r[0].1, LoraValue::String("she said \"hi\"".into()));
912 }
913
914 #[test]
915 fn encoder_quotes_when_needed() {
916 let mut buf = Vec::new();
917 {
918 let mut enc = CsvEncoder::new(&mut buf);
919 enc.begin(&["v".into()]).unwrap();
920 enc.write_named_row(&[("v".into(), LoraValue::String("hi, \"there\"\n".into()))])
921 .unwrap();
922 enc.finish().unwrap();
923 }
924 let text = std::str::from_utf8(&buf).unwrap();
925 assert_eq!(text, "v\n\"hi, \"\"there\"\"\n\"\n");
926 }
927
928 #[test]
929 fn json_typed_cells_round_trip_temporal() {
930 let csv = "ts:datetime\n2024-01-15T10:30:00Z\n";
931 let mut dec = CsvDecoder::new(Cursor::new(csv));
932 let _ = dec.header().unwrap();
933 let r = dec.next_row().unwrap().unwrap();
934 match &r[0].1 {
935 LoraValue::DateTime(_) => {}
936 other => panic!("expected DateTime, got {other:?}"),
937 }
938 }
939
940 #[test]
941 fn streaming_csv_split_across_chunks() {
942 let mut dec = StreamingCsvDecoder::new();
944 dec.feed(b"name:s").unwrap();
945 assert!(dec.header().is_none());
946 dec.feed(b"tring,age:int\nalice,30\nbo").unwrap();
947 let rows = dec.drain().unwrap();
948 assert_eq!(rows.len(), 1);
949 assert_eq!(
950 rows[0][0],
951 ("name".into(), LoraValue::String("alice".into()))
952 );
953 assert_eq!(rows[0][1], ("age".into(), LoraValue::Int(30)));
954 dec.feed(b"b,25\n").unwrap();
955 let rows2 = dec.drain().unwrap();
956 assert_eq!(rows2.len(), 1);
957 assert_eq!(
958 rows2[0][0],
959 ("name".into(), LoraValue::String("bob".into()))
960 );
961 assert_eq!(
963 dec.header().unwrap(),
964 &["name".to_string(), "age".to_string()]
965 );
966 dec.feed(b"carol,40").unwrap();
968 assert!(dec.drain().unwrap().is_empty());
969 let final_rows = dec.finish().unwrap();
970 assert_eq!(final_rows.len(), 1);
971 assert_eq!(
972 final_rows[0][0],
973 ("name".into(), LoraValue::String("carol".into()))
974 );
975 assert_eq!(dec.rows_emitted(), 3);
976 }
977
978 #[test]
979 fn streaming_csv_quoted_newline_across_chunks() {
980 let mut dec = StreamingCsvDecoder::new();
985 dec.feed(b"v:string\n\"line").unwrap();
986 assert!(dec.drain().unwrap().is_empty());
987 dec.feed(b"1\nline2\"\n").unwrap();
988 let rows = dec.drain().unwrap();
989 assert_eq!(rows.len(), 1);
990 assert_eq!(
991 rows[0][0],
992 ("v".into(), LoraValue::String("line1\nline2".into()))
993 );
994 }
995
996 #[test]
997 fn streaming_csv_utf8_split_across_chunks() {
998 let mut dec = StreamingCsvDecoder::new();
999 dec.feed(b"name:string\nal").unwrap();
1000 dec.feed(&[0xc3]).unwrap();
1001 dec.feed(&[0xa9, b'\n']).unwrap();
1002 let rows = dec.drain().unwrap();
1003 assert_eq!(rows.len(), 1);
1004 assert_eq!(rows[0][0], ("name".into(), LoraValue::String("alé".into())));
1005 }
1006
1007 #[test]
1008 fn streaming_csv_strict_attributes_failing_cell() {
1009 let mut dec = StreamingCsvDecoder::new();
1010 let err = dec
1011 .feed(b"name:string,age:int\nalice,30\nbob,not-a-number\n")
1012 .unwrap_err();
1013 let parse = super::super::format::downcast_row_parse_error(&err)
1014 .expect("error should carry RowParseError");
1015 assert_eq!(parse.row, 2);
1016 assert_eq!(parse.column.as_deref(), Some("age"));
1017 assert!(parse.message.contains("not-a-number"));
1018 assert!(parse.raw_sample.contains("bob"));
1019 }
1020
1021 #[test]
1022 fn streaming_csv_permissive_skips_bad_rows() {
1023 let mut dec = StreamingCsvDecoder::new();
1024 dec.set_permissive(true);
1025 dec.feed(b"name:string,age:int\nalice,30\nbob,oops\ncarol,40\n")
1026 .unwrap();
1027 let rows = dec.drain().unwrap();
1028 assert_eq!(rows.len(), 2);
1029 assert_eq!(
1030 rows[0][0],
1031 ("name".into(), LoraValue::String("alice".into()))
1032 );
1033 assert_eq!(
1034 rows[1][0],
1035 ("name".into(), LoraValue::String("carol".into()))
1036 );
1037 let errors = dec.take_errors();
1038 assert_eq!(errors.len(), 1);
1039 assert_eq!(errors[0].row, 2);
1040 assert_eq!(errors[0].column.as_deref(), Some("age"));
1041 }
1042
1043 #[test]
1044 fn pull_decoder_strips_utf8_bom_from_first_header() {
1045 let bytes = [0xEF, 0xBB, 0xBF];
1050 let mut csv = String::from_utf8(bytes.to_vec()).unwrap();
1051 csv.push_str("name,age\nalice,30\n");
1052 let mut dec = CsvDecoder::new(Cursor::new(csv));
1053 let h = dec.header().unwrap().unwrap();
1054 assert_eq!(h, vec!["name".to_string(), "age".to_string()]);
1055 let r = dec.next_row().unwrap().unwrap();
1056 assert_eq!(r[0].0, "name");
1057 }
1058
1059 #[test]
1060 fn streaming_csv_strips_utf8_bom_from_first_header() {
1061 let mut dec = StreamingCsvDecoder::new();
1062 dec.feed(&[0xEF, 0xBB]).unwrap();
1066 dec.feed(&[0xBF]).unwrap();
1067 dec.feed(b"name,age\nalice,30\n").unwrap();
1068 let rows = dec.drain().unwrap();
1069 assert_eq!(rows.len(), 1);
1070 assert_eq!(rows[0][0].0, "name");
1071 assert_eq!(
1072 dec.header().unwrap(),
1073 &["name".to_string(), "age".to_string()]
1074 );
1075 }
1076
1077 #[test]
1078 fn encoder_falls_back_to_json_when_list_element_contains_separator() {
1079 let mut buf = Vec::new();
1083 {
1084 let mut enc = CsvEncoder::new(&mut buf);
1085 enc.begin(&["tags".into()]).unwrap();
1086 enc.write_named_row(&[(
1087 "tags".into(),
1088 LoraValue::List(vec![
1089 LoraValue::String("a;b".into()),
1090 LoraValue::String("c".into()),
1091 ]),
1092 )])
1093 .unwrap();
1094 enc.finish().unwrap();
1095 }
1096 let text = std::str::from_utf8(&buf).unwrap();
1097 assert!(
1100 text.contains(r#""[""a;b"",""c""]""#),
1101 "expected JSON-encoded list, got: {text}"
1102 );
1103 }
1104
1105 #[test]
1106 fn encoder_uses_semicolon_join_for_safe_list_elements() {
1107 let mut buf = Vec::new();
1108 {
1109 let mut enc = CsvEncoder::new(&mut buf);
1110 enc.begin(&["tags".into()]).unwrap();
1111 enc.write_named_row(&[(
1112 "tags".into(),
1113 LoraValue::List(vec![
1114 LoraValue::String("a".into()),
1115 LoraValue::String("b".into()),
1116 ]),
1117 )])
1118 .unwrap();
1119 enc.finish().unwrap();
1120 }
1121 let text = std::str::from_utf8(&buf).unwrap();
1122 assert_eq!(text, "tags\na;b\n");
1123 }
1124
1125 #[test]
1126 fn streaming_csv_permissive_handles_cell_count_mismatch() {
1127 let mut dec = StreamingCsvDecoder::new();
1128 dec.set_permissive(true);
1129 dec.feed(b"a:string,b:string\nx,y\nz\nq,r\n").unwrap();
1130 let rows = dec.drain().unwrap();
1131 assert_eq!(rows.len(), 2);
1132 let errors = dec.take_errors();
1133 assert_eq!(errors.len(), 1);
1134 assert_eq!(errors[0].row, 2);
1135 assert!(errors[0].column.is_none());
1136 assert!(errors[0].message.contains("cells"));
1137 }
1138}