lambda_simulator/
invocation.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Invocation {
11 pub request_id: String,
13
14 pub payload: Value,
16
17 pub created_at: DateTime<Utc>,
19
20 pub deadline: DateTime<Utc>,
22
23 pub aws_request_id: String,
25
26 pub invoked_function_arn: String,
28
29 pub trace_id: String,
31
32 pub client_context: Option<String>,
34
35 pub cognito_identity: Option<String>,
37}
38
39impl Invocation {
40 pub fn new(payload: Value, timeout_ms: u64) -> Self {
61 let request_id = Uuid::new_v4().to_string();
62 let created_at = Utc::now();
63 let deadline = created_at + chrono::Duration::milliseconds(timeout_ms as i64);
64
65 let trace_id = Self::generate_trace_id(created_at);
66
67 Self {
68 request_id: request_id.clone(),
69 payload,
70 created_at,
71 deadline,
72 aws_request_id: request_id.clone(),
73 invoked_function_arn: "arn:aws:lambda:us-east-1:123456789012:function:test-function"
74 .to_string(),
75 trace_id,
76 client_context: None,
77 cognito_identity: None,
78 }
79 }
80
81 fn generate_trace_id(timestamp: DateTime<Utc>) -> String {
86 let epoch_time = timestamp.timestamp() as u32;
87 let random_bytes = Uuid::new_v4();
88 let random_hex = format!("{:032x}", random_bytes.as_u128())
89 .chars()
90 .take(24)
91 .collect::<String>();
92
93 format!("Root=1-{:08x}-{}", epoch_time, random_hex)
94 }
95
96 pub fn deadline_ms(&self) -> i64 {
100 self.deadline.timestamp_millis()
101 }
102}
103
104#[derive(Debug, Default)]
106#[must_use = "builders do nothing unless .build() is called"]
107pub struct InvocationBuilder {
108 payload: Option<Value>,
109 timeout_ms: Option<u64>,
110 function_arn: Option<String>,
111 client_context: Option<String>,
112 cognito_identity: Option<String>,
113}
114
115impl InvocationBuilder {
116 pub fn new() -> Self {
129 Self::default()
130 }
131
132 pub fn payload(mut self, payload: Value) -> Self {
134 self.payload = Some(payload);
135 self
136 }
137
138 pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
140 self.timeout_ms = Some(timeout_ms);
141 self
142 }
143
144 pub fn function_arn(mut self, arn: impl Into<String>) -> Self {
146 self.function_arn = Some(arn.into());
147 self
148 }
149
150 pub fn client_context(mut self, context: impl Into<String>) -> Self {
152 self.client_context = Some(context.into());
153 self
154 }
155
156 pub fn cognito_identity(mut self, identity: impl Into<String>) -> Self {
158 self.cognito_identity = Some(identity.into());
159 self
160 }
161
162 pub fn build(self) -> Result<Invocation, crate::error::BuilderError> {
180 let payload = self
181 .payload
182 .ok_or_else(|| crate::error::BuilderError::MissingField("payload".to_string()))?;
183 let timeout_ms = self.timeout_ms.unwrap_or(3000);
184
185 let mut invocation = Invocation::new(payload, timeout_ms);
186
187 if let Some(arn) = self.function_arn {
188 invocation.invoked_function_arn = arn;
189 }
190
191 if let Some(context) = self.client_context {
192 invocation.client_context = Some(context);
193 }
194
195 if let Some(identity) = self.cognito_identity {
196 invocation.cognito_identity = Some(identity);
197 }
198
199 Ok(invocation)
200 }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub enum InvocationStatus {
206 Pending,
208
209 InProgress,
211
212 Success,
214
215 Error,
217
218 Timeout,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct InvocationResponse {
225 pub request_id: String,
227
228 pub payload: Value,
230
231 pub received_at: DateTime<Utc>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct InvocationError {
238 pub request_id: String,
240
241 pub error_type: String,
243
244 pub error_message: String,
246
247 pub stack_trace: Option<Vec<String>>,
249
250 pub received_at: DateTime<Utc>,
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use proptest::prelude::*;
258
259 proptest! {
260 #[test]
261 fn invocation_new_produces_valid_ids(timeout_ms in 1u64..=900000u64) {
262 let invocation = Invocation::new(serde_json::json!({}), timeout_ms);
263
264 prop_assert!(!invocation.request_id.is_empty());
265 prop_assert!(!invocation.aws_request_id.is_empty());
266 prop_assert_eq!(invocation.request_id, invocation.aws_request_id);
267 }
268
269 #[test]
270 fn invocation_deadline_is_in_future(timeout_ms in 1u64..=900000u64) {
271 let invocation = Invocation::new(serde_json::json!({}), timeout_ms);
272
273 prop_assert!(invocation.deadline > invocation.created_at);
274 prop_assert!(invocation.deadline_ms() > invocation.created_at.timestamp_millis());
275 }
276
277 #[test]
278 fn invocation_trace_id_format_is_valid(timeout_ms in 1u64..=900000u64) {
279 let invocation = Invocation::new(serde_json::json!({}), timeout_ms);
280
281 prop_assert!(invocation.trace_id.starts_with("Root=1-"));
282 let parts: Vec<_> = invocation.trace_id.split('-').collect();
283 prop_assert_eq!(parts.len(), 3);
284 prop_assert_eq!(parts[1].len(), 8);
285 prop_assert_eq!(parts[2].len(), 24);
286 }
287
288 #[test]
289 fn builder_produces_same_result_as_new(timeout_ms in 1u64..=900000u64) {
290 let payload = serde_json::json!({"test": true});
291
292 let from_builder = InvocationBuilder::new()
293 .payload(payload.clone())
294 .timeout_ms(timeout_ms)
295 .build()
296 .unwrap();
297
298 prop_assert!(!from_builder.request_id.is_empty());
299 prop_assert_eq!(from_builder.payload, payload);
300 }
301
302 #[test]
303 fn builder_without_payload_fails(_timeout_ms in 1u64..=900000u64) {
304 let result = InvocationBuilder::new().build();
305 prop_assert!(result.is_err());
306 }
307 }
308}