1use std::collections::HashMap;
2use std::sync::Arc;
3
4pub struct DeliveryBus {
10 sqs_sender: Option<Arc<dyn SqsDelivery>>,
12 sns_sender: Option<Arc<dyn SnsDelivery>>,
14 eventbridge_sender: Option<Arc<dyn EventBridgeDelivery>>,
16 lambda_invoker: Option<Arc<dyn LambdaDelivery>>,
18 kinesis_sender: Option<Arc<dyn KinesisDelivery>>,
20 stepfunctions_starter: Option<Arc<dyn StepFunctionsDelivery>>,
22 sagemaker_pipeline_starter: Option<Arc<dyn SageMakerPipelineDelivery>>,
24 s3_writer: Option<Arc<dyn S3Delivery>>,
26 firehose_sender: Option<Arc<dyn FirehoseDelivery>>,
28 ses_dispatcher: Option<Arc<dyn SesSendEmailDispatcher>>,
30 ecs_task_runner: Option<Arc<dyn EcsTaskRunner>>,
32 elbv2_target_registration: Option<Arc<dyn Elbv2TargetRegistration>>,
34 ec2_network_lookup: Option<Arc<dyn Ec2NetworkLookup>>,
35 cloudwatch_metrics: Option<Arc<dyn CloudwatchDelivery>>,
38 cloudwatch_logs: Option<Arc<dyn CloudwatchLogsDelivery>>,
41 cognito_jwt_verifier: Option<Arc<dyn CognitoJwtVerifier>>,
45 kms_hook: Option<Arc<dyn KmsHook>>,
48}
49
50#[derive(Debug, Clone)]
52pub struct SqsMessageAttribute {
53 pub data_type: String,
54 pub string_value: Option<String>,
55 pub binary_value: Option<String>,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum SqsDeliveryError {
64 QueueNotFound(String),
66 InvalidArn(String),
68 InvalidParameter(String),
73 AccessDenied(String),
77 TargetUnavailable(String),
80}
81
82impl std::fmt::Display for SqsDeliveryError {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 match self {
85 Self::QueueNotFound(arn) => write!(f, "queue not found: {arn}"),
86 Self::InvalidArn(arn) => write!(f, "invalid queue ARN: {arn}"),
87 Self::InvalidParameter(msg) => write!(f, "invalid parameter: {msg}"),
88 Self::AccessDenied(msg) => write!(f, "access denied: {msg}"),
89 Self::TargetUnavailable(msg) => write!(f, "target unavailable: {msg}"),
90 }
91 }
92}
93
94impl std::error::Error for SqsDeliveryError {}
95
96pub trait SqsDelivery: Send + Sync {
98 fn deliver_to_queue(
99 &self,
100 queue_arn: &str,
101 message_body: &str,
102 attributes: &HashMap<String, String>,
103 );
104
105 fn deliver_to_queue_with_attrs(
107 &self,
108 queue_arn: &str,
109 message_body: &str,
110 message_attributes: &HashMap<String, SqsMessageAttribute>,
111 message_group_id: Option<&str>,
112 message_dedup_id: Option<&str>,
113 ) {
114 let _ = (message_attributes, message_group_id, message_dedup_id);
116 self.deliver_to_queue(queue_arn, message_body, &HashMap::new());
117 }
118
119 fn queue_arn_for_url(&self, region: &str, queue_url: &str) -> Option<String> {
126 let _ = (region, queue_url);
127 None
128 }
129
130 fn try_deliver_to_queue_with_attrs(
136 &self,
137 queue_arn: &str,
138 message_body: &str,
139 message_attributes: &HashMap<String, SqsMessageAttribute>,
140 message_group_id: Option<&str>,
141 message_dedup_id: Option<&str>,
142 ) -> Result<(), SqsDeliveryError> {
143 self.deliver_to_queue_with_attrs(
144 queue_arn,
145 message_body,
146 message_attributes,
147 message_group_id,
148 message_dedup_id,
149 );
150 Ok(())
151 }
152}
153
154pub trait SnsDelivery: Send + Sync {
156 fn publish_to_topic(&self, topic_arn: &str, message: &str, subject: Option<&str>);
157
158 fn publish_to_topic_fifo(
162 &self,
163 topic_arn: &str,
164 message: &str,
165 subject: Option<&str>,
166 _message_group_id: Option<&str>,
167 _message_dedup_id: Option<&str>,
168 ) {
169 self.publish_to_topic(topic_arn, message, subject);
170 }
171}
172
173#[derive(Debug, Clone, Copy)]
185pub struct CrossServiceEvent<'a> {
186 pub source: &'a str,
187 pub detail_type: &'a str,
188 pub detail: &'a str,
190 pub event_bus: &'a str,
194 pub account_id: &'a str,
196 pub region: &'a str,
198 pub resources: &'a [String],
200 pub principal_arn: Option<&'a str>,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
209pub enum EventBridgeDeliveryError {
210 #[error("AccessDeniedException: {0}")]
214 AccessDenied(String),
215 #[error("InternalException: {0}")]
218 Unavailable(String),
219}
220
221pub trait EventBridgeDelivery: Send + Sync {
223 fn put_event(&self, event: &CrossServiceEvent<'_>) -> Result<String, EventBridgeDeliveryError>;
227}
228
229pub trait LambdaDelivery: Send + Sync {
231 fn invoke_lambda(
234 &self,
235 function_arn: &str,
236 payload: &str,
237 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>, String>> + Send>>;
238}
239
240pub trait KinesisDelivery: Send + Sync {
242 fn put_record(&self, stream_arn: &str, data: &str, partition_key: &str);
245}
246
247pub trait StepFunctionsDelivery: Send + Sync {
249 fn start_execution(&self, state_machine_arn: &str, input: &str);
252}
253
254pub trait SageMakerPipelineDelivery: Send + Sync {
259 fn start_pipeline_execution(&self, pipeline_arn: &str, parameters: &serde_json::Value);
260}
261
262pub trait FirehoseDelivery: Send + Sync {
268 fn put_record(&self, delivery_stream_arn: &str, data: &[u8]);
269}
270
271pub trait S3Delivery: Send + Sync {
275 fn put_object(
278 &self,
279 account_id: &str,
280 bucket: &str,
281 key: &str,
282 body: Vec<u8>,
283 content_type: Option<&str>,
284 ) -> Result<(), String>;
285
286 fn get_object(&self, account_id: &str, bucket: &str, key: &str) -> Result<Vec<u8>, String>;
291}
292
293pub trait SesSendEmailDispatcher: Send + Sync {
298 #[allow(clippy::too_many_arguments)]
299 fn send_email(
300 &self,
301 account_id: &str,
302 from: &str,
303 to: Vec<String>,
304 cc: Vec<String>,
305 bcc: Vec<String>,
306 subject: Option<&str>,
307 text_body: Option<&str>,
308 html_body: Option<&str>,
309 ) -> Result<(), String>;
310}
311
312pub trait EcsTaskRunner: Send + Sync {
316 fn run_task(
317 &self,
318 account_id: &str,
319 cluster: &str,
320 task_definition: &str,
321 launch_type: Option<&str>,
322 count: usize,
323 ) -> Result<(), String>;
324}
325
326pub trait Elbv2TargetRegistration: Send + Sync {
330 fn register_targets(
331 &self,
332 account_id: &str,
333 target_group_arn: &str,
334 targets: Vec<(String, Option<i64>)>,
335 );
336 fn deregister_targets(
337 &self,
338 account_id: &str,
339 target_group_arn: &str,
340 targets: Vec<(String, Option<i64>)>,
341 );
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct TaskEni {
347 pub eni_id: String,
348 pub private_ip: String,
349 pub mac_address: String,
350}
351
352pub trait Ec2NetworkLookup: Send + Sync {
357 fn create_task_eni(
360 &self,
361 account_id: &str,
362 subnet_id: &str,
363 group_ids: Vec<String>,
364 description: String,
365 ) -> Result<TaskEni, String>;
366 fn delete_task_eni(&self, account_id: &str, eni_id: &str);
368}
369
370pub trait CloudwatchDelivery: Send + Sync {
374 #[allow(clippy::too_many_arguments)]
375 fn put_metric(
376 &self,
377 account_id: &str,
378 region: &str,
379 namespace: &str,
380 metric_name: &str,
381 value: f64,
382 unit: Option<&str>,
383 dimensions: std::collections::BTreeMap<String, String>,
384 timestamp_ms: i64,
385 );
386}
387
388pub trait CloudwatchLogsDelivery: Send + Sync {
393 fn put_log_events(
394 &self,
395 account_id: &str,
396 region: &str,
397 log_group_name: &str,
398 log_stream_name: &str,
399 events: &[(i64, String)],
400 );
401}
402
403pub trait EmailDispatcher: Send + Sync {
407 fn send_email(
408 &self,
409 account_id: &str,
410 from: &str,
411 to: &str,
412 subject: &str,
413 body_text: &str,
414 body_html: Option<&str>,
415 );
416}
417
418pub trait SmsDispatcher: Send + Sync {
421 fn send_sms(&self, account_id: &str, region: &str, phone_number: &str, message: &str);
424}
425
426pub trait KmsHook: Send + Sync {
436 fn encrypt(
437 &self,
438 account_id: &str,
439 region: &str,
440 key_id: &str,
441 plaintext: &[u8],
442 service_principal: &str,
443 encryption_context: std::collections::HashMap<String, String>,
444 ) -> Result<String, String>;
445
446 fn decrypt(
447 &self,
448 account_id: &str,
449 ciphertext_b64: &str,
450 service_principal: &str,
451 encryption_context: std::collections::HashMap<String, String>,
452 ) -> Result<Vec<u8>, String>;
453
454 fn resolve_key_arn(
458 &self,
459 _account_id: &str,
460 _region: &str,
461 _key_id: &str,
462 _service_principal: &str,
463 ) -> Result<String, String> {
464 Err("key resolution is not supported by this hook".to_string())
465 }
466
467 fn aws_managed_key_arn(
472 &self,
473 _account_id: &str,
474 _region: &str,
475 _service: &str,
476 _service_principal: &str,
477 ) -> Result<String, String> {
478 Err("AWS-managed key resolution is not supported by this hook".to_string())
479 }
480}
481
482pub fn aws_managed_kms_key_arn(
491 hook: Option<&dyn KmsHook>,
492 account_id: &str,
493 region: &str,
494 service: &str,
495) -> Option<String> {
496 let hook = hook?;
497 let alias = format!("alias/aws/{service}");
498 let principal = format!("{service}.amazonaws.com");
499 match hook.aws_managed_key_arn(account_id, region, service, &principal) {
500 Ok(arn) => Some(arn),
501 Err(err) => {
502 tracing::warn!(
503 %err,
504 account_id,
505 region,
506 alias = %alias,
507 "could not resolve the AWS-managed KMS key; reporting no key"
508 );
509 None
510 }
511 }
512}
513
514pub fn kms_key_or_aws_managed(
518 hook: Option<&dyn KmsHook>,
519 named: Option<&str>,
520 account_id: &str,
521 region: &str,
522 service: &str,
523) -> Option<String> {
524 match named.filter(|k| !k.is_empty()) {
525 Some(key) => Some(key.to_string()),
526 None => aws_managed_kms_key_arn(hook, account_id, region, service),
527 }
528}
529
530pub fn kms_key_arn_or_aws_managed(
534 hook: Option<&dyn KmsHook>,
535 named: Option<&str>,
536 account_id: &str,
537 region: &str,
538 service: &str,
539) -> Option<String> {
540 match named.filter(|k| !k.is_empty()) {
541 Some(key) => Some(resolve_named_kms_key_arn(
542 hook, key, account_id, region, service,
543 )),
544 None => aws_managed_kms_key_arn(hook, account_id, region, service),
545 }
546}
547
548pub fn resolve_named_kms_key_arn(
554 hook: Option<&dyn KmsHook>,
555 key: &str,
556 account_id: &str,
557 region: &str,
558 service: &str,
559) -> String {
560 let Some(h) = hook else {
561 return key.to_string();
562 };
563 if let Some(rest) = key.strip_prefix("arn:") {
567 if rest.split(':').nth(2).is_some_and(|r| r != region) {
568 return key.to_string();
569 }
570 }
571 let resource = key.rsplit(':').next().unwrap_or(key);
575 let owner = resource
576 .strip_prefix("alias/aws/")
577 .filter(|s| !s.is_empty())
578 .unwrap_or(service);
579 let principal = format!("{owner}.amazonaws.com");
580 match h.resolve_key_arn(account_id, region, key, &principal) {
581 Ok(arn) => arn,
582 Err(err) => {
583 tracing::debug!(%err, key, "KMS key not resolvable; reporting it as given");
584 key.to_string()
585 }
586 }
587}
588
589pub trait CognitoJwtVerifier: Send + Sync {
597 fn verify_token(
598 &self,
599 account_id: &str,
600 user_pool_arn: &str,
601 token: &str,
602 ) -> Result<serde_json::Value, String>;
603}
604
605impl DeliveryBus {
606 pub fn new() -> Self {
607 Self {
608 sqs_sender: None,
609 sns_sender: None,
610 eventbridge_sender: None,
611 lambda_invoker: None,
612 kinesis_sender: None,
613 stepfunctions_starter: None,
614 sagemaker_pipeline_starter: None,
615 s3_writer: None,
616 firehose_sender: None,
617 ses_dispatcher: None,
618 ecs_task_runner: None,
619 elbv2_target_registration: None,
620 ec2_network_lookup: None,
621 cloudwatch_metrics: None,
622 cloudwatch_logs: None,
623 cognito_jwt_verifier: None,
624 kms_hook: None,
625 }
626 }
627
628 pub fn with_cognito_jwt_verifier(mut self, verifier: Arc<dyn CognitoJwtVerifier>) -> Self {
629 self.cognito_jwt_verifier = Some(verifier);
630 self
631 }
632
633 pub fn with_kms_hook(mut self, hook: Arc<dyn KmsHook>) -> Self {
634 self.kms_hook = Some(hook);
635 self
636 }
637
638 pub fn kms_encrypt(
641 &self,
642 account_id: &str,
643 region: &str,
644 key_id: &str,
645 plaintext: &[u8],
646 service_principal: &str,
647 encryption_context: std::collections::HashMap<String, String>,
648 ) -> Result<String, String> {
649 match self.kms_hook {
650 Some(ref h) => h.encrypt(
651 account_id,
652 region,
653 key_id,
654 plaintext,
655 service_principal,
656 encryption_context,
657 ),
658 None => Err("KMS hook not configured".to_string()),
659 }
660 }
661
662 pub fn kms_decrypt(
665 &self,
666 account_id: &str,
667 ciphertext_b64: &str,
668 service_principal: &str,
669 encryption_context: std::collections::HashMap<String, String>,
670 ) -> Result<Vec<u8>, String> {
671 match self.kms_hook {
672 Some(ref h) => h.decrypt(
673 account_id,
674 ciphertext_b64,
675 service_principal,
676 encryption_context,
677 ),
678 None => Err("KMS hook not configured".to_string()),
679 }
680 }
681
682 pub fn verify_cognito_jwt(
685 &self,
686 account_id: &str,
687 user_pool_arn: &str,
688 token: &str,
689 ) -> Result<serde_json::Value, String> {
690 match self.cognito_jwt_verifier {
691 Some(ref v) => v.verify_token(account_id, user_pool_arn, token),
692 None => Err("Cognito JWT verifier not configured".to_string()),
693 }
694 }
695
696 pub fn with_cloudwatch_metrics(mut self, sender: Arc<dyn CloudwatchDelivery>) -> Self {
697 self.cloudwatch_metrics = Some(sender);
698 self
699 }
700
701 #[allow(clippy::too_many_arguments)]
705 pub fn put_cloudwatch_metric(
706 &self,
707 account_id: &str,
708 region: &str,
709 namespace: &str,
710 metric_name: &str,
711 value: f64,
712 unit: Option<&str>,
713 dimensions: std::collections::BTreeMap<String, String>,
714 timestamp_ms: i64,
715 ) {
716 if let Some(ref sender) = self.cloudwatch_metrics {
717 sender.put_metric(
718 account_id,
719 region,
720 namespace,
721 metric_name,
722 value,
723 unit,
724 dimensions,
725 timestamp_ms,
726 );
727 }
728 }
729
730 pub fn with_cloudwatch_logs(mut self, sender: Arc<dyn CloudwatchLogsDelivery>) -> Self {
731 self.cloudwatch_logs = Some(sender);
732 self
733 }
734
735 pub fn put_log_events(
738 &self,
739 account_id: &str,
740 region: &str,
741 log_group_name: &str,
742 log_stream_name: &str,
743 events: &[(i64, String)],
744 ) {
745 if let Some(ref sender) = self.cloudwatch_logs {
746 sender.put_log_events(account_id, region, log_group_name, log_stream_name, events);
747 }
748 }
749
750 pub fn with_ses_dispatcher(mut self, dispatcher: Arc<dyn SesSendEmailDispatcher>) -> Self {
751 self.ses_dispatcher = Some(dispatcher);
752 self
753 }
754
755 pub fn with_ecs_task_runner(mut self, runner: Arc<dyn EcsTaskRunner>) -> Self {
756 self.ecs_task_runner = Some(runner);
757 self
758 }
759
760 pub fn with_elbv2_target_registration(mut self, reg: Arc<dyn Elbv2TargetRegistration>) -> Self {
761 self.elbv2_target_registration = Some(reg);
762 self
763 }
764
765 pub fn with_ec2_network_lookup(mut self, lookup: Arc<dyn Ec2NetworkLookup>) -> Self {
766 self.ec2_network_lookup = Some(lookup);
767 self
768 }
769
770 pub fn create_task_eni(
773 &self,
774 account_id: &str,
775 subnet_id: &str,
776 group_ids: Vec<String>,
777 description: String,
778 ) -> Option<Result<TaskEni, String>> {
779 self.ec2_network_lookup
780 .as_ref()
781 .map(|l| l.create_task_eni(account_id, subnet_id, group_ids, description))
782 }
783
784 pub fn delete_task_eni(&self, account_id: &str, eni_id: &str) {
786 if let Some(l) = &self.ec2_network_lookup {
787 l.delete_task_eni(account_id, eni_id);
788 }
789 }
790
791 pub fn register_elbv2_targets(
794 &self,
795 account_id: &str,
796 target_group_arn: &str,
797 targets: Vec<(String, Option<i64>)>,
798 ) {
799 if let Some(ref reg) = self.elbv2_target_registration {
800 reg.register_targets(account_id, target_group_arn, targets);
801 }
802 }
803
804 pub fn deregister_elbv2_targets(
807 &self,
808 account_id: &str,
809 target_group_arn: &str,
810 targets: Vec<(String, Option<i64>)>,
811 ) {
812 if let Some(ref reg) = self.elbv2_target_registration {
813 reg.deregister_targets(account_id, target_group_arn, targets);
814 }
815 }
816
817 #[allow(clippy::too_many_arguments)]
820 pub fn send_ses_email(
821 &self,
822 account_id: &str,
823 from: &str,
824 to: Vec<String>,
825 cc: Vec<String>,
826 bcc: Vec<String>,
827 subject: Option<&str>,
828 text_body: Option<&str>,
829 html_body: Option<&str>,
830 ) -> Result<(), String> {
831 match self.ses_dispatcher {
832 Some(ref d) => {
833 d.send_email(account_id, from, to, cc, bcc, subject, text_body, html_body)
834 }
835 None => Err("SES dispatcher not configured".to_string()),
836 }
837 }
838
839 pub fn run_ecs_task(
842 &self,
843 account_id: &str,
844 cluster: &str,
845 task_definition: &str,
846 launch_type: Option<&str>,
847 count: usize,
848 ) -> Result<(), String> {
849 match self.ecs_task_runner {
850 Some(ref r) => r.run_task(account_id, cluster, task_definition, launch_type, count),
851 None => Err("ECS task runner not configured".to_string()),
852 }
853 }
854
855 pub fn with_s3(mut self, sender: Arc<dyn S3Delivery>) -> Self {
856 self.s3_writer = Some(sender);
857 self
858 }
859
860 pub fn with_firehose(mut self, sender: Arc<dyn FirehoseDelivery>) -> Self {
861 self.firehose_sender = Some(sender);
862 self
863 }
864
865 pub fn put_record_to_firehose(&self, delivery_stream_arn: &str, data: &[u8]) {
869 if let Some(ref sender) = self.firehose_sender {
870 sender.put_record(delivery_stream_arn, data);
871 }
872 }
873
874 pub fn put_object_to_s3(
877 &self,
878 account_id: &str,
879 bucket: &str,
880 key: &str,
881 body: Vec<u8>,
882 content_type: Option<&str>,
883 ) -> Result<(), String> {
884 match self.s3_writer {
885 Some(ref sender) => sender.put_object(account_id, bucket, key, body, content_type),
886 None => Err("S3 writer not configured".to_string()),
887 }
888 }
889
890 pub fn get_object_from_s3(
893 &self,
894 account_id: &str,
895 bucket: &str,
896 key: &str,
897 ) -> Result<Vec<u8>, String> {
898 match self.s3_writer {
899 Some(ref sender) => sender.get_object(account_id, bucket, key),
900 None => Err("S3 client not configured".to_string()),
901 }
902 }
903
904 pub fn with_sqs(mut self, sender: Arc<dyn SqsDelivery>) -> Self {
905 self.sqs_sender = Some(sender);
906 self
907 }
908
909 pub fn with_sns(mut self, sender: Arc<dyn SnsDelivery>) -> Self {
910 self.sns_sender = Some(sender);
911 self
912 }
913
914 pub fn with_eventbridge(mut self, sender: Arc<dyn EventBridgeDelivery>) -> Self {
915 self.eventbridge_sender = Some(sender);
916 self
917 }
918
919 pub fn with_lambda(mut self, invoker: Arc<dyn LambdaDelivery>) -> Self {
920 self.lambda_invoker = Some(invoker);
921 self
922 }
923
924 pub fn with_kinesis(mut self, sender: Arc<dyn KinesisDelivery>) -> Self {
925 self.kinesis_sender = Some(sender);
926 self
927 }
928
929 pub fn put_record_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
932 if let Some(ref sender) = self.kinesis_sender {
933 sender.put_record(stream_arn, data, partition_key);
934 }
935 }
936
937 pub fn with_sagemaker_pipeline(mut self, starter: Arc<dyn SageMakerPipelineDelivery>) -> Self {
938 self.sagemaker_pipeline_starter = Some(starter);
939 self
940 }
941
942 pub fn start_sagemaker_pipeline(&self, pipeline_arn: &str, parameters: &serde_json::Value) {
944 if let Some(ref starter) = self.sagemaker_pipeline_starter {
945 starter.start_pipeline_execution(pipeline_arn, parameters);
946 }
947 }
948
949 pub fn with_stepfunctions(mut self, starter: Arc<dyn StepFunctionsDelivery>) -> Self {
950 self.stepfunctions_starter = Some(starter);
951 self
952 }
953
954 pub fn sqs_queue_arn_for_url(&self, region: &str, queue_url: &str) -> Option<String> {
957 self.sqs_sender
958 .as_ref()?
959 .queue_arn_for_url(region, queue_url)
960 }
961
962 pub fn send_to_sqs(
964 &self,
965 queue_arn: &str,
966 message_body: &str,
967 attributes: &HashMap<String, String>,
968 ) {
969 if let Some(ref sender) = self.sqs_sender {
970 sender.deliver_to_queue(queue_arn, message_body, attributes);
971 }
972 }
973
974 pub fn send_to_sqs_with_attrs(
976 &self,
977 queue_arn: &str,
978 message_body: &str,
979 message_attributes: &HashMap<String, SqsMessageAttribute>,
980 message_group_id: Option<&str>,
981 message_dedup_id: Option<&str>,
982 ) {
983 if let Some(ref sender) = self.sqs_sender {
984 sender.deliver_to_queue_with_attrs(
985 queue_arn,
986 message_body,
987 message_attributes,
988 message_group_id,
989 message_dedup_id,
990 );
991 }
992 }
993
994 pub fn try_send_to_sqs_with_attrs(
999 &self,
1000 queue_arn: &str,
1001 message_body: &str,
1002 message_attributes: &HashMap<String, SqsMessageAttribute>,
1003 message_group_id: Option<&str>,
1004 message_dedup_id: Option<&str>,
1005 ) -> Result<(), SqsDeliveryError> {
1006 match self.sqs_sender {
1007 Some(ref sender) => sender.try_deliver_to_queue_with_attrs(
1008 queue_arn,
1009 message_body,
1010 message_attributes,
1011 message_group_id,
1012 message_dedup_id,
1013 ),
1014 None => Err(SqsDeliveryError::QueueNotFound(queue_arn.to_string())),
1015 }
1016 }
1017
1018 pub fn publish_to_sns(&self, topic_arn: &str, message: &str, subject: Option<&str>) {
1020 if let Some(ref sender) = self.sns_sender {
1021 sender.publish_to_topic(topic_arn, message, subject);
1022 }
1023 }
1024
1025 pub fn put_event_to_eventbridge(
1029 &self,
1030 event: &CrossServiceEvent<'_>,
1031 ) -> Result<Option<String>, EventBridgeDeliveryError> {
1032 match self.eventbridge_sender {
1033 Some(ref sender) => sender.put_event(event).map(Some),
1034 None => Ok(None),
1035 }
1036 }
1037
1038 pub async fn invoke_lambda(
1040 &self,
1041 function_arn: &str,
1042 payload: &str,
1043 ) -> Option<Result<Vec<u8>, String>> {
1044 if let Some(ref invoker) = self.lambda_invoker {
1045 Some(invoker.invoke_lambda(function_arn, payload).await)
1046 } else {
1047 None
1048 }
1049 }
1050
1051 pub fn send_to_kinesis(&self, stream_arn: &str, data: &str, partition_key: &str) {
1053 if let Some(ref sender) = self.kinesis_sender {
1054 sender.put_record(stream_arn, data, partition_key);
1055 }
1056 }
1057
1058 pub fn start_stepfunctions_execution(&self, state_machine_arn: &str, input: &str) {
1060 if let Some(ref starter) = self.stepfunctions_starter {
1061 starter.start_execution(state_machine_arn, input);
1062 }
1063 }
1064}
1065
1066impl Default for DeliveryBus {
1067 fn default() -> Self {
1068 Self::new()
1069 }
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075 use std::sync::atomic::{AtomicUsize, Ordering};
1076 use std::sync::Arc;
1077
1078 struct MockSqs {
1080 call_count: AtomicUsize,
1081 }
1082 impl SqsDelivery for MockSqs {
1083 fn deliver_to_queue(
1084 &self,
1085 _queue_arn: &str,
1086 _message_body: &str,
1087 _attributes: &HashMap<String, String>,
1088 ) {
1089 self.call_count.fetch_add(1, Ordering::SeqCst);
1090 }
1091 }
1092
1093 struct MockSns {
1094 call_count: AtomicUsize,
1095 }
1096 impl SnsDelivery for MockSns {
1097 fn publish_to_topic(&self, _topic_arn: &str, _message: &str, _subject: Option<&str>) {
1098 self.call_count.fetch_add(1, Ordering::SeqCst);
1099 }
1100 }
1101
1102 struct MockEventBridge {
1103 call_count: AtomicUsize,
1104 }
1105 impl EventBridgeDelivery for MockEventBridge {
1106 fn put_event(
1107 &self,
1108 _event: &CrossServiceEvent<'_>,
1109 ) -> Result<String, EventBridgeDeliveryError> {
1110 self.call_count.fetch_add(1, Ordering::SeqCst);
1111 Ok("id".to_string())
1112 }
1113 }
1114
1115 fn test_event<'a>(source: &'a str, bus: &'a str) -> CrossServiceEvent<'a> {
1116 CrossServiceEvent {
1117 source,
1118 detail_type: "type",
1119 detail: "{}",
1120 event_bus: bus,
1121 account_id: "111111111111",
1122 region: "eu-west-1",
1123 resources: &[],
1124 principal_arn: None,
1125 }
1126 }
1127
1128 struct MockKinesis {
1129 call_count: AtomicUsize,
1130 }
1131 impl KinesisDelivery for MockKinesis {
1132 fn put_record(&self, _stream_arn: &str, _data: &str, _partition_key: &str) {
1133 self.call_count.fetch_add(1, Ordering::SeqCst);
1134 }
1135 }
1136
1137 struct MockStepFunctions {
1138 call_count: AtomicUsize,
1139 }
1140 impl StepFunctionsDelivery for MockStepFunctions {
1141 fn start_execution(&self, _state_machine_arn: &str, _input: &str) {
1142 self.call_count.fetch_add(1, Ordering::SeqCst);
1143 }
1144 }
1145
1146 #[test]
1147 fn delivery_bus_new_has_no_senders() {
1148 let bus = DeliveryBus::new();
1149 bus.send_to_sqs("arn:queue", "body", &HashMap::new());
1151 bus.publish_to_sns("arn:topic", "msg", None);
1152 let _ = bus.put_event_to_eventbridge(&test_event("src", "default"));
1153 bus.send_to_kinesis("arn:stream", "data", "pk");
1154 bus.start_stepfunctions_execution("arn:sfn", "{}");
1155 }
1157
1158 #[test]
1159 fn delivery_bus_default_is_same_as_new() {
1160 let bus = DeliveryBus::default();
1161 bus.send_to_sqs("arn:q", "b", &HashMap::new());
1162 }
1163
1164 #[test]
1165 fn send_to_sqs_calls_sender() {
1166 let mock = Arc::new(MockSqs {
1167 call_count: AtomicUsize::new(0),
1168 });
1169 let bus = DeliveryBus::new().with_sqs(mock.clone());
1170
1171 bus.send_to_sqs("arn:queue", "msg", &HashMap::new());
1172 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1173
1174 bus.send_to_sqs("arn:queue2", "msg2", &HashMap::new());
1175 assert_eq!(mock.call_count.load(Ordering::SeqCst), 2);
1176 }
1177
1178 #[test]
1179 fn send_to_sqs_with_attrs_calls_sender() {
1180 let mock = Arc::new(MockSqs {
1181 call_count: AtomicUsize::new(0),
1182 });
1183 let bus = DeliveryBus::new().with_sqs(mock.clone());
1184
1185 let mut attrs = HashMap::new();
1186 attrs.insert(
1187 "key".to_string(),
1188 SqsMessageAttribute {
1189 data_type: "String".to_string(),
1190 string_value: Some("val".to_string()),
1191 binary_value: None,
1192 },
1193 );
1194 bus.send_to_sqs_with_attrs("arn:q", "body", &attrs, Some("group"), Some("dedup"));
1195 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1196 }
1197
1198 #[test]
1199 fn publish_to_sns_calls_sender() {
1200 let mock = Arc::new(MockSns {
1201 call_count: AtomicUsize::new(0),
1202 });
1203 let bus = DeliveryBus::new().with_sns(mock.clone());
1204
1205 bus.publish_to_sns("arn:topic", "message", Some("subject"));
1206 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1207 }
1208
1209 #[test]
1210 fn put_event_to_eventbridge_calls_sender() {
1211 let mock = Arc::new(MockEventBridge {
1212 call_count: AtomicUsize::new(0),
1213 });
1214 let bus = DeliveryBus::new().with_eventbridge(mock.clone());
1215
1216 let _ = bus.put_event_to_eventbridge(&test_event("aws.s3", "default"));
1217 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1218 }
1219
1220 #[test]
1221 fn send_to_kinesis_calls_sender() {
1222 let mock = Arc::new(MockKinesis {
1223 call_count: AtomicUsize::new(0),
1224 });
1225 let bus = DeliveryBus::new().with_kinesis(mock.clone());
1226
1227 bus.send_to_kinesis("arn:stream", "data", "partition-key");
1228 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1229 }
1230
1231 #[test]
1232 fn start_stepfunctions_calls_sender() {
1233 let mock = Arc::new(MockStepFunctions {
1234 call_count: AtomicUsize::new(0),
1235 });
1236 let bus = DeliveryBus::new().with_stepfunctions(mock.clone());
1237
1238 bus.start_stepfunctions_execution("arn:sfn:machine", r#"{"key":"val"}"#);
1239 assert_eq!(mock.call_count.load(Ordering::SeqCst), 1);
1240 }
1241
1242 #[test]
1243 fn builder_chaining_works() {
1244 let sqs = Arc::new(MockSqs {
1245 call_count: AtomicUsize::new(0),
1246 });
1247 let sns = Arc::new(MockSns {
1248 call_count: AtomicUsize::new(0),
1249 });
1250 let eb = Arc::new(MockEventBridge {
1251 call_count: AtomicUsize::new(0),
1252 });
1253 let kin = Arc::new(MockKinesis {
1254 call_count: AtomicUsize::new(0),
1255 });
1256 let sfn = Arc::new(MockStepFunctions {
1257 call_count: AtomicUsize::new(0),
1258 });
1259
1260 let bus = DeliveryBus::new()
1261 .with_sqs(sqs.clone())
1262 .with_sns(sns.clone())
1263 .with_eventbridge(eb.clone())
1264 .with_kinesis(kin.clone())
1265 .with_stepfunctions(sfn.clone());
1266
1267 bus.send_to_sqs("q", "m", &HashMap::new());
1268 bus.publish_to_sns("t", "m", None);
1269 let _ = bus.put_event_to_eventbridge(&test_event("s", "b"));
1270 bus.send_to_kinesis("s", "d", "k");
1271 bus.start_stepfunctions_execution("sm", "{}");
1272
1273 assert_eq!(sqs.call_count.load(Ordering::SeqCst), 1);
1274 assert_eq!(sns.call_count.load(Ordering::SeqCst), 1);
1275 assert_eq!(eb.call_count.load(Ordering::SeqCst), 1);
1276 assert_eq!(kin.call_count.load(Ordering::SeqCst), 1);
1277 assert_eq!(sfn.call_count.load(Ordering::SeqCst), 1);
1278 }
1279
1280 #[tokio::test]
1281 async fn invoke_lambda_returns_none_without_invoker() {
1282 let bus = DeliveryBus::new();
1283 let result = bus.invoke_lambda("arn:lambda", "{}").await;
1284 assert!(result.is_none());
1285 }
1286}