faucet_source_csv/
config.rs1use faucet_core::DEFAULT_BATCH_SIZE;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
9pub struct CsvSourceConfig {
10 pub path: String,
12 #[serde(default = "default_true")]
14 pub has_headers: bool,
15 #[serde(default = "default_delimiter")]
17 pub delimiter: u8,
18 #[serde(default = "default_quote")]
20 pub quote: u8,
21 #[serde(default)]
33 pub flexible: bool,
34 #[serde(default = "default_batch_size")]
43 pub batch_size: usize,
44 #[cfg(feature = "compression")]
49 #[serde(default)]
50 pub compression: faucet_core::CompressionConfig,
51}
52
53fn default_true() -> bool {
54 true
55}
56
57fn default_delimiter() -> u8 {
58 b','
59}
60
61fn default_quote() -> u8 {
62 b'"'
63}
64
65fn default_batch_size() -> usize {
66 DEFAULT_BATCH_SIZE
67}
68
69impl CsvSourceConfig {
70 pub fn new(path: impl Into<String>) -> Self {
72 Self {
73 path: path.into(),
74 has_headers: true,
75 delimiter: b',',
76 quote: b'"',
77 flexible: false,
78 batch_size: DEFAULT_BATCH_SIZE,
79 #[cfg(feature = "compression")]
80 compression: faucet_core::CompressionConfig::Auto,
81 }
82 }
83
84 pub fn has_headers(mut self, v: bool) -> Self {
86 self.has_headers = v;
87 self
88 }
89
90 pub fn delimiter(mut self, d: u8) -> Self {
92 self.delimiter = d;
93 self
94 }
95
96 pub fn quote(mut self, q: u8) -> Self {
98 self.quote = q;
99 self
100 }
101
102 pub fn flexible(mut self, v: bool) -> Self {
107 self.flexible = v;
108 self
109 }
110
111 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
116 self.batch_size = batch_size;
117 self
118 }
119
120 #[cfg(feature = "compression")]
122 pub fn compression(mut self, c: faucet_core::CompressionConfig) -> Self {
123 self.compression = c;
124 self
125 }
126
127 pub fn validate(&self) -> Result<(), faucet_core::FaucetError> {
135 if self.path.trim().is_empty() {
136 return Err(faucet_core::FaucetError::Config(
137 "CSV source requires a non-empty `path`".into(),
138 ));
139 }
140 faucet_core::validate_batch_size(self.batch_size)?;
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn default_config() {
151 let config = CsvSourceConfig::new("/tmp/data.csv");
152 assert_eq!(config.path, "/tmp/data.csv");
153 assert!(config.has_headers);
154 assert_eq!(config.delimiter, b',');
155 assert_eq!(config.quote, b'"');
156 }
157
158 #[test]
159 fn builder_methods() {
160 let config = CsvSourceConfig::new("/tmp/data.tsv")
161 .has_headers(false)
162 .delimiter(b'\t')
163 .quote(b'\'');
164 assert!(!config.has_headers);
165 assert_eq!(config.delimiter, b'\t');
166 assert_eq!(config.quote, b'\'');
167 }
168
169 #[test]
170 fn flexible_defaults_to_false_strict() {
171 let config = CsvSourceConfig::new("/tmp/data.csv");
172 assert!(!config.flexible);
173 }
174
175 #[test]
176 fn flexible_builder_and_serde_default() {
177 let config = CsvSourceConfig::new("/tmp/data.csv").flexible(true);
178 assert!(config.flexible);
179
180 let json = r#"{ "path": "/tmp/data.csv" }"#;
182 let parsed: CsvSourceConfig = serde_json::from_str(json).unwrap();
183 assert!(!parsed.flexible);
184
185 let json = r#"{ "path": "/tmp/data.csv", "flexible": true }"#;
187 let parsed: CsvSourceConfig = serde_json::from_str(json).unwrap();
188 assert!(parsed.flexible);
189 }
190
191 #[test]
192 fn batch_size_defaults_to_default_batch_size() {
193 let config = CsvSourceConfig::new("/tmp/data.csv");
194 assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
195 }
196
197 #[test]
198 fn with_batch_size_overrides_default() {
199 let config = CsvSourceConfig::new("/tmp/data.csv").with_batch_size(500);
200 assert_eq!(config.batch_size, 500);
201 }
202
203 #[test]
204 fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
205 let config = CsvSourceConfig::new("/tmp/data.csv").with_batch_size(0);
206 assert_eq!(config.batch_size, 0);
207 assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
208 }
209
210 #[test]
211 fn batch_size_above_max_is_rejected_by_validate_batch_size() {
212 let config =
213 CsvSourceConfig::new("/tmp/data.csv").with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
214 assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
215 }
216
217 #[test]
218 fn batch_size_deserializes_from_json() {
219 let json = r#"{
220 "path": "/tmp/data.csv",
221 "batch_size": 250
222 }"#;
223 let config: CsvSourceConfig = serde_json::from_str(json).unwrap();
224 assert_eq!(config.batch_size, 250);
225 }
226
227 #[test]
228 fn validate_accepts_valid_config() {
229 assert!(CsvSourceConfig::new("/tmp/data.csv").validate().is_ok());
230 }
231
232 #[test]
233 fn validate_rejects_oversized_batch_size() {
234 let config =
235 CsvSourceConfig::new("/tmp/data.csv").with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
236 assert!(matches!(
237 config.validate(),
238 Err(faucet_core::FaucetError::Config(_))
239 ));
240 }
241
242 #[test]
243 fn validate_rejects_empty_path() {
244 assert!(matches!(
245 CsvSourceConfig::new(" ").validate(),
246 Err(faucet_core::FaucetError::Config(_))
247 ));
248 }
249}