Skip to main content

reliakit_csv/
record.rs

1//! Typed encoding and decoding of whole records.
2
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use crate::error::{CsvDecodeError, CsvFromStrError};
7use crate::limits::CsvLimits;
8use crate::reader::read_str_with_limits;
9use crate::writer::CsvWriter;
10
11/// A record type that can be written as a CSV row.
12///
13/// Implementors describe a fixed column [`header`](Self::header) and append
14/// their fields, in the same order, in [`encode_fields`](Self::encode_fields).
15/// The companion [`CsvDecode`] reads the row back.
16pub trait CsvEncode {
17    /// The column headers, in field order.
18    fn header() -> Vec<&'static str>;
19
20    /// Appends this record's fields, in column order, to `out`.
21    fn encode_fields(&self, out: &mut Vec<String>);
22}
23
24/// A record type that can be read from a CSV row.
25pub trait CsvDecode: Sized {
26    /// Decodes a record from its fields, or returns a [`CsvDecodeError`].
27    ///
28    /// Implementors should reject a wrong field count with
29    /// [`CsvDecodeError::field_count`].
30    fn decode_fields(fields: &[&str]) -> Result<Self, CsvDecodeError>;
31}
32
33/// Encodes a slice of records to CSV text, with a leading header row.
34///
35/// The header comes from [`CsvEncode::header`]; each record follows in order.
36/// Output is deterministic (see [`CsvWriter`]).
37pub fn to_csv_string<T: CsvEncode>(records: &[T]) -> String {
38    let mut writer = CsvWriter::new();
39    writer.write_record(T::header());
40    let mut fields: Vec<String> = Vec::new();
41    for record in records {
42        fields.clear();
43        record.encode_fields(&mut fields);
44        writer.write_record(&fields);
45    }
46    writer.into_string()
47}
48
49/// Encodes a slice of records to CSV text with no header row.
50pub fn to_csv_string_headerless<T: CsvEncode>(records: &[T]) -> String {
51    let mut writer = CsvWriter::new();
52    let mut fields: Vec<String> = Vec::new();
53    for record in records {
54        fields.clear();
55        record.encode_fields(&mut fields);
56        writer.write_record(&fields);
57    }
58    writer.into_string()
59}
60
61/// Reads CSV text into typed records, validating the header row.
62///
63/// The first record must equal [`CsvEncode::header`] exactly (same fields, same
64/// order); the rest are decoded with [`CsvDecode::decode_fields`]. Uses
65/// conservative [`CsvLimits`].
66///
67/// An empty input yields no records. An input whose only record is the header
68/// also yields no records.
69pub fn from_csv_str<T: CsvEncode + CsvDecode>(input: &str) -> Result<Vec<T>, CsvFromStrError> {
70    from_csv_str_with_limits(input, &CsvLimits::conservative())
71}
72
73/// Reads CSV text into typed records, validating the header, with explicit limits.
74pub fn from_csv_str_with_limits<T: CsvEncode + CsvDecode>(
75    input: &str,
76    limits: &CsvLimits,
77) -> Result<Vec<T>, CsvFromStrError> {
78    let records = read_str_with_limits(input, limits)?;
79    let mut rows = records.into_iter();
80
81    match rows.next() {
82        None => Ok(Vec::new()),
83        Some(header) => {
84            let expected = T::header();
85            if header.len() != expected.len()
86                || header.iter().zip(expected.iter()).any(|(a, b)| a != b)
87            {
88                return Err(CsvDecodeError::header_mismatch().at_record(0).into());
89            }
90            decode_rows::<T>(rows, 1)
91        }
92    }
93}
94
95/// Reads headerless CSV text into typed records, decoding every record.
96pub fn from_csv_str_headerless<T: CsvDecode>(input: &str) -> Result<Vec<T>, CsvFromStrError> {
97    from_csv_str_headerless_with_limits(input, &CsvLimits::conservative())
98}
99
100/// Reads headerless CSV text into typed records, with explicit limits.
101pub fn from_csv_str_headerless_with_limits<T: CsvDecode>(
102    input: &str,
103    limits: &CsvLimits,
104) -> Result<Vec<T>, CsvFromStrError> {
105    let records = read_str_with_limits(input, limits)?;
106    decode_rows::<T>(records.into_iter(), 0)
107}
108
109/// Decodes an iterator of string records into typed values, numbering record
110/// errors from `base_record`.
111fn decode_rows<T: CsvDecode>(
112    rows: impl Iterator<Item = Vec<String>>,
113    base_record: usize,
114) -> Result<Vec<T>, CsvFromStrError> {
115    let mut out = Vec::new();
116    for (offset, record) in rows.enumerate() {
117        let refs: Vec<&str> = record.iter().map(String::as_str).collect();
118        let value =
119            T::decode_fields(&refs).map_err(|error| error.at_record(base_record + offset))?;
120        out.push(value);
121    }
122    Ok(out)
123}