Skip to main content

reliakit_csv/
field.rs

1//! Encoding and decoding of individual CSV fields.
2
3use alloc::string::{String, ToString};
4
5use crate::error::CsvDecodeError;
6
7/// A scalar value that maps to and from a single CSV field.
8///
9/// Encoding never fails — every supported value has a text form. Decoding is
10/// strict: the field text must parse exactly into the target type.
11///
12/// Implemented for the integer types, `bool` (`"true"`/`"false"`), `String`,
13/// and `Option<T>` (an empty field decodes to `None`).
14pub trait CsvField: Sized {
15    /// Encodes `self` into a field value.
16    fn encode_field(&self) -> String;
17
18    /// Decodes a field value into `Self`, or returns a [`CsvDecodeError`].
19    fn decode_field(input: &str) -> Result<Self, CsvDecodeError>;
20}
21
22macro_rules! impl_int {
23    ($($t:ty),* $(,)?) => {$(
24        impl CsvField for $t {
25            fn encode_field(&self) -> String {
26                self.to_string()
27            }
28            fn decode_field(input: &str) -> Result<Self, CsvDecodeError> {
29                input.parse::<$t>().map_err(|_| {
30                    CsvDecodeError::field("field is not an integer that fits the target type")
31                })
32            }
33        }
34    )*};
35}
36impl_int!(u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize);
37
38impl CsvField for bool {
39    fn encode_field(&self) -> String {
40        if *self { "true" } else { "false" }.to_string()
41    }
42    fn decode_field(input: &str) -> Result<Self, CsvDecodeError> {
43        match input {
44            "true" => Ok(true),
45            "false" => Ok(false),
46            _ => Err(CsvDecodeError::field("field is not `true` or `false`")),
47        }
48    }
49}
50
51impl CsvField for String {
52    fn encode_field(&self) -> String {
53        self.clone()
54    }
55    fn decode_field(input: &str) -> Result<Self, CsvDecodeError> {
56        Ok(input.to_string())
57    }
58}
59
60impl<T: CsvField> CsvField for Option<T> {
61    fn encode_field(&self) -> String {
62        match self {
63            Some(value) => value.encode_field(),
64            None => String::new(),
65        }
66    }
67    fn decode_field(input: &str) -> Result<Self, CsvDecodeError> {
68        if input.is_empty() {
69            Ok(None)
70        } else {
71            T::decode_field(input).map(Some)
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn integers_round_trip_and_reject() {
82        assert_eq!(255u8.encode_field(), "255");
83        assert_eq!(u8::decode_field("255").unwrap(), 255);
84        assert!(u8::decode_field("256").is_err());
85        assert!(u8::decode_field("").is_err());
86        assert_eq!(i32::decode_field("-5").unwrap(), -5);
87    }
88
89    #[test]
90    fn bool_is_strict() {
91        assert_eq!(true.encode_field(), "true");
92        assert_eq!(false.encode_field(), "false");
93        assert!(bool::decode_field("true").unwrap());
94        assert!(!bool::decode_field("false").unwrap());
95        assert!(bool::decode_field("True").is_err());
96        assert!(bool::decode_field("1").is_err());
97    }
98
99    #[test]
100    fn string_encode_and_decode() {
101        assert_eq!(String::from("hi").encode_field(), "hi");
102        assert_eq!(String::decode_field("hi").unwrap(), "hi");
103        assert_eq!(String::decode_field("").unwrap(), "");
104    }
105
106    #[test]
107    fn option_uses_empty_for_none() {
108        assert_eq!(Option::<u8>::None.encode_field(), "");
109        assert_eq!(Some(7u8).encode_field(), "7");
110        assert_eq!(Option::<u8>::decode_field("").unwrap(), None);
111        assert_eq!(Option::<u8>::decode_field("7").unwrap(), Some(7));
112        assert!(Option::<u8>::decode_field("x").is_err());
113    }
114}