1#![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#[derive(Debug, thiserror::Error)]
36pub enum Error {
37 #[error("quick_csv")]
39 QuickCsv(#[from] quick_csv::error::Error),
40 #[error("csv")]
42 Csv(#[from] csv::Error),
43}
44
45pub type Result<T> = core::result::Result<T, Error>;
47
48pub struct CSVLine {
50 separator: u8,
51}
52
53impl CSVLine {
54 pub fn new() -> Self {
56 Default::default()
57 }
58
59 pub fn with_separator(mut self, separator: u8) -> Self {
61 self.separator = separator;
62 self
63 }
64
65 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
86pub fn from_str<T: DeserializeOwned>(s: &str) -> Result<T> {
88 CSVLine::new().decode_str(s)
89}
90
91pub 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}