rusty_cdk_core/sns/
builder.rs

1use crate::iam::PolicyDocument;
2use crate::intrinsic::{get_arn, get_ref};
3use crate::lambda::{FunctionRef, PermissionBuilder};
4use crate::shared::Id;
5use crate::sns::{SnsSubscriptionProperties, Subscription, Topic, TopicPolicy, TopicPolicyProperties, TopicPolicyRef, TopicProperties, TopicRef};
6use crate::stack::{Resource, StackBuilder};
7use crate::type_state;
8use crate::wrappers::{LambdaPermissionAction, StringWithOnlyAlphaNumericsUnderscoresAndHyphens};
9use serde_json::Value;
10use std::marker::PhantomData;
11
12const FIFO_SUFFIX: &str = ".fifo";
13
14#[derive(Debug, Clone)]
15pub enum FifoThroughputScope {
16    Topic,
17    MessageGroup
18}
19
20pub enum SubscriptionType<'a> {
21    Lambda(&'a FunctionRef)
22}
23
24impl From<FifoThroughputScope> for String {
25    fn from(value: FifoThroughputScope) -> Self {
26        match value {
27            FifoThroughputScope::Topic => "Topic".to_string(),
28            FifoThroughputScope::MessageGroup => "MessageGroup".to_string(),
29        }
30    }
31}
32
33type_state!(
34    TopicBuilderState,
35    StartState,
36    StandardStateWithSubscriptions,
37    FifoState,
38    FifoStateWithSubscriptions,
39);
40
41/// Builder for SNS topics.
42///
43/// Supports both standard and FIFO topics with Lambda subscriptions.
44/// FIFO topics have additional configuration for deduplication and throughput.
45///
46/// # Example
47///
48/// ```rust,no_run
49/// use rusty_cdk_core::stack::StackBuilder;
50/// use rusty_cdk_core::sns::{TopicBuilder, SubscriptionType};
51/// # use rusty_cdk_core::lambda::{FunctionBuilder, Architecture, Runtime, Zip};
52/// # use rusty_cdk_core::wrappers::*;
53/// # use rusty_cdk_macros::{memory, timeout, zip_file};
54///
55/// let mut stack_builder = StackBuilder::new();
56///
57/// // Create a simple topic without subscriptions
58/// let simple_topic = TopicBuilder::new("simple-topic")
59///     .build(&mut stack_builder);
60/// 
61/// let function = unimplemented!("create a function");
62///
63/// // Create a topic with a Lambda subscription
64/// let topic = TopicBuilder::new("my-topic")
65///     .add_subscription(SubscriptionType::Lambda(&function))
66///     .build(&mut stack_builder);
67///
68/// ```
69pub struct TopicBuilder<T: TopicBuilderState> {
70    state: PhantomData<T>,
71    id: Id,
72    topic_name: Option<String>,
73    content_based_deduplication: Option<bool>,
74    fifo_throughput_scope: Option<FifoThroughputScope>,
75    lambda_subscription_ids: Vec<(Id, String)>,
76}
77
78impl TopicBuilder<StartState> {
79    /// Creates a new SNS topic builder.
80    ///
81    /// # Arguments
82    /// * `id` - Unique identifier for the topic
83    pub fn new(id: &str) -> Self {
84        Self {
85            state: Default::default(),
86            id: Id(id.to_string()),
87            topic_name: None,
88            content_based_deduplication: None,
89            fifo_throughput_scope: None,
90            lambda_subscription_ids: vec![],
91        }
92    }
93
94    /// Adds a subscription to the topic.
95    ///
96    /// For Lambda subscriptions, automatically creates the necessary permission.
97    pub fn add_subscription(mut self, subscription: SubscriptionType) -> TopicBuilder<StandardStateWithSubscriptions> {
98        self.add_subscription_internal(subscription);
99
100        TopicBuilder {
101            state: Default::default(),
102            id: self.id,
103            topic_name: self.topic_name,
104            content_based_deduplication: self.content_based_deduplication,
105            fifo_throughput_scope: self.fifo_throughput_scope,
106            lambda_subscription_ids: self.lambda_subscription_ids,
107        }
108    }
109
110    pub fn build(self, stack_builder: &mut StackBuilder) -> TopicRef {
111        self.build_internal(false, stack_builder)
112    }
113}
114
115impl TopicBuilder<StandardStateWithSubscriptions> {
116    pub fn add_subscription(mut self, subscription: SubscriptionType) -> TopicBuilder<StandardStateWithSubscriptions> {
117        self.add_subscription_internal(subscription);
118
119        TopicBuilder {
120            state: Default::default(),
121            id: self.id,
122            topic_name: self.topic_name,
123            content_based_deduplication: self.content_based_deduplication,
124            fifo_throughput_scope: self.fifo_throughput_scope,
125            lambda_subscription_ids: self.lambda_subscription_ids,
126        }
127    }
128
129    pub fn build(self, stack_builder: &mut StackBuilder) -> TopicRef {
130        self.build_internal(false, stack_builder)
131    }
132}
133
134impl<T: TopicBuilderState> TopicBuilder<T> {
135    pub fn topic_name(self, topic_name: StringWithOnlyAlphaNumericsUnderscoresAndHyphens) -> TopicBuilder<T> {
136        TopicBuilder {
137            topic_name: Some(topic_name.0),
138            id: self.id,
139            state: Default::default(),
140            content_based_deduplication: self.content_based_deduplication,
141            fifo_throughput_scope: self.fifo_throughput_scope,
142            lambda_subscription_ids: self.lambda_subscription_ids,
143        }
144    }
145
146    pub fn fifo(self) -> TopicBuilder<FifoState> {
147        TopicBuilder {
148            state: Default::default(),
149            id: self.id,
150            topic_name: self.topic_name,
151            content_based_deduplication: self.content_based_deduplication,
152            fifo_throughput_scope: self.fifo_throughput_scope,
153            lambda_subscription_ids: self.lambda_subscription_ids,
154        }
155    }
156    
157    fn add_subscription_internal(&mut self, subscription: SubscriptionType) {
158        match subscription {
159            SubscriptionType::Lambda(l) => self.lambda_subscription_ids.push((l.get_id().clone(), l.get_resource_id().to_string()))
160        };
161    }
162    
163    fn build_internal(self, fifo: bool, stack_builder: &mut StackBuilder) -> TopicRef {
164        let topic_resource_id = Resource::generate_id("SnsTopic");
165        
166        self.lambda_subscription_ids.iter().for_each(|(to_subscribe_id, to_subscribe_resource_id)| {
167            let subscription_id = Id::combine_ids(&self.id, to_subscribe_id);
168            let subscription_resource_id = Resource::generate_id("SnsSubscription");
169            
170            PermissionBuilder::new(&Id::generate_id(&subscription_id, "Permission"), LambdaPermissionAction("lambda:InvokeFunction".to_string()), get_arn(to_subscribe_resource_id), "sns.amazonaws.com")
171                .source_arn(get_ref(&topic_resource_id))
172                .build(stack_builder);
173
174            let subscription = Subscription {
175                id: subscription_id,
176                resource_id: subscription_resource_id,
177                r#type: "AWS::SNS::Subscription".to_string(),
178                properties: SnsSubscriptionProperties {
179                    protocol: "lambda".to_string(),
180                    endpoint: get_arn(to_subscribe_resource_id),
181                    topic_arn: get_ref(&topic_resource_id),
182                },
183            };
184
185            stack_builder.add_resource(subscription);
186        });
187        
188        let properties = TopicProperties {
189            topic_name: self.topic_name,
190            fifo_topic: Some(fifo),
191            content_based_deduplication: self.content_based_deduplication,
192            fifo_throughput_scope: self.fifo_throughput_scope.map(Into::into),
193        };
194        
195        stack_builder.add_resource(Topic {
196            id: self.id,
197            resource_id: topic_resource_id.to_string(),
198            r#type: "AWS::SNS::Topic".to_string(),
199            properties,
200        });
201
202        TopicRef::new(topic_resource_id)
203    }
204}
205
206impl TopicBuilder<FifoState> {
207    pub fn fifo_throughput_scope(self, scope: FifoThroughputScope) -> TopicBuilder<FifoState> {
208        Self {
209            fifo_throughput_scope: Some(scope),
210            ..self
211        }
212    }
213
214    pub fn content_based_deduplication(self, content_based_deduplication: bool) -> TopicBuilder<FifoState> {
215        Self {
216            content_based_deduplication: Some(content_based_deduplication),
217            ..self
218        }
219    }
220
221    pub fn add_subscription(mut self, subscription: SubscriptionType) -> TopicBuilder<FifoStateWithSubscriptions> {
222        self.add_subscription_internal(subscription);
223
224        TopicBuilder {
225            state: Default::default(),
226            id: self.id,
227            topic_name: self.topic_name,
228            content_based_deduplication: self.content_based_deduplication,
229            fifo_throughput_scope: self.fifo_throughput_scope,
230            lambda_subscription_ids: self.lambda_subscription_ids,
231        }
232    }
233
234    /// Builds the FIFO topic and adds it to the stack.
235    ///
236    /// Automatically appends the required ".fifo" suffix to the topic name if not already present.
237    pub fn build(mut self, stack_builder: &mut StackBuilder) -> TopicRef {
238        if let Some(ref name) = self.topic_name
239            && !name.ends_with(FIFO_SUFFIX) {
240                self.topic_name = Some(format!("{}{}", name, FIFO_SUFFIX));
241            }
242        self.build_internal(true, stack_builder)
243    }
244}
245
246impl TopicBuilder<FifoStateWithSubscriptions> {
247    pub fn fifo_throughput_scope(self, scope: FifoThroughputScope) -> TopicBuilder<FifoStateWithSubscriptions> {
248        Self {
249            fifo_throughput_scope: Some(scope),
250            ..self
251        }
252    }
253
254    pub fn content_based_deduplication(self, content_based_deduplication: bool) -> TopicBuilder<FifoStateWithSubscriptions> {
255        Self {
256            content_based_deduplication: Some(content_based_deduplication),
257            ..self
258        }
259    }
260
261    pub fn add_subscription(mut self, subscription: SubscriptionType) -> TopicBuilder<FifoStateWithSubscriptions> {
262        self.add_subscription_internal(subscription);
263
264        TopicBuilder {
265            state: Default::default(),
266            id: self.id,
267            topic_name: self.topic_name,
268            content_based_deduplication: self.content_based_deduplication,
269            fifo_throughput_scope: self.fifo_throughput_scope,
270            lambda_subscription_ids: self.lambda_subscription_ids,
271        }
272    }
273    
274    /// Builds the FIFO topic with subscriptions and adds it to the stack.
275    ///
276    /// Automatically appends the required ".fifo" suffix to the topic name if not already present.
277    /// Creates Lambda permissions for all subscriptions.
278    pub fn build(mut self, stack_builder: &mut StackBuilder) -> TopicRef {
279        if let Some(ref name) = self.topic_name
280            && !name.ends_with(FIFO_SUFFIX) {
281                self.topic_name = Some(format!("{}{}", name, FIFO_SUFFIX));
282            }
283        self.build_internal(true, stack_builder)
284    }
285}
286
287pub struct TopicPolicyBuilder {
288    id: Id,
289    doc: PolicyDocument,
290    topics: Vec<Value>
291}
292
293impl TopicPolicyBuilder {
294    // could help user by setting resource and condition
295
296    /// Creates a new SNS topic policy builder.
297    ///
298    /// *Important* Current limitation: CloudFormation only allows one resource policy for a given topic, applying the last one it receives.
299    /// If you've added a bucket notification for this topic, which requires a policy, and you also define one yourself, one of both will get lost.
300    ///
301    /// # Arguments
302    /// * `id` - Unique identifier for the topic
303    /// * `doc` - The resource policy that should be applied to the topics
304    /// * `topics` - Topics for which the policy is valid
305    pub fn new(id: &str, doc: PolicyDocument, topics: Vec<&TopicRef>) -> Self {
306        Self::new_with_values(id, doc, topics.into_iter().map(|v| v.get_ref()).collect())
307    }
308    
309    pub(crate) fn new_with_values(id: &str, doc: PolicyDocument, topics: Vec<Value>) -> Self {
310        Self {
311            id: Id(id.to_string()),
312            doc,
313            topics,
314        }
315    }
316    
317    pub fn build(self, stack_builder: &mut StackBuilder) -> TopicPolicyRef {
318        let resource_id = Resource::generate_id("TopicPolicy");
319        stack_builder.add_resource(TopicPolicy {
320            id: self.id.clone(),
321            resource_id: resource_id.clone(),
322            r#type: "AWS::SNS::TopicPolicy".to_string(),
323            properties: TopicPolicyProperties {
324                doc: self.doc,
325                topics: self.topics,
326            },
327        });
328        
329        TopicPolicyRef::new(self.id, resource_id)
330    }
331}
332