faucet_sink_sqs/
config.rs1use faucet_common_sqs::SqsCredentials;
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6
7pub const MAX_ENTRIES_PER_REQUEST: usize = 10;
9pub const MAX_MESSAGE_BYTES: usize = 262_144;
11pub const MAX_BATCH_BYTES: usize = 262_144;
13
14#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
16pub struct SqsSinkConfig {
17 pub queue_url: String,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub region: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub endpoint_url: Option<String>,
25 #[serde(default)]
27 pub credentials: SqsCredentials,
28
29 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub message_group_id: Option<String>,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub message_deduplication_id_field: Option<String>,
39
40 #[serde(default = "default_batch_size")]
42 pub batch_size: usize,
43
44 #[serde(default = "default_concurrency")]
46 pub concurrency: usize,
47 #[serde(default = "default_retry_max_attempts")]
49 pub retry_max_attempts: usize,
50 #[serde(default = "default_retry_initial_backoff_ms")]
52 pub retry_initial_backoff_ms: u64,
53 #[serde(default = "default_retry_max_backoff_ms")]
55 pub retry_max_backoff_ms: u64,
56}
57
58fn default_batch_size() -> usize {
59 MAX_ENTRIES_PER_REQUEST
60}
61fn default_concurrency() -> usize {
62 4
63}
64fn default_retry_max_attempts() -> usize {
65 5
66}
67fn default_retry_initial_backoff_ms() -> u64 {
68 100
69}
70fn default_retry_max_backoff_ms() -> u64 {
71 30_000
72}
73
74impl SqsSinkConfig {
75 pub fn new(queue_url: impl Into<String>) -> Self {
77 Self {
78 queue_url: queue_url.into(),
79 region: None,
80 endpoint_url: None,
81 credentials: SqsCredentials::default(),
82 message_group_id: None,
83 message_deduplication_id_field: None,
84 batch_size: default_batch_size(),
85 concurrency: default_concurrency(),
86 retry_max_attempts: default_retry_max_attempts(),
87 retry_initial_backoff_ms: default_retry_initial_backoff_ms(),
88 retry_max_backoff_ms: default_retry_max_backoff_ms(),
89 }
90 }
91
92 pub fn validate(&self) -> Result<(), faucet_core::FaucetError> {
94 use faucet_core::FaucetError;
95 if self.queue_url.trim().is_empty() {
96 return Err(FaucetError::Config(
97 "sqs sink: queue_url must not be empty".into(),
98 ));
99 }
100 if self.batch_size == 0 || self.batch_size > MAX_ENTRIES_PER_REQUEST {
101 return Err(FaucetError::Config(format!(
102 "sqs sink: batch_size must be 1..={MAX_ENTRIES_PER_REQUEST} (got {}) — the \
103 SendMessageBatch API limit",
104 self.batch_size
105 )));
106 }
107 if self.concurrency == 0 {
108 return Err(FaucetError::Config(
109 "sqs sink: concurrency must be at least 1".into(),
110 ));
111 }
112 if self.retry_max_attempts == 0 {
113 return Err(FaucetError::Config(
114 "sqs sink: retry_max_attempts must be at least 1".into(),
115 ));
116 }
117 if let Some(field) = &self.message_deduplication_id_field
118 && field.is_empty()
119 {
120 return Err(FaucetError::Config(
121 "sqs sink: message_deduplication_id_field must not be empty".into(),
122 ));
123 }
124 Ok(())
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn defaults_are_the_api_limits() {
134 let c = SqsSinkConfig::new("https://q");
135 c.validate().unwrap();
136 assert_eq!(c.batch_size, 10);
137 assert_eq!(c.concurrency, 4);
138 assert!(c.message_group_id.is_none());
139 assert!(c.message_deduplication_id_field.is_none());
140 }
141
142 #[test]
143 fn validation_bounds() {
144 let mut c = SqsSinkConfig::new("https://q");
145 c.batch_size = 11;
146 assert!(c.validate().is_err(), "SendMessageBatch cap is 10");
147 c.batch_size = 0;
148 assert!(c.validate().is_err());
149
150 let mut c = SqsSinkConfig::new("https://q");
151 c.concurrency = 0;
152 assert!(c.validate().is_err());
153
154 let mut c = SqsSinkConfig::new("https://q");
155 c.retry_max_attempts = 0;
156 assert!(c.validate().is_err());
157
158 let mut c = SqsSinkConfig::new("https://q");
159 c.message_deduplication_id_field = Some(String::new());
160 assert!(c.validate().is_err());
161
162 let mut c = SqsSinkConfig::new(" ");
163 c.batch_size = 5;
164 assert!(c.validate().is_err());
165 }
166
167 #[test]
168 fn full_config_parses_from_yaml() {
169 let yaml = r#"
170queue_url: https://sqs.us-east-1.amazonaws.com/1/events.fifo
171region: us-east-1
172endpoint_url: http://127.0.0.1:4566
173credentials: { type: default }
174message_group_id: orders
175message_deduplication_id_field: order_id
176batch_size: 5
177concurrency: 2
178retry_max_attempts: 3
179"#;
180 let c: SqsSinkConfig = serde_yaml::from_str(yaml).unwrap();
181 c.validate().unwrap();
182 assert_eq!(c.message_group_id.as_deref(), Some("orders"));
183 assert_eq!(
184 c.message_deduplication_id_field.as_deref(),
185 Some("order_id")
186 );
187 assert_eq!(c.batch_size, 5);
188 }
189}