faucet_source_s3/
config.rs1use faucet_core::DEFAULT_BATCH_SIZE;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
9#[serde(rename_all = "snake_case")]
10pub enum S3FileFormat {
11 #[default]
13 JsonLines,
14 JsonArray,
16 RawText,
18 #[cfg(feature = "arrow")]
26 Parquet,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
31pub struct S3SourceConfig {
32 pub bucket: String,
34 pub prefix: Option<String>,
36 pub region: Option<String>,
38 pub endpoint_url: Option<String>,
40 pub file_format: S3FileFormat,
42 pub max_objects: Option<usize>,
44 pub concurrency: usize,
46 #[serde(default = "default_batch_size")]
60 pub batch_size: usize,
61 #[serde(default = "default_true")]
69 pub verify_length: bool,
70 #[serde(default)]
79 pub verify_checksum: bool,
80 #[cfg(feature = "compression")]
86 #[serde(default)]
87 pub compression: faucet_core::CompressionConfig,
88}
89
90fn default_batch_size() -> usize {
91 DEFAULT_BATCH_SIZE
92}
93
94fn default_true() -> bool {
95 true
96}
97
98impl S3SourceConfig {
99 pub fn new(bucket: impl Into<String>) -> Self {
101 Self {
102 bucket: bucket.into(),
103 prefix: None,
104 region: None,
105 endpoint_url: None,
106 file_format: S3FileFormat::default(),
107 max_objects: None,
108 concurrency: 10,
109 batch_size: DEFAULT_BATCH_SIZE,
110 verify_length: true,
111 verify_checksum: false,
112 #[cfg(feature = "compression")]
113 compression: faucet_core::CompressionConfig::default(),
114 }
115 }
116
117 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
119 self.prefix = Some(prefix.into());
120 self
121 }
122
123 pub fn region(mut self, region: impl Into<String>) -> Self {
125 self.region = Some(region.into());
126 self
127 }
128
129 pub fn endpoint_url(mut self, url: impl Into<String>) -> Self {
131 self.endpoint_url = Some(url.into());
132 self
133 }
134
135 pub fn file_format(mut self, format: S3FileFormat) -> Self {
137 self.file_format = format;
138 self
139 }
140
141 pub fn max_objects(mut self, max: usize) -> Self {
143 self.max_objects = Some(max);
144 self
145 }
146
147 pub fn concurrency(mut self, concurrency: usize) -> Self {
149 self.concurrency = concurrency;
150 self
151 }
152
153 pub fn with_batch_size(mut self, batch_size: usize) -> Self {
159 self.batch_size = batch_size;
160 self
161 }
162
163 pub fn verify_length(mut self, verify: bool) -> Self {
166 self.verify_length = verify;
167 self
168 }
169
170 pub fn verify_checksum(mut self, verify: bool) -> Self {
173 self.verify_checksum = verify;
174 self
175 }
176
177 #[cfg(feature = "compression")]
179 pub fn compression(mut self, c: faucet_core::CompressionConfig) -> Self {
180 self.compression = c;
181 self
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn default_config() {
191 let config = S3SourceConfig::new("my-bucket");
192 assert_eq!(config.bucket, "my-bucket");
193 assert!(config.prefix.is_none());
194 assert!(config.region.is_none());
195 assert!(config.endpoint_url.is_none());
196 assert!(matches!(config.file_format, S3FileFormat::JsonLines));
197 assert!(config.max_objects.is_none());
198 }
199
200 #[test]
201 fn builder_methods() {
202 let config = S3SourceConfig::new("my-bucket")
203 .prefix("data/")
204 .region("us-west-2")
205 .endpoint_url("http://localhost:9000")
206 .file_format(S3FileFormat::JsonArray)
207 .max_objects(10);
208
209 assert_eq!(config.bucket, "my-bucket");
210 assert_eq!(config.prefix.as_deref(), Some("data/"));
211 assert_eq!(config.region.as_deref(), Some("us-west-2"));
212 assert_eq!(
213 config.endpoint_url.as_deref(),
214 Some("http://localhost:9000")
215 );
216 assert!(matches!(config.file_format, S3FileFormat::JsonArray));
217 assert_eq!(config.max_objects, Some(10));
218 }
219
220 #[test]
221 fn file_format_default_is_json_lines() {
222 let format = S3FileFormat::default();
223 assert!(matches!(format, S3FileFormat::JsonLines));
224 }
225
226 #[test]
227 fn batch_size_defaults_to_default_batch_size() {
228 let config = S3SourceConfig::new("my-bucket");
229 assert_eq!(config.batch_size, faucet_core::DEFAULT_BATCH_SIZE);
230 }
231
232 #[test]
233 fn with_batch_size_overrides_default() {
234 let config = S3SourceConfig::new("my-bucket").with_batch_size(500);
235 assert_eq!(config.batch_size, 500);
236 }
237
238 #[test]
239 fn batch_size_zero_is_accepted_as_no_batching_sentinel() {
240 let config = S3SourceConfig::new("my-bucket").with_batch_size(0);
241 assert_eq!(config.batch_size, 0);
242 assert!(faucet_core::validate_batch_size(config.batch_size).is_ok());
243 }
244
245 #[test]
246 fn batch_size_above_max_is_rejected_by_validate_batch_size() {
247 let config =
248 S3SourceConfig::new("my-bucket").with_batch_size(faucet_core::MAX_BATCH_SIZE + 1);
249 assert!(faucet_core::validate_batch_size(config.batch_size).is_err());
250 }
251
252 #[test]
253 fn batch_size_deserializes_from_json() {
254 let json = r#"{
255 "bucket": "my-bucket",
256 "prefix": null,
257 "region": null,
258 "endpoint_url": null,
259 "file_format": "json_lines",
260 "max_objects": null,
261 "concurrency": 10,
262 "batch_size": 250
263 }"#;
264 let config: S3SourceConfig = serde_json::from_str(json).unwrap();
265 assert_eq!(config.batch_size, 250);
266 }
267
268 #[test]
269 fn verify_defaults_length_on_checksum_off() {
270 let cfg = S3SourceConfig::new("b");
271 assert!(cfg.verify_length, "length verification defaults on");
272 assert!(!cfg.verify_checksum, "checksum verification defaults off");
273 }
274
275 #[test]
276 fn verify_fields_default_when_absent_from_json() {
277 let json = r#"{
280 "bucket": "my-bucket",
281 "prefix": null,
282 "region": null,
283 "endpoint_url": null,
284 "file_format": "json_lines",
285 "max_objects": null,
286 "concurrency": 10,
287 "batch_size": 250
288 }"#;
289 let config: S3SourceConfig = serde_json::from_str(json).unwrap();
290 assert!(config.verify_length);
291 assert!(!config.verify_checksum);
292 }
293
294 #[test]
295 fn verify_builders_override() {
296 let cfg = S3SourceConfig::new("b")
297 .verify_length(false)
298 .verify_checksum(true);
299 assert!(!cfg.verify_length);
300 assert!(cfg.verify_checksum);
301 }
302
303 #[cfg(feature = "compression")]
304 #[test]
305 fn compression_default_is_auto() {
306 let cfg = S3SourceConfig::new("bucket");
307 assert_eq!(cfg.compression, faucet_core::CompressionConfig::Auto);
308 }
309}