Skip to main content

reliakit_csv/
error.rs

1//! Error types for reading, writing, and typed decoding of CSV.
2
3use core::fmt;
4
5/// A resource limit that was exceeded while reading.
6///
7/// `#[non_exhaustive]`: new limit kinds may be added in a future release.
8#[non_exhaustive]
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum CsvLimitKind {
11    /// `max_input_bytes` exceeded.
12    InputBytes,
13    /// `max_records` exceeded.
14    Records,
15    /// `max_fields_per_record` exceeded for a single record.
16    FieldsPerRecord,
17    /// `max_field_bytes` exceeded for a single field.
18    FieldBytes,
19}
20
21impl CsvLimitKind {
22    /// A short, stable description of the limit.
23    pub const fn as_str(self) -> &'static str {
24        match self {
25            Self::InputBytes => "input bytes",
26            Self::Records => "records",
27            Self::FieldsPerRecord => "fields per record",
28            Self::FieldBytes => "field bytes",
29        }
30    }
31}
32
33/// The category of a CSV reading failure.
34///
35/// This is a stable, machine-readable classification: match on it for
36/// programmatic handling rather than on [`Display`](fmt::Display) text.
37///
38/// `#[non_exhaustive]`: new kinds may be added in a future release, so match
39/// with a wildcard arm.
40#[non_exhaustive]
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum CsvErrorKind {
43    /// A carriage return (`\r`) that was not immediately followed by a line
44    /// feed (`\n`). Only `\n` and `\r\n` terminate a record.
45    BareCarriageReturn,
46    /// A double quote (`"`) appeared inside an unquoted field.
47    QuoteInUnquotedField,
48    /// A quoted field began with `"` but the closing quote was never found.
49    UnterminatedQuotedField,
50    /// Characters other than a delimiter or record terminator followed the
51    /// closing quote of a quoted field.
52    TextAfterQuotedField,
53    /// A record had a different number of fields than the first record. CSV
54    /// read by this crate is rectangular.
55    FieldCountMismatch {
56        /// The field count established by the first record.
57        expected: usize,
58        /// The field count of the offending record.
59        found: usize,
60    },
61    /// A configured [`CsvLimits`](crate::CsvLimits) value was exceeded.
62    LimitExceeded(CsvLimitKind),
63}
64
65/// An error produced while reading CSV.
66///
67/// Carries a stable [`kind`](Self::kind), the byte `offset`, 1-based `line` and
68/// `column`, and the 0-based `record` and `field` index being read.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct CsvError {
71    kind: CsvErrorKind,
72    offset: usize,
73    line: usize,
74    column: usize,
75    record: usize,
76    field: usize,
77}
78
79impl CsvError {
80    pub(crate) const fn new(
81        kind: CsvErrorKind,
82        offset: usize,
83        line: usize,
84        column: usize,
85        record: usize,
86        field: usize,
87    ) -> Self {
88        Self {
89            kind,
90            offset,
91            line,
92            column,
93            record,
94            field,
95        }
96    }
97
98    /// The stable error category.
99    pub const fn kind(&self) -> CsvErrorKind {
100        self.kind
101    }
102
103    /// The byte offset of the error in the input.
104    pub const fn offset(&self) -> usize {
105        self.offset
106    }
107
108    /// The 1-based line of the error.
109    pub const fn line(&self) -> usize {
110        self.line
111    }
112
113    /// The 1-based column of the error.
114    pub const fn column(&self) -> usize {
115        self.column
116    }
117
118    /// The 0-based index of the record being read.
119    pub const fn record(&self) -> usize {
120        self.record
121    }
122
123    /// The 0-based index of the field within the record being read.
124    pub const fn field(&self) -> usize {
125        self.field
126    }
127}
128
129impl fmt::Display for CsvError {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match self.kind {
132            CsvErrorKind::BareCarriageReturn => {
133                f.write_str("carriage return not followed by line feed")?
134            }
135            CsvErrorKind::QuoteInUnquotedField => f.write_str("quote inside an unquoted field")?,
136            CsvErrorKind::UnterminatedQuotedField => f.write_str("unterminated quoted field")?,
137            CsvErrorKind::TextAfterQuotedField => {
138                f.write_str("unexpected text after a quoted field")?
139            }
140            CsvErrorKind::FieldCountMismatch { expected, found } => write!(
141                f,
142                "record has {found} field(s) but {expected} were expected"
143            )?,
144            CsvErrorKind::LimitExceeded(limit) => write!(f, "limit exceeded: {}", limit.as_str())?,
145        }
146        write!(
147            f,
148            " at byte {}, line {}, column {} (record {}, field {})",
149            self.offset, self.line, self.column, self.record, self.field
150        )
151    }
152}
153
154#[cfg(feature = "std")]
155impl std::error::Error for CsvError {}
156
157/// The kind of a typed-CSV decoding error.
158///
159/// `#[non_exhaustive]`: new kinds may be added in a future release.
160#[non_exhaustive]
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162pub enum CsvDecodeErrorKind {
163    /// A record had a different number of fields than the target expected.
164    FieldCount,
165    /// A field could not be parsed into the target type.
166    Field,
167    /// The header row did not match the target's [`header`](crate::CsvEncode::header).
168    HeaderMismatch,
169}
170
171/// An error from decoding a CSV record into a typed value.
172///
173/// Carries a stable [`kind`](Self::kind), a human-readable message, and the
174/// 0-based `record`/`field` indices when known.
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub struct CsvDecodeError {
177    kind: CsvDecodeErrorKind,
178    message: &'static str,
179    record: Option<usize>,
180    field: Option<usize>,
181}
182
183impl CsvDecodeError {
184    /// Creates a decode error with a stable kind and an actionable message.
185    pub const fn new(kind: CsvDecodeErrorKind, message: &'static str) -> Self {
186        Self {
187            kind,
188            message,
189            record: None,
190            field: None,
191        }
192    }
193
194    /// A record had the wrong number of fields for the target type. The
195    /// offending location is reported through the record/field indices rather
196    /// than the message, which stays a stable `&'static str`.
197    pub const fn field_count() -> Self {
198        Self::new(
199            CsvDecodeErrorKind::FieldCount,
200            "record has the wrong number of fields for the target type",
201        )
202    }
203
204    /// A field could not be parsed into the target type.
205    pub const fn field(message: &'static str) -> Self {
206        Self::new(CsvDecodeErrorKind::Field, message)
207    }
208
209    /// The header row did not match the target's expected header.
210    pub const fn header_mismatch() -> Self {
211        Self::new(
212            CsvDecodeErrorKind::HeaderMismatch,
213            "header row does not match the target type's header",
214        )
215    }
216
217    /// Returns the stable error category.
218    pub const fn kind(&self) -> CsvDecodeErrorKind {
219        self.kind
220    }
221
222    /// Returns a human-readable message.
223    pub const fn message(&self) -> &'static str {
224        self.message
225    }
226
227    /// The 0-based record index where the error occurred, if known.
228    pub const fn record(&self) -> Option<usize> {
229        self.record
230    }
231
232    /// The 0-based field index where the error occurred, if known.
233    pub const fn field_index(&self) -> Option<usize> {
234        self.field
235    }
236
237    /// Attaches the 0-based record index to this error.
238    pub const fn at_record(mut self, record: usize) -> Self {
239        self.record = Some(record);
240        self
241    }
242
243    /// Attaches the 0-based field index to this error.
244    pub const fn at_field(mut self, field: usize) -> Self {
245        self.field = Some(field);
246        self
247    }
248}
249
250impl fmt::Display for CsvDecodeError {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        f.write_str(self.message)?;
253        match (self.record, self.field) {
254            (Some(record), Some(field)) => write!(f, " (record {record}, field {field})"),
255            (Some(record), None) => write!(f, " (record {record})"),
256            (None, Some(field)) => write!(f, " (field {field})"),
257            (None, None) => Ok(()),
258        }
259    }
260}
261
262#[cfg(feature = "std")]
263impl std::error::Error for CsvDecodeError {}
264
265/// The error type of [`from_csv_str`](crate::from_csv_str): either the input
266/// was not valid CSV, or a record did not match the target type.
267///
268/// `#[non_exhaustive]`: new variants may be added in a future release.
269#[non_exhaustive]
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum CsvFromStrError {
272    /// The input was not valid CSV.
273    Parse(CsvError),
274    /// The CSV parsed but a record did not match the target type.
275    Decode(CsvDecodeError),
276}
277
278impl fmt::Display for CsvFromStrError {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        match self {
281            Self::Parse(error) => write!(f, "invalid CSV: {error}"),
282            Self::Decode(error) => write!(f, "CSV did not match the target type: {error}"),
283        }
284    }
285}
286
287impl From<CsvError> for CsvFromStrError {
288    fn from(error: CsvError) -> Self {
289        Self::Parse(error)
290    }
291}
292
293impl From<CsvDecodeError> for CsvFromStrError {
294    fn from(error: CsvDecodeError) -> Self {
295        Self::Decode(error)
296    }
297}
298
299#[cfg(feature = "std")]
300impl std::error::Error for CsvFromStrError {}