rusty_cdk_core/apigateway/
builder.rs

1use crate::apigateway::{ApiGatewayV2Api, ApiGatewayV2ApiProperties, ApiGatewayV2ApiRef, ApiGatewayV2Integration, ApiGatewayV2IntegrationProperties, ApiGatewayV2Route, ApiGatewayV2RouteProperties, ApiGatewayV2Stage, ApiGatewayV2StageProperties, ApiGatewayV2StageRef, CorsConfiguration};
2use crate::intrinsic::{get_arn, get_ref, join, AWS_ACCOUNT_PSEUDO_PARAM, AWS_PARTITION_PSEUDO_PARAM, AWS_REGION_PSEUDO_PARAM};
3use crate::lambda::{FunctionRef, PermissionBuilder};
4use crate::shared::http::HttpMethod;
5use crate::shared::Id;
6use crate::stack::{Resource, StackBuilder};
7use serde_json::Value;
8use std::time::Duration;
9use crate::wrappers::LambdaPermissionAction;
10
11// most of the websocket stuff left out, some things specific to http (cors), others for websocket (RouteSelectionExpression)
12// auth also still to do
13
14// TODO api keys (and should check existence of the key? and actually value also has to be unique, but is it ok to test that?)
15
16struct RouteInfo {
17    lambda_id: Id,
18    path: String,
19    method: Option<HttpMethod>,
20    resource_id: String,
21}
22
23/// Builder for API Gateway V2 HTTP APIs.
24///
25/// Creates an HTTP API with routes to Lambda functions. Automatically creates integrations and permissions for each route.
26///
27/// # Example
28///
29/// ```rust,no_run
30/// use rusty_cdk_core::stack::StackBuilder;
31/// use rusty_cdk_core::apigateway::ApiGatewayV2Builder;
32/// use rusty_cdk_core::shared::http::HttpMethod;
33/// use rusty_cdk_core::lambda::{FunctionBuilder, Architecture, Runtime, Zip};
34/// use rusty_cdk_core::wrappers::*;
35/// use rusty_cdk_macros::{memory, timeout, zip_file};
36///
37/// let mut stack_builder = StackBuilder::new();
38///
39/// let function = unimplemented!("create a function");
40///
41/// let (api, stage) = ApiGatewayV2Builder::new("my-api", "MyHttpApi")
42///     .add_route_lambda("/hello", HttpMethod::Get, &function)
43///     .add_route_lambda("/world", HttpMethod::Post, &function)
44///     .build(&mut stack_builder);
45/// ```
46pub struct ApiGatewayV2Builder {
47    id: Id,
48    name: Option<String>,
49    disable_execute_api_endpoint: Option<bool>,
50    cors_configuration: Option<CorsConfiguration>,
51    route_info: Vec<RouteInfo>,
52}
53
54impl ApiGatewayV2Builder {
55    /// Creates a new API Gateway V2 HTTP API builder.
56    ///
57    /// # Arguments
58    /// * `id` - Unique identifier for the API Gateway
59    /// * `name` - Name of the API Gateway
60    pub fn new<T: Into<String>>(id: &str, name: T) -> Self {
61        Self {
62            id: Id(id.to_string()),
63            name: Some(name.into()), // TODO name is required when not OpenAPI (so currently always)
64            disable_execute_api_endpoint: None,
65            cors_configuration: None,
66            route_info: vec![],
67        }
68    }
69
70    pub fn disable_execute_api_endpoint(self, disable_api_endpoint: bool) -> Self {
71        Self {
72            disable_execute_api_endpoint: Some(disable_api_endpoint),
73            ..self
74        }
75    }
76
77    pub fn cors_configuration(self, config: CorsConfiguration) -> Self {
78        Self {
79            cors_configuration: Some(config),
80            ..self
81        }
82    }
83
84    /// Adds a default route that catches all requests not matching other routes.
85    ///
86    /// Automatically creates the integration and Lambda permission.
87    pub fn add_default_route_lambda(mut self, lambda: &FunctionRef) -> Self {
88        self.route_info.push(RouteInfo {
89            lambda_id: lambda.get_id().clone(),
90            path: "$default".to_string(),
91            method: None,
92            resource_id: lambda.get_resource_id().to_string(),
93        });
94        Self { ..self }
95    }
96
97    /// Adds a route for a specific HTTP method and path.
98    ///
99    /// Automatically creates the integration and Lambda permission.
100    pub fn add_route_lambda<T: Into<String>>(mut self, path: T, method: HttpMethod, lambda: &FunctionRef) -> Self {
101        let path = path.into();
102        let path = if path.starts_with("/") { path } else { format!("/{}", path) };
103
104        self.route_info.push(RouteInfo {
105            lambda_id: lambda.get_id().clone(),
106            path,
107            method: Some(method),
108            resource_id: lambda.get_resource_id().to_string(),
109        });
110        Self { ..self }
111    }
112
113    pub fn build(
114        self, stack_builder: &mut StackBuilder
115    ) -> (
116        ApiGatewayV2ApiRef,
117        ApiGatewayV2StageRef,
118    ) {
119        let api_resource_id = Resource::generate_id("HttpApiGateway");
120        let stage_resource_id = Resource::generate_id("HttpApiStage");
121        let stage_id = Id::generate_id(&self.id, "Stage");
122
123        self
124            .route_info
125            .into_iter()
126            .for_each(|info| {
127                let route_id = Id::combine_with_resource_id(&self.id, &info.lambda_id);
128                let route_permission_id = Id::generate_id(&self.id, "Permission");
129                let route_integration_id = Id::generate_id(&self.id, "Integration");
130
131                let integration_resource_id = Resource::generate_id("HttpApiIntegration");
132                let route_resource_id = Resource::generate_id("HttpApiRoute");
133
134                PermissionBuilder::new(
135                    &route_permission_id,
136                    LambdaPermissionAction("lambda:InvokeFunction".to_string()),
137                    get_arn(&info.resource_id),
138                    "apigateway.amazonaws.com".to_string(),
139                )
140                .source_arn(join(
141                    "",
142                    vec![
143                        Value::String("arn:".to_string()),
144                        get_ref(AWS_PARTITION_PSEUDO_PARAM),
145                        Value::String(":execute-api:".to_string()),
146                        get_ref(AWS_REGION_PSEUDO_PARAM),
147                        Value::String(":".to_string()),
148                        get_ref(AWS_ACCOUNT_PSEUDO_PARAM),
149                        Value::String(":".to_string()),
150                        get_ref(&api_resource_id),
151                        Value::String(format!("*/*{}", info.path)),
152                    ],
153                ))
154                .build(stack_builder);
155
156                let integration = ApiGatewayV2Integration {
157                    id: route_integration_id,
158                    resource_id: integration_resource_id.clone(),
159                    r#type: "AWS::ApiGatewayV2::Integration".to_string(),
160                    properties: ApiGatewayV2IntegrationProperties {
161                        api_id: get_ref(&api_resource_id),
162                        integration_type: "AWS_PROXY".to_string(),
163                        payload_format_version: Some("2.0".to_string()),
164                        integration_uri: Some(get_arn(&info.resource_id)),
165                        integration_method: None,
166                        passthrough_behavior: None,
167                        request_parameters: None,
168                        request_templates: None,
169                        response_parameters: None,
170                        timeout_in_millis: None,
171                    },
172                };
173                stack_builder.add_resource(integration);
174
175                let route_key = if let Some(method) = info.method {
176                    let method: String = method.into();
177                    format!("{} {}", method, info.path)
178                } else {
179                    info.path
180                };
181
182                let route = ApiGatewayV2Route {
183                    id: route_id,
184                    resource_id: route_resource_id.clone(),
185                    r#type: "AWS::ApiGatewayV2::Route".to_string(),
186                    properties: ApiGatewayV2RouteProperties {
187                        api_id: get_ref(&api_resource_id),
188                        route_key,
189                        target: Some(join(
190                            "",
191                            vec![Value::String("integrations/".to_string()), get_ref(&integration_resource_id)],
192                        )),
193                    },
194                };
195                stack_builder.add_resource(route);
196            });
197        
198        stack_builder.add_resource(ApiGatewayV2Stage {
199            id: stage_id,
200            resource_id: stage_resource_id.clone(),
201            r#type: "AWS::ApiGatewayV2::Stage".to_string(),
202            properties: ApiGatewayV2StageProperties {
203                api_id: get_ref(&api_resource_id),
204                stage_name: "$default".to_string(),
205                auto_deploy: true,
206                default_route_settings: None,
207                route_settings: None,
208            },
209        });
210
211        stack_builder.add_resource(ApiGatewayV2Api {
212            id: self.id,
213            resource_id: api_resource_id.clone(),
214            r#type: "AWS::ApiGatewayV2::Api".to_string(),
215            properties: ApiGatewayV2ApiProperties {
216                name: self.name,
217                protocol_type: "HTTP".to_string(),
218                disable_execute_api_endpoint: self.disable_execute_api_endpoint,
219                cors_configuration: self.cors_configuration,
220            },
221        });
222
223        let stage = ApiGatewayV2StageRef::new(stage_resource_id);
224        let api = ApiGatewayV2ApiRef::new(api_resource_id);
225
226        (api, stage)
227    }
228}
229
230pub struct CorsConfigurationBuilder {
231    allow_credentials: Option<bool>,
232    allow_headers: Option<Vec<String>>,
233    allow_methods: Option<Vec<String>>,
234    allow_origins: Option<Vec<String>>,
235    expose_headers: Option<Vec<String>>,
236    max_age: Option<u64>,
237}
238
239impl Default for CorsConfigurationBuilder {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245impl CorsConfigurationBuilder {
246    pub fn new() -> Self {
247        Self {
248            allow_credentials: None,
249            allow_headers: None,
250            allow_methods: None,
251            allow_origins: None,
252            expose_headers: None,
253            max_age: None,
254        }
255    }
256
257    pub fn allow_credentials(self, allow: bool) -> Self {
258        Self {
259            allow_credentials: Some(allow),
260            ..self
261        }
262    }
263
264    pub fn allow_headers(self, headers: Vec<String>) -> Self {
265        Self {
266            allow_headers: Some(headers),
267            ..self
268        }
269    }
270
271    pub fn allow_methods(self, methods: Vec<HttpMethod>) -> Self {
272        Self {
273            allow_methods: Some(methods.into_iter().map(Into::into).collect()),
274            ..self
275        }
276    }
277
278    pub fn allow_origins(self, origins: Vec<String>) -> Self {
279        Self {
280            allow_origins: Some(origins),
281            ..self
282        }
283    }
284
285    pub fn expose_headers(self, headers: Vec<String>) -> Self {
286        Self {
287            expose_headers: Some(headers),
288            ..self
289        }
290    }
291
292    pub fn max_age(self, age: Duration) -> Self {
293        Self {
294            max_age: Some(age.as_secs()),
295            ..self
296        }
297    }
298
299    #[must_use]
300    pub fn build(self) -> CorsConfiguration {
301        CorsConfiguration {
302            allow_credentials: self.allow_credentials,
303            allow_headers: self.allow_headers,
304            allow_methods: self.allow_methods,
305            allow_origins: self.allow_origins,
306            expose_headers: self.expose_headers,
307            max_age: self.max_age,
308        }
309    }
310}