rusty_cdk_core/sns/
builder.rs

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