1#![allow(clippy::disallowed_types)] use std::collections::HashMap;
4use std::fmt;
5use std::sync::Arc;
6
7use arrow_ipc::writer::{DictionaryTracker, IpcDataGenerator, IpcWriteOptions};
8use arrow_schema::{Schema, SchemaRef};
9
10use crate::error::ConnectorError;
11
12#[must_use]
14pub fn encode_arrow_schema_ipc(schema: &Schema) -> String {
15 let mut dt = DictionaryTracker::new(false);
16 let enc = IpcDataGenerator::default().schema_to_bytes_with_dictionary_tracker(
17 schema,
18 &mut dt,
19 &IpcWriteOptions::default(),
20 );
21 let bytes = &enc.ipc_message;
22 let mut s = String::with_capacity(bytes.len() * 2);
23 for b in bytes {
24 use std::fmt::Write;
25 let _ = write!(s, "{b:02x}");
26 }
27 s
28}
29
30#[must_use]
32pub fn decode_arrow_schema_ipc(hex: &str) -> Option<Schema> {
33 let bytes: Option<Vec<u8>> = (0..hex.len())
34 .step_by(2)
35 .map(|i| {
36 hex.get(i..i + 2)
37 .and_then(|h| u8::from_str_radix(h, 16).ok())
38 })
39 .collect();
40 arrow_ipc::convert::try_schema_from_flatbuffer_bytes(&bytes?).ok()
41}
42
43#[derive(Debug, Clone, Default)]
48pub struct ConnectorConfig {
49 connector_type: String,
51
52 properties: HashMap<String, String>,
54 schema_binding: Option<Arc<crate::schema::resolution::SchemaBinding>>,
56}
57
58impl ConnectorConfig {
59 #[must_use]
61 pub fn new(connector_type: impl Into<String>) -> Self {
62 Self {
63 connector_type: connector_type.into(),
64 properties: HashMap::new(),
65 schema_binding: None,
66 }
67 }
68
69 #[must_use]
71 pub fn with_properties(
72 connector_type: impl Into<String>,
73 properties: HashMap<String, String>,
74 ) -> Self {
75 Self {
76 connector_type: connector_type.into(),
77 properties,
78 schema_binding: None,
79 }
80 }
81
82 #[must_use]
84 pub fn connector_type(&self) -> &str {
85 &self.connector_type
86 }
87
88 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
90 self.properties.insert(key.into(), value.into());
91 }
92
93 #[must_use]
95 pub fn get(&self, key: &str) -> Option<&str> {
96 self.properties.get(key).map(String::as_str)
97 }
98
99 pub fn require(&self, key: &str) -> Result<&str, ConnectorError> {
105 self.get(key)
106 .ok_or_else(|| ConnectorError::missing_config(key.to_string()))
107 }
108
109 pub fn get_parsed<T: std::str::FromStr>(&self, key: &str) -> Result<Option<T>, ConnectorError>
115 where
116 T::Err: fmt::Display,
117 {
118 match self.get(key) {
119 Some(v) => v.parse::<T>().map(Some).map_err(|e| {
120 ConnectorError::ConfigurationError(format!("invalid value for '{key}': {e}"))
121 }),
122 None => Ok(None),
123 }
124 }
125
126 pub fn require_parsed<T: std::str::FromStr>(&self, key: &str) -> Result<T, ConnectorError>
133 where
134 T::Err: fmt::Display,
135 {
136 let value = self.require(key)?;
137 value.parse::<T>().map_err(|e| {
138 ConnectorError::ConfigurationError(format!("invalid value for '{key}': {e}"))
139 })
140 }
141
142 pub fn get_non_empty(&self, key: &str) -> Result<Option<String>, ConnectorError> {
148 match self.get(key).map(str::trim) {
149 None => Ok(None),
150 Some("") => Err(ConnectorError::ConfigurationError(format!(
151 "'{key}' must not be empty"
152 ))),
153 Some(value) => Ok(Some(value.to_string())),
154 }
155 }
156
157 #[must_use]
159 pub fn get_list(&self, key: &str) -> Vec<String> {
160 self.get(key)
161 .into_iter()
162 .flat_map(|value| value.split(','))
163 .map(str::trim)
164 .filter(|item| !item.is_empty())
165 .map(str::to_string)
166 .collect()
167 }
168
169 pub fn get_positive(&self, key: &str, default: usize) -> Result<usize, ConnectorError> {
175 match self.get_parsed::<usize>(key)? {
176 Some(0) => Err(ConnectorError::ConfigurationError(format!(
177 "'{key}' must be positive"
178 ))),
179 Some(value) => Ok(value),
180 None => Ok(default),
181 }
182 }
183
184 pub fn get_millis(
190 &self,
191 key: &str,
192 default: std::time::Duration,
193 max: std::time::Duration,
194 ) -> Result<std::time::Duration, ConnectorError> {
195 let Some(millis) = self.get_parsed::<u64>(key)? else {
196 return Ok(default);
197 };
198 let value = std::time::Duration::from_millis(millis);
199 if value.is_zero() || value > max {
200 return Err(ConnectorError::ConfigurationError(format!(
201 "'{key}' must be between 1 and {} milliseconds",
202 max.as_millis()
203 )));
204 }
205 Ok(value)
206 }
207
208 pub fn set_schema_binding(
213 &mut self,
214 binding: crate::schema::resolution::SchemaBinding,
215 ) -> Result<(), ConnectorError> {
216 binding
217 .canonical_bytes()
218 .map_err(crate::schema::resolution::binding_error)?;
219 if binding.connector != self.connector_type {
220 return Err(ConnectorError::SchemaMismatch(
221 "schema contract belongs to another connector".into(),
222 ));
223 }
224 self.schema_binding = Some(Arc::new(binding));
225 Ok(())
226 }
227
228 #[must_use]
230 pub fn schema_binding(&self) -> Option<&crate::schema::resolution::SchemaBinding> {
231 self.schema_binding.as_deref()
232 }
233
234 #[must_use]
236 pub fn properties(&self) -> &HashMap<String, String> {
237 &self.properties
238 }
239
240 #[must_use]
242 pub fn properties_with_prefix(&self, prefix: &str) -> HashMap<String, String> {
243 self.properties
244 .iter()
245 .filter_map(|(k, v)| {
246 k.strip_prefix(prefix)
247 .map(|stripped| (stripped.to_string(), v.clone()))
248 })
249 .collect()
250 }
251
252 #[must_use]
254 pub fn arrow_schema(&self) -> Option<SchemaRef> {
255 let s = self.get("_arrow_schema")?;
256 decode_arrow_schema_ipc(s).map(Arc::new)
257 }
258
259 #[cfg(test)]
264 #[must_use]
265 pub fn display_redacted(&self) -> String {
266 use crate::storage::SecretMasker;
267 SecretMasker::display_map(&self.properties)
268 }
269
270 pub fn validate(&self, specs: &[ConfigKeySpec]) -> Result<(), ConnectorError> {
277 for spec in specs {
278 if spec.required && self.get(&spec.key).is_none() {
279 if let Some(ref default) = spec.default {
280 let _ = default;
282 } else {
283 return Err(ConnectorError::missing_config(spec.key.clone()));
284 }
285 }
286 }
287 Ok(())
288 }
289
290 pub fn reject_unknown_properties(
296 &self,
297 allowed: &[&str],
298 connector: &str,
299 ) -> Result<(), ConnectorError> {
300 let mut unknown: Vec<&str> = self
301 .properties
302 .keys()
303 .map(String::as_str)
304 .filter(|key| !allowed.contains(key))
305 .collect();
306 if unknown.is_empty() {
307 return Ok(());
308 }
309
310 unknown.sort_unstable();
311 Err(ConnectorError::ConfigurationError(format!(
312 "unknown {connector} propert{}: {}",
313 if unknown.len() == 1 { "y" } else { "ies" },
314 unknown.join(", ")
315 )))
316 }
317}
318
319pub fn require_non_empty(value: &str, field_name: &str) -> Result<(), ConnectorError> {
325 if value.is_empty() {
326 return Err(ConnectorError::ConfigurationError(format!(
327 "{field_name} must not be empty",
328 )));
329 }
330 Ok(())
331}
332
333pub fn parse_port(value: &str) -> Result<u16, ConnectorError> {
339 value
340 .parse()
341 .map_err(|_| ConnectorError::ConfigurationError(format!("invalid port: '{value}'")))
342}
343
344#[derive(Debug, Clone, serde::Serialize)]
348pub struct ConfigKeySpec {
349 pub key: String,
351
352 pub description: String,
354
355 pub required: bool,
357
358 pub default: Option<String>,
360}
361
362impl ConfigKeySpec {
363 #[must_use]
365 pub fn required(key: impl Into<String>, description: impl Into<String>) -> Self {
366 Self {
367 key: key.into(),
368 description: description.into(),
369 required: true,
370 default: None,
371 }
372 }
373
374 #[must_use]
376 pub fn optional(
377 key: impl Into<String>,
378 description: impl Into<String>,
379 default: impl Into<String>,
380 ) -> Self {
381 Self {
382 key: key.into(),
383 description: description.into(),
384 required: false,
385 default: Some(default.into()),
386 }
387 }
388}
389
390#[derive(Debug, Clone, serde::Serialize)]
392pub struct ConnectorInfo {
393 pub schema_capabilities: crate::schema::resolution::SchemaCapabilities,
395 pub name: String,
397
398 pub display_name: String,
400
401 pub version: String,
403
404 pub is_source: bool,
406
407 pub is_sink: bool,
409
410 pub config_keys: Vec<ConfigKeySpec>,
412}
413
414#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub enum ConnectorState {
417 Created,
419
420 Initializing,
422
423 Running,
425
426 Paused,
428
429 Recovering,
431
432 Closed,
434
435 Failed,
437}
438
439impl fmt::Display for ConnectorState {
440 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441 match self {
442 ConnectorState::Created => write!(f, "Created"),
443 ConnectorState::Initializing => write!(f, "Initializing"),
444 ConnectorState::Running => write!(f, "Running"),
445 ConnectorState::Paused => write!(f, "Paused"),
446 ConnectorState::Recovering => write!(f, "Recovering"),
447 ConnectorState::Closed => write!(f, "Closed"),
448 ConnectorState::Failed => write!(f, "Failed"),
449 }
450 }
451}
452
453#[cfg(test)]
454mod tests {
455 use super::*;
456 use arrow_schema::Schema;
457
458 #[test]
459 fn test_config_basic_operations() {
460 let mut config = ConnectorConfig::new("kafka");
461 config.set("bootstrap.servers", "localhost:9092");
462 config.set("topic", "events");
463
464 assert_eq!(config.connector_type(), "kafka");
465 assert_eq!(config.get("bootstrap.servers"), Some("localhost:9092"));
466 assert_eq!(config.get("topic"), Some("events"));
467 assert_eq!(config.get("missing"), None);
468 }
469
470 #[test]
471 fn test_config_require() {
472 let mut config = ConnectorConfig::new("kafka");
473 config.set("topic", "events");
474
475 assert!(config.require("topic").is_ok());
476 assert!(config.require("missing").is_err());
477 }
478
479 #[test]
480 fn test_config_parsed() {
481 let mut config = ConnectorConfig::new("kafka");
482 config.set("batch.size", "1000");
483 config.set("bad_number", "not_a_number");
484
485 let size: Option<usize> = config.get_parsed("batch.size").unwrap();
486 assert_eq!(size, Some(1000));
487
488 let missing: Option<usize> = config.get_parsed("missing").unwrap();
489 assert_eq!(missing, None);
490
491 let bad: Result<Option<usize>, _> = config.get_parsed("bad_number");
492 assert!(bad.is_err());
493 }
494
495 #[test]
496 fn test_config_require_parsed() {
497 let mut config = ConnectorConfig::new("test");
498 config.set("port", "8080");
499
500 let port: u16 = config.require_parsed("port").unwrap();
501 assert_eq!(port, 8080);
502
503 let missing: Result<u16, _> = config.require_parsed("missing");
504 assert!(missing.is_err());
505 }
506
507 #[test]
508 fn test_config_prefix_extraction() {
509 let mut config = ConnectorConfig::new("kafka");
510 config.set("kafka.bootstrap.servers", "localhost:9092");
511 config.set("kafka.group.id", "my-group");
512 config.set("topic", "events");
513
514 let kafka_props = config.properties_with_prefix("kafka.");
515 assert_eq!(kafka_props.len(), 2);
516 assert_eq!(
517 kafka_props.get("bootstrap.servers"),
518 Some(&"localhost:9092".to_string())
519 );
520 assert_eq!(kafka_props.get("group.id"), Some(&"my-group".to_string()));
521 }
522
523 #[test]
524 fn test_config_validate() {
525 let specs = vec![
526 ConfigKeySpec::required("topic", "Kafka topic"),
527 ConfigKeySpec::optional("batch.size", "Batch size", "100"),
528 ];
529
530 let mut config = ConnectorConfig::new("kafka");
531 config.set("topic", "events");
532
533 assert!(config.validate(&specs).is_ok());
534
535 let empty_config = ConnectorConfig::new("kafka");
536 assert!(empty_config.validate(&specs).is_err());
537 }
538
539 #[test]
540 fn test_config_with_properties() {
541 let mut props = HashMap::new();
542 props.insert("key1".to_string(), "val1".to_string());
543 props.insert("key2".to_string(), "val2".to_string());
544
545 let config = ConnectorConfig::with_properties("test", props);
546 assert_eq!(config.get("key1"), Some("val1"));
547 assert_eq!(config.get("key2"), Some("val2"));
548 }
549
550 #[test]
551 fn test_connector_state_display() {
552 assert_eq!(ConnectorState::Running.to_string(), "Running");
553 assert_eq!(ConnectorState::Failed.to_string(), "Failed");
554 }
555
556 #[test]
557 fn test_display_redacted_masks_secrets() {
558 let mut config = ConnectorConfig::new("delta-lake");
559 config.set("aws_region", "us-east-1");
560 config.set("aws_secret_access_key", "TOP_SECRET");
561 config.set("aws_access_key_id", "AKID123");
562
563 let display = config.display_redacted();
564 assert!(display.contains("aws_region=us-east-1"));
565 assert!(display.contains("aws_secret_access_key=***"));
566 assert!(display.contains("aws_access_key_id=***"));
567 assert!(!display.contains("AKID123"));
568 assert!(!display.contains("TOP_SECRET"));
569 }
570
571 #[test]
572 fn test_display_redacted_empty() {
573 let config = ConnectorConfig::new("test");
574 assert!(config.display_redacted().is_empty());
575 }
576
577 #[test]
578 fn test_arrow_schema_ipc_round_trip() {
579 use arrow_schema::{DataType, Field};
580
581 let original = Schema::new(vec![
582 Field::new("s", DataType::Utf8, true),
583 Field::new("p", DataType::Float64, true),
584 Field::new("q", DataType::Float64, true),
585 Field::new("T", DataType::Int64, true),
586 ]);
587 let hex = encode_arrow_schema_ipc(&original);
588
589 let mut config = ConnectorConfig::new("websocket");
590 config.set("_arrow_schema", hex);
591
592 let schema = config.arrow_schema().expect("should parse");
593 assert_eq!(schema.fields().len(), 4);
594 assert_eq!(schema.field(0).name(), "s");
595 assert_eq!(schema.field(0).data_type(), &DataType::Utf8);
596 assert_eq!(schema.field(1).name(), "p");
597 assert_eq!(schema.field(1).data_type(), &DataType::Float64);
598 assert_eq!(schema.field(3).name(), "T");
599 assert_eq!(schema.field(3).data_type(), &DataType::Int64);
600 }
601
602 #[test]
603 fn test_arrow_schema_ipc_handles_map_type() {
604 use arrow_schema::{DataType, Field, Fields};
605
606 let map_field = Field::new(
607 "entries",
608 DataType::Struct(Fields::from(vec![
609 Field::new("key", DataType::Utf8, false),
610 Field::new("value", DataType::Float64, true),
611 ])),
612 false,
613 );
614 let original = Schema::new(vec![
615 Field::new("sensor_id", DataType::Utf8, true),
616 Field::new("data", DataType::Map(Arc::new(map_field), false), true),
617 ]);
618 let hex = encode_arrow_schema_ipc(&original);
619
620 let mut config = ConnectorConfig::new("kafka");
621 config.set("_arrow_schema", hex);
622
623 let schema = config.arrow_schema().expect("should parse Map type");
624 assert_eq!(schema.fields().len(), 2);
625 assert_eq!(schema.field(0).name(), "sensor_id");
626 assert!(matches!(schema.field(1).data_type(), DataType::Map(_, _)));
627 }
628
629 #[test]
630 fn test_arrow_schema_returns_none_when_absent() {
631 let config = ConnectorConfig::new("kafka");
632 assert!(config.arrow_schema().is_none());
633 }
634
635 #[test]
636 fn test_arrow_schema_returns_none_for_empty_value() {
637 let mut config = ConnectorConfig::new("kafka");
638 config.set("_arrow_schema", "");
639 assert!(config.arrow_schema().is_none());
640 }
641}