csv_line/
lib.rs

1//! Fast deserialization of a single csv line.
2//!
3//! Usage
4//! -----
5//! ```
6//! #[derive(Debug, PartialEq, serde::Deserialize)]
7//! struct Foo(String, i32);
8//!
9//! assert_eq!(csv_line::from_str::<Foo>("foo,42").unwrap(), Foo("foo".into(), 42));
10//! assert_eq!(csv_line::from_str_sep::<Foo>("foo 42", b' ').unwrap(), Foo("foo".into(), 42));
11//! ```
12//!
13//! Speed
14//! -----
15//! The performance is comparable with [serde_json] (lower is better):
16//! ```bench
17//! test csv_builder ... bench:      16,003 ns/iter (+/- 914)
18//! test csv_core    ... bench:      15,695 ns/iter (+/- 1,155)
19//! test csv_line    ... bench:         240 ns/iter (+/- 14)
20//! test serde_json  ... bench:         124 ns/iter (+/- 5)
21//! ```
22//! The benchmark code is [here][bench].
23//!
24//! [serde_json]: https://github.com/serde-rs/json
25//! [bench]: https://github.com/imbolc/csv-line/blob/main/benches/csv-line.rs
26
27#![warn(clippy::all, missing_docs, nonstandard_style, future_incompatible)]
28#![forbid(unsafe_code)]
29#![cfg_attr(docsrs, feature(doc_cfg))]
30
31use csv::StringRecord;
32use serde::de::DeserializeOwned;
33
34/// An error that can occur when processing CSV data
35#[derive(Debug, thiserror::Error)]
36pub enum Error {
37    /// A wrapper for `quick_csv::Error`
38    #[error("quick_csv")]
39    QuickCsv(#[from] quick_csv::error::Error),
40    /// A wrapper for `csv::Error`
41    #[error("csv")]
42    Csv(#[from] csv::Error),
43}
44
45/// A type alias for `Result<T, csv_line::Error>`
46pub type Result<T> = core::result::Result<T, Error>;
47
48/// A struct to hold the parser settings
49pub struct CSVLine {
50    separator: u8,
51}
52
53impl CSVLine {
54    /// Returns a new parser initialized with the default separator
55    pub fn new() -> Self {
56        Default::default()
57    }
58
59    /// Sets a new separator, the default is `,`
60    pub fn with_separator(mut self, separator: u8) -> Self {
61        self.separator = separator;
62        self
63    }
64
65    /// Deserializes the string
66    pub fn decode_str<T: DeserializeOwned>(&self, s: &str) -> Result<T> {
67        let record = if let Some(row) = quick_csv::Csv::from_string(s)
68            .delimiter(self.separator)
69            .into_iter()
70            .next()
71        {
72            StringRecord::from_iter(row?.columns()?)
73        } else {
74            StringRecord::from(vec![""])
75        };
76        Ok(record.deserialize(None)?)
77    }
78}
79
80impl Default for CSVLine {
81    fn default() -> Self {
82        Self { separator: b',' }
83    }
84}
85
86/// Deserializes the string
87pub fn from_str<T: DeserializeOwned>(s: &str) -> Result<T> {
88    CSVLine::new().decode_str(s)
89}
90
91/// Deserialize a csv formatted &str where the separator is specified
92///
93/// # Arguments
94///
95/// * `s` - A borrowed string slice containing csv formatted data
96/// * `sep` - A u8 containing the separator use to csv format `s`
97///
98/// # Example with whitespace as separator:
99///
100/// ```
101/// #[derive(Debug, PartialEq, serde::Deserialize)]
102/// struct Bar(Vec<u32>);
103///
104/// assert_eq!(csv_line::from_str_sep::<Bar>("31 42 28 97 0", b' ').unwrap(), Bar(vec![31,42,28,97,0]));
105/// ```
106pub fn from_str_sep<T: DeserializeOwned>(s: &str, sep: u8) -> Result<T> {
107    CSVLine::new().with_separator(sep).decode_str(s)
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use serde::Deserialize;
114
115    #[test]
116    fn basic() {
117        #[derive(Debug, PartialEq, Deserialize)]
118        struct Foo(String);
119        assert_eq!(from_str::<Foo>("foo").unwrap(), Foo("foo".into()));
120        assert_eq!(from_str_sep::<Foo>("foo", b' ').unwrap(), Foo("foo".into()));
121    }
122
123    #[test]
124    fn empty() {
125        #[derive(Debug, PartialEq, Deserialize)]
126        struct Foo(Option<String>);
127        assert_eq!(from_str::<Foo>("").unwrap(), Foo(None));
128        assert_eq!(from_str_sep::<Foo>("", b' ').unwrap(), Foo(None));
129    }
130
131    #[test]
132    fn types() {
133        #[derive(Debug, PartialEq, Deserialize)]
134        struct Foo {
135            text: String,
136            maybe_text: Option<String>,
137            num: i32,
138            flag: bool,
139        }
140        assert_eq!(
141            from_str::<Foo>(r#""foo,bar",,1,true"#).unwrap(),
142            Foo {
143                text: "foo,bar".into(),
144                maybe_text: None,
145                num: 1,
146                flag: true
147            }
148        );
149        assert_eq!(
150            from_str_sep::<Foo>(r#""foo bar"  1 true"#, b' ').unwrap(),
151            Foo {
152                text: "foo bar".into(),
153                maybe_text: None,
154                num: 1,
155                flag: true
156            }
157        );
158    }
159
160    #[test]
161    fn tsv() {
162        #[derive(Debug, PartialEq, Deserialize)]
163        struct Foo(String, String);
164        assert_eq!(
165            CSVLine::new()
166                .with_separator(b'\t')
167                .decode_str::<Foo>("foo\tbar")
168                .unwrap(),
169            Foo("foo".into(), "bar".into())
170        );
171    }
172}