1#[cfg(feature = "rust")]
2use bon::Builder;
3#[cfg(feature = "node")]
4use napi_derive::napi;
5#[cfg(feature = "python")]
6use pyo3::{pyclass, pymethods};
7#[cfg(feature = "python")]
8use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pymethods};
9use serde::{Deserialize, Deserializer, Serialize};
10
11fn deserialize_as_json_string<'de, D>(deserializer: D) -> Result<String, D::Error>
12where
13 D: Deserializer<'de>,
14{
15 let value = serde_json::Value::deserialize(deserializer)?;
16 serde_json::to_string(&value).map_err(serde::de::Error::custom)
17}
18
19#[cfg_attr(feature = "node", napi(string_enum))]
23#[derive(Clone, Debug, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum StreamRegion {
26 UsaEast,
27 EuropeCentral,
28 AsiaEast,
29}
30
31#[cfg_attr(feature = "node", napi(string_enum))]
33#[derive(Clone, Debug, Serialize, Deserialize)]
34#[serde(rename_all = "snake_case")]
35pub enum StreamDataset {
36 Block,
37 BlockWithReceipts,
38 Transactions,
39 Logs,
40 Receipts,
41 TraceBlocks,
42 DebugTraces,
43 BlockWithReceiptsDebugTrace,
44 BlockWithReceiptsTraceBlock,
45 BlobSidecars,
46 ProgramsWithLogs,
47 Ledger,
48 Events,
49 Orders,
50 Trades,
51 BookUpdates,
52 Twap,
53 WriterActions,
54}
55
56#[cfg_attr(feature = "node", napi(string_enum))]
58#[derive(Clone, Debug, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case")]
60pub enum StreamDestination {
61 Webhook,
62 S3,
63 Azure,
64 Postgres,
65 Kafka,
66}
67
68#[cfg_attr(feature = "node", napi(string_enum))]
70#[derive(Clone, Debug, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum FilterLanguage {
73 Javascript,
74 Go,
75 Wasm,
76}
77
78#[cfg_attr(feature = "node", napi(string_enum))]
80#[derive(Clone, Debug, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum StreamMetadataLocation {
83 Body,
84 Header,
85 None,
86}
87
88#[cfg_attr(feature = "node", napi(string_enum))]
90#[derive(Clone, Debug, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum ProductType {
93 Stream,
94 Webhook,
95}
96
97#[cfg_attr(feature = "node", napi(string_enum))]
99#[derive(Clone, Debug, Serialize, Deserialize)]
100#[serde(rename_all = "snake_case")]
101pub enum StreamStatus {
102 Active,
103 Paused,
104 Terminated,
105 Completed,
106 Blocked,
107}
108
109#[cfg_attr(feature = "python", gen_stub_pyclass)]
116#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
117#[cfg_attr(feature = "node", napi(object))]
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct WebhookAttributes {
120 pub url: String,
122 pub max_retry: i32,
124 pub retry_interval_sec: i32,
126 pub post_timeout_sec: i32,
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub security_token: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub compression: Option<String>,
134}
135
136#[cfg(feature = "python")]
137#[gen_stub_pymethods]
138#[pymethods]
139impl WebhookAttributes {
140 #[new]
141 #[pyo3(signature = (url, max_retry, retry_interval_sec, post_timeout_sec, compression=None, security_token=None))]
142 pub fn new(
143 url: String,
144 max_retry: i32,
145 retry_interval_sec: i32,
146 post_timeout_sec: i32,
147 compression: Option<String>,
148 security_token: Option<String>,
149 ) -> Self {
150 Self {
151 url,
152 max_retry,
153 retry_interval_sec,
154 post_timeout_sec,
155 security_token,
156 compression,
157 }
158 }
159}
160
161#[cfg_attr(feature = "python", gen_stub_pyclass)]
163#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
164#[cfg_attr(feature = "node", napi(object))]
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct S3Attributes {
167 pub endpoint: String,
169 pub access_key: String,
171 pub secret_key: String,
173 pub bucket: String,
175 pub object_prefix: String,
177 pub compression: String,
179 pub file_type: String,
181 pub max_retry: i32,
183 pub retry_interval_sec: i32,
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub use_ssl: Option<bool>,
188}
189
190#[cfg(feature = "python")]
191#[gen_stub_pymethods]
192#[pymethods]
193impl S3Attributes {
194 #[new]
195 #[allow(clippy::too_many_arguments)]
196 #[pyo3(signature = (endpoint, access_key, secret_key, bucket, object_prefix, compression, file_type, max_retry, retry_interval_sec, use_ssl=None))]
197 pub fn new(
198 endpoint: String,
199 access_key: String,
200 secret_key: String,
201 bucket: String,
202 object_prefix: String,
203 compression: String,
204 file_type: String,
205 max_retry: i32,
206 retry_interval_sec: i32,
207 use_ssl: Option<bool>,
208 ) -> Self {
209 Self {
210 endpoint,
211 access_key,
212 secret_key,
213 bucket,
214 object_prefix,
215 compression,
216 file_type,
217 max_retry,
218 retry_interval_sec,
219 use_ssl,
220 }
221 }
222}
223
224#[cfg_attr(feature = "python", gen_stub_pyclass)]
226#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
227#[cfg_attr(feature = "node", napi(object))]
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct AzureAttributes {
230 pub storage_account: String,
232 pub sas_token: String,
234 pub container: String,
236 pub compression: String,
238 pub file_type: String,
240 pub max_retry: i32,
242 pub retry_interval_sec: i32,
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub blob_prefix: Option<String>,
247}
248
249#[cfg(feature = "python")]
250#[gen_stub_pymethods]
251#[pymethods]
252impl AzureAttributes {
253 #[new]
254 #[allow(clippy::too_many_arguments)]
255 #[pyo3(signature = (storage_account, sas_token, container, compression, file_type, max_retry, retry_interval_sec, blob_prefix=None))]
256 pub fn new(
257 storage_account: String,
258 sas_token: String,
259 container: String,
260 compression: String,
261 file_type: String,
262 max_retry: i32,
263 retry_interval_sec: i32,
264 blob_prefix: Option<String>,
265 ) -> Self {
266 Self {
267 storage_account,
268 sas_token,
269 container,
270 compression,
271 file_type,
272 max_retry,
273 retry_interval_sec,
274 blob_prefix,
275 }
276 }
277}
278
279#[cfg_attr(feature = "python", gen_stub_pyclass)]
281#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
282#[cfg_attr(feature = "node", napi(object))]
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct PostgresAttributes {
285 pub host: String,
287 pub port: i32,
289 pub database: String,
291 pub username: String,
293 pub password: String,
295 pub table_name: String,
297 pub sslmode: String,
299 pub max_retry: i32,
301 pub retry_interval_sec: i32,
303}
304
305#[cfg(feature = "python")]
306#[gen_stub_pymethods]
307#[pymethods]
308impl PostgresAttributes {
309 #[new]
310 #[allow(clippy::too_many_arguments)]
311 pub fn new(
312 host: String,
313 port: i32,
314 database: String,
315 username: String,
316 password: String,
317 table_name: String,
318 sslmode: String,
319 max_retry: i32,
320 retry_interval_sec: i32,
321 ) -> Self {
322 Self {
323 host,
324 port,
325 database,
326 username,
327 password,
328 table_name,
329 sslmode,
330 max_retry,
331 retry_interval_sec,
332 }
333 }
334}
335
336#[cfg_attr(feature = "python", gen_stub_pyclass)]
338#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
339#[cfg_attr(feature = "node", napi(object))]
340#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct KafkaAttributes {
342 pub bootstrap_servers: String,
344 pub topic_name: String,
346 pub compression_type: String,
348 pub batch_size: i32,
350 pub linger_ms: i32,
352 pub max_message_bytes: i32,
354 pub timeout_sec: i32,
356 pub max_retry: i32,
358 pub retry_interval_sec: i32,
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub username: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub password: Option<String>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub protocol: Option<String>,
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub mechanisms: Option<String>,
372}
373
374#[cfg(feature = "python")]
375#[gen_stub_pymethods]
376#[pymethods]
377impl KafkaAttributes {
378 #[new]
379 #[pyo3(signature = (bootstrap_servers, topic_name, compression_type, batch_size, linger_ms, max_message_bytes, timeout_sec, max_retry, retry_interval_sec, username=None, password=None, protocol=None, mechanisms=None))]
380 #[allow(clippy::too_many_arguments)]
381 pub fn new(
382 bootstrap_servers: String,
383 topic_name: String,
384 compression_type: String,
385 batch_size: i32,
386 linger_ms: i32,
387 max_message_bytes: i32,
388 timeout_sec: i32,
389 max_retry: i32,
390 retry_interval_sec: i32,
391 username: Option<String>,
392 password: Option<String>,
393 protocol: Option<String>,
394 mechanisms: Option<String>,
395 ) -> Self {
396 Self {
397 bootstrap_servers,
398 topic_name,
399 compression_type,
400 batch_size,
401 linger_ms,
402 max_message_bytes,
403 timeout_sec,
404 max_retry,
405 retry_interval_sec,
406 username,
407 password,
408 protocol,
409 mechanisms,
410 }
411 }
412}
413
414#[cfg_attr(feature = "python", gen_stub_pyclass)]
419#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
420#[cfg_attr(feature = "node", napi(object))]
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub struct AddressBookConfig {
423 pub address_book_id: String,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub objects_filter_path: Option<String>,
428 pub elements_filter_paths: Vec<String>,
430}
431
432#[cfg(feature = "python")]
433#[gen_stub_pymethods]
434#[pymethods]
435impl AddressBookConfig {
436 #[new]
437 #[pyo3(signature = (address_book_id, elements_filter_paths, objects_filter_path=None))]
438 pub fn new(
439 address_book_id: String,
440 elements_filter_paths: Vec<String>,
441 objects_filter_path: Option<String>,
442 ) -> Self {
443 Self {
444 address_book_id,
445 objects_filter_path,
446 elements_filter_paths,
447 }
448 }
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
461#[serde(
462 tag = "destination",
463 content = "destination_attributes",
464 rename_all = "snake_case"
465)]
466pub enum DestinationAttributes {
467 Webhook(WebhookAttributes),
469 S3(S3Attributes),
471 Azure(AzureAttributes),
473 Postgres(PostgresAttributes),
475 Kafka(KafkaAttributes),
477}
478
479impl DestinationAttributes {
480 pub fn tag(&self) -> StreamDestination {
481 match self {
482 Self::Webhook(_) => StreamDestination::Webhook,
483 Self::S3(_) => StreamDestination::S3,
484 Self::Azure(_) => StreamDestination::Azure,
485 Self::Postgres(_) => StreamDestination::Postgres,
486 Self::Kafka(_) => StreamDestination::Kafka,
487 }
488 }
489}
490
491#[cfg_attr(feature = "rust", derive(Builder))]
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct CreateStreamParams {
497 pub name: String,
499 pub region: StreamRegion,
501 pub network: String,
503 pub dataset: StreamDataset,
505 pub start_range: i64,
507 pub end_range: i64,
509 #[serde(flatten)]
512 pub destination_attributes: DestinationAttributes,
513 #[serde(skip_serializing_if = "Option::is_none")]
515 pub plan: Option<String>,
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub threshold_fetch_buffer: Option<i64>,
519 pub dataset_batch_size: i64,
521 #[serde(skip_serializing_if = "Option::is_none")]
523 pub max_batch_size: Option<i64>,
524 #[serde(skip_serializing_if = "Option::is_none")]
526 pub max_buffer_range_size: Option<i64>,
527 #[serde(skip_serializing_if = "Option::is_none")]
529 pub max_buffer_processing_workers: Option<i64>,
530 #[serde(skip_serializing_if = "Option::is_none")]
532 pub keep_distance_from_tip: Option<i64>,
533 #[serde(skip_serializing_if = "Option::is_none")]
535 pub filter_function: Option<String>,
536 #[serde(skip_serializing_if = "Option::is_none")]
538 pub filter_language: Option<FilterLanguage>,
539 #[serde(skip_serializing_if = "Option::is_none")]
541 pub address_book_config: Option<AddressBookConfig>,
542 #[serde(skip_serializing_if = "Option::is_none")]
544 pub include_stream_metadata: Option<StreamMetadataLocation>,
545 #[serde(skip_serializing_if = "Option::is_none")]
547 pub product_type: Option<ProductType>,
548 #[serde(skip_serializing_if = "Option::is_none")]
550 pub status: Option<StreamStatus>,
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub notification_email: Option<String>,
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub charge_min_cap: Option<i32>,
557 #[serde(skip_serializing_if = "Option::is_none")]
559 pub fix_block_reorgs: Option<i32>,
560 pub elastic_batch_enabled: bool,
562 #[serde(skip_serializing_if = "Option::is_none")]
565 pub extra_destinations: Option<Vec<DestinationAttributes>>,
566}
567
568#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct Stream {
573 pub id: String,
575 pub name: String,
577 pub status: String,
579 pub created_at: String,
581 pub updated_at: String,
583 pub sequence: i64,
585 pub network: String,
587 pub dataset: String,
589 pub region: String,
591 pub start_range: i64,
593 pub end_range: i64,
595 #[serde(skip_serializing_if = "Option::is_none")]
597 pub plan: Option<String>,
598 #[serde(skip_serializing_if = "Option::is_none")]
600 pub threshold_fetch_buffer: Option<i64>,
601 #[serde(skip_serializing_if = "Option::is_none")]
603 pub dataset_batch_size: Option<i64>,
604 #[serde(skip_serializing_if = "Option::is_none")]
606 pub max_batch_size: Option<i64>,
607 #[serde(skip_serializing_if = "Option::is_none")]
609 pub max_buffer_range_size: Option<i64>,
610 #[serde(skip_serializing_if = "Option::is_none")]
612 pub max_buffer_processing_workers: Option<i64>,
613 #[serde(skip_serializing_if = "Option::is_none")]
615 pub keep_distance_from_tip: Option<i64>,
616 #[serde(skip_serializing_if = "Option::is_none")]
618 pub filter_function: Option<String>,
619 #[serde(skip_serializing_if = "Option::is_none")]
621 pub filter_language: Option<String>,
622 #[serde(skip_serializing_if = "Option::is_none")]
624 pub include_stream_metadata: Option<String>,
625 #[serde(skip_serializing_if = "Option::is_none")]
627 pub product_type: Option<String>,
628 #[serde(skip_serializing_if = "Option::is_none")]
630 pub notification_email: Option<String>,
631 #[serde(skip_serializing_if = "Option::is_none")]
633 pub fix_block_reorgs: Option<i32>,
634 #[serde(skip_serializing_if = "Option::is_none")]
636 pub current_hash: Option<String>,
637 #[serde(flatten, default, skip_serializing_if = "Option::is_none")]
640 pub destination_attributes: Option<DestinationAttributes>,
641 #[serde(skip_serializing_if = "Option::is_none")]
643 pub elastic_batch_enabled: Option<bool>,
644 #[serde(skip_serializing_if = "Option::is_none")]
646 pub qn_account_id: Option<String>,
647 #[serde(skip_serializing_if = "Option::is_none")]
649 pub charge_min_cap: Option<i32>,
650 #[serde(skip_serializing_if = "Option::is_none")]
652 pub memo: Option<String>,
653 #[serde(skip_serializing_if = "Option::is_none")]
655 pub address_book_config: Option<AddressBookConfig>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub extra_destinations: Option<Vec<DestinationAttributes>>,
659}
660
661#[cfg_attr(feature = "python", gen_stub_pyclass)]
665#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
666#[cfg_attr(feature = "node", napi(object))]
667#[derive(Debug, Clone, Serialize, Deserialize)]
668pub struct PageInfo {
669 pub limit: i64,
671 pub offset: i64,
673 pub total: i64,
675}
676
677#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct ListStreamsResponse {
680 pub data: Vec<Stream>,
682 #[serde(rename = "pageInfo")]
684 pub page_info: PageInfo,
685}
686
687#[cfg_attr(feature = "node", napi(object))]
689#[cfg_attr(not(feature = "node"), derive(Clone))]
690#[derive(Debug, Default, Serialize, Deserialize)]
691pub struct ListStreamsParams {
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub stream_type: Option<String>,
695 #[serde(skip_serializing_if = "Option::is_none")]
697 pub offset: Option<i64>,
698 #[serde(skip_serializing_if = "Option::is_none")]
700 pub limit: Option<i64>,
701 #[serde(skip_serializing_if = "Option::is_none")]
703 pub order_by: Option<String>,
704 #[serde(skip_serializing_if = "Option::is_none")]
706 pub order_direction: Option<String>,
707}
708
709#[derive(Debug, Default, Clone, Serialize, Deserialize)]
712pub struct UpdateStreamParams {
713 #[serde(skip_serializing_if = "Option::is_none")]
715 pub name: Option<String>,
716 #[serde(skip_serializing_if = "Option::is_none")]
718 pub region: Option<StreamRegion>,
719 #[serde(skip_serializing_if = "Option::is_none")]
721 pub network: Option<String>,
722 #[serde(skip_serializing_if = "Option::is_none")]
724 pub dataset: Option<StreamDataset>,
725 #[serde(skip_serializing_if = "Option::is_none")]
727 pub start_range: Option<i64>,
728 #[serde(skip_serializing_if = "Option::is_none")]
730 pub end_range: Option<i64>,
731 #[serde(flatten, skip_serializing_if = "Option::is_none")]
734 pub destination_attributes: Option<DestinationAttributes>,
735 #[serde(skip_serializing_if = "Option::is_none")]
737 pub plan: Option<String>,
738 #[serde(skip_serializing_if = "Option::is_none")]
740 pub threshold_fetch_buffer: Option<i64>,
741 #[serde(skip_serializing_if = "Option::is_none")]
743 pub dataset_batch_size: Option<i64>,
744 #[serde(skip_serializing_if = "Option::is_none")]
746 pub max_batch_size: Option<i64>,
747 #[serde(skip_serializing_if = "Option::is_none")]
749 pub max_buffer_range_size: Option<i64>,
750 #[serde(skip_serializing_if = "Option::is_none")]
752 pub max_buffer_processing_workers: Option<i64>,
753 #[serde(skip_serializing_if = "Option::is_none")]
755 pub keep_distance_from_tip: Option<i64>,
756 #[serde(skip_serializing_if = "Option::is_none")]
758 pub filter_function: Option<String>,
759 #[serde(skip_serializing_if = "Option::is_none")]
761 pub filter_language: Option<FilterLanguage>,
762 #[serde(skip_serializing_if = "Option::is_none")]
764 pub address_book_config: Option<AddressBookConfig>,
765 #[serde(skip_serializing_if = "Option::is_none")]
767 pub include_stream_metadata: Option<StreamMetadataLocation>,
768 #[serde(skip_serializing_if = "Option::is_none")]
770 pub notification_email: Option<String>,
771 #[serde(skip_serializing_if = "Option::is_none")]
773 pub charge_min_cap: Option<i32>,
774 #[serde(skip_serializing_if = "Option::is_none")]
776 pub fix_block_reorgs: Option<i32>,
777 #[serde(skip_serializing_if = "Option::is_none")]
779 pub elastic_batch_enabled: Option<bool>,
780 #[serde(skip_serializing_if = "Option::is_none")]
782 pub status: Option<StreamStatus>,
783 #[serde(skip_serializing_if = "Option::is_none")]
785 pub memo: Option<String>,
786 #[serde(skip_serializing_if = "Option::is_none")]
788 pub extra_destinations: Option<Vec<DestinationAttributes>>,
789}
790
791#[cfg_attr(feature = "node", napi(object))]
793#[derive(Clone, Debug, Serialize, Deserialize)]
794pub struct TestFilterParams {
795 pub network: String,
797 pub dataset: StreamDataset,
799 pub block: String,
801 pub filter_function: String,
803 #[serde(skip_serializing_if = "Option::is_none")]
805 pub filter_language: Option<FilterLanguage>,
806 #[serde(skip_serializing_if = "Option::is_none")]
808 pub address_book_config: Option<AddressBookConfig>,
809}
810
811#[cfg_attr(feature = "python", gen_stub_pyclass)]
813#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
814#[cfg_attr(feature = "node", napi(object))]
815#[derive(Debug, Clone, Serialize, Deserialize)]
816pub struct TestFilterResponse {
817 #[serde(deserialize_with = "deserialize_as_json_string")]
819 pub result: String,
820 pub logs: Vec<String>,
822}
823
824#[cfg_attr(feature = "python", gen_stub_pyclass)]
826#[cfg_attr(feature = "python", pyclass(get_all, set_all))]
827#[cfg_attr(feature = "node", napi(object))]
828#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct EnabledCountResponse {
830 pub total: i64,
832}
833
834#[cfg(test)]
835#[allow(clippy::unwrap_used)]
836mod destination_attributes_tests {
837 use super::*;
838
839 #[test]
840 fn webhook_roundtrip() {
841 let attrs = DestinationAttributes::Webhook(WebhookAttributes {
842 url: "https://x.example/hook".to_string(),
843 max_retry: 3,
844 retry_interval_sec: 5,
845 post_timeout_sec: 10,
846 compression: Some("none".to_string()),
847 security_token: None,
848 });
849 let json = serde_json::to_string(&attrs).unwrap();
850 assert!(json.contains(r#""destination":"webhook""#));
851 assert!(json.contains(r#""url":"https://x.example/hook""#));
852 let parsed: DestinationAttributes = serde_json::from_str(&json).unwrap();
853 assert!(matches!(parsed, DestinationAttributes::Webhook(_)));
854 assert!(matches!(parsed.tag(), StreamDestination::Webhook));
855 }
856
857 #[test]
858 fn s3_roundtrip() {
859 let attrs = DestinationAttributes::S3(S3Attributes {
860 endpoint: "s3.amazonaws.com".to_string(),
861 access_key: "AK".to_string(),
862 secret_key: "SK".to_string(),
863 bucket: "b".to_string(),
864 object_prefix: "p".to_string(),
865 compression: "none".to_string(),
866 file_type: "json".to_string(),
867 max_retry: 3,
868 retry_interval_sec: 5,
869 use_ssl: Some(true),
870 });
871 let json = serde_json::to_string(&attrs).unwrap();
872 assert!(json.contains(r#""destination":"s3""#));
873 let parsed: DestinationAttributes = serde_json::from_str(&json).unwrap();
874 assert!(matches!(parsed, DestinationAttributes::S3(_)));
875 }
876
877 #[test]
878 fn azure_roundtrip() {
879 let attrs = DestinationAttributes::Azure(AzureAttributes {
880 storage_account: "acct".to_string(),
881 sas_token: "tok".to_string(),
882 container: "c".to_string(),
883 compression: "none".to_string(),
884 file_type: "json".to_string(),
885 max_retry: 3,
886 retry_interval_sec: 5,
887 blob_prefix: None,
888 });
889 let json = serde_json::to_string(&attrs).unwrap();
890 assert!(json.contains(r#""destination":"azure""#));
891 let parsed: DestinationAttributes = serde_json::from_str(&json).unwrap();
892 assert!(matches!(parsed, DestinationAttributes::Azure(_)));
893 }
894
895 #[test]
896 fn postgres_roundtrip() {
897 let attrs = DestinationAttributes::Postgres(PostgresAttributes {
898 host: "h".to_string(),
899 port: 5432,
900 database: "db".to_string(),
901 username: "u".to_string(),
902 password: "p".to_string(),
903 table_name: "t".to_string(),
904 sslmode: "disable".to_string(),
905 max_retry: 3,
906 retry_interval_sec: 5,
907 });
908 let json = serde_json::to_string(&attrs).unwrap();
909 assert!(json.contains(r#""destination":"postgres""#));
910 let parsed: DestinationAttributes = serde_json::from_str(&json).unwrap();
911 assert!(matches!(parsed, DestinationAttributes::Postgres(_)));
912 }
913
914 #[test]
915 fn kafka_roundtrip() {
916 let attrs = DestinationAttributes::Kafka(KafkaAttributes {
917 bootstrap_servers: "host:9092".to_string(),
918 topic_name: "t".to_string(),
919 compression_type: "gzip".to_string(),
920 batch_size: 100,
921 linger_ms: 10,
922 max_message_bytes: 1024,
923 timeout_sec: 30,
924 max_retry: 3,
925 retry_interval_sec: 5,
926 username: None,
927 password: None,
928 protocol: None,
929 mechanisms: None,
930 });
931 let json = serde_json::to_string(&attrs).unwrap();
932 assert!(json.contains(r#""destination":"kafka""#));
933 let parsed: DestinationAttributes = serde_json::from_str(&json).unwrap();
934 assert!(matches!(parsed, DestinationAttributes::Kafka(_)));
935 }
936}