lambda_simulator/
invocation.rs

1//! Invocation lifecycle management and data structures.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use uuid::Uuid;
7
8/// Represents a Lambda invocation request.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Invocation {
11    /// Unique identifier for this invocation.
12    pub request_id: String,
13
14    /// The invocation payload (function input).
15    pub payload: Value,
16
17    /// Timestamp when the invocation was created.
18    pub created_at: DateTime<Utc>,
19
20    /// Deadline by which the invocation must complete.
21    pub deadline: DateTime<Utc>,
22
23    /// AWS request ID (for X-Ray tracing).
24    pub aws_request_id: String,
25
26    /// ARN of the function being invoked.
27    pub invoked_function_arn: String,
28
29    /// AWS X-Ray trace ID.
30    pub trace_id: String,
31
32    /// Client context (for mobile SDK).
33    pub client_context: Option<String>,
34
35    /// Cognito identity (for mobile SDK).
36    pub cognito_identity: Option<String>,
37}
38
39impl Invocation {
40    /// Creates a new invocation with default values.
41    ///
42    /// # Arguments
43    ///
44    /// * `payload` - The JSON payload for this invocation
45    /// * `timeout_ms` - Timeout in milliseconds for this invocation
46    ///
47    /// # Returns
48    ///
49    /// A new `Invocation` instance with generated IDs and timestamps.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use serde_json::json;
55    /// use lambda_simulator::invocation::Invocation;
56    ///
57    /// let invocation = Invocation::new(json!({"key": "value"}), 3000);
58    /// assert!(!invocation.request_id.is_empty());
59    /// ```
60    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    /// Generates an AWS X-Ray trace ID in the correct format.
82    ///
83    /// Format: Root=1-{8-hex-time}-{24-hex-random}
84    /// Where time is Unix timestamp and random is 96 random bits.
85    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    /// Returns the deadline as milliseconds since Unix epoch.
97    ///
98    /// This is used for the `Lambda-Runtime-Deadline-Ms` header.
99    pub fn deadline_ms(&self) -> i64 {
100        self.deadline.timestamp_millis()
101    }
102}
103
104/// Builder for creating invocations with custom properties.
105#[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    /// Creates a new invocation builder.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use serde_json::json;
122    /// use lambda_simulator::invocation::InvocationBuilder;
123    ///
124    /// let invocation = InvocationBuilder::new()
125    ///     .payload(json!({"key": "value"}))
126    ///     .build();
127    /// ```
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Sets the payload for the invocation.
133    pub fn payload(mut self, payload: Value) -> Self {
134        self.payload = Some(payload);
135        self
136    }
137
138    /// Sets the timeout in milliseconds.
139    pub fn timeout_ms(mut self, timeout_ms: u64) -> Self {
140        self.timeout_ms = Some(timeout_ms);
141        self
142    }
143
144    /// Sets the function ARN.
145    pub fn function_arn(mut self, arn: impl Into<String>) -> Self {
146        self.function_arn = Some(arn.into());
147        self
148    }
149
150    /// Sets the client context.
151    pub fn client_context(mut self, context: impl Into<String>) -> Self {
152        self.client_context = Some(context.into());
153        self
154    }
155
156    /// Sets the Cognito identity.
157    pub fn cognito_identity(mut self, identity: impl Into<String>) -> Self {
158        self.cognito_identity = Some(identity.into());
159        self
160    }
161
162    /// Builds the invocation.
163    ///
164    /// # Errors
165    ///
166    /// Returns `BuilderError::MissingField` if no payload was provided.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use lambda_simulator::InvocationBuilder;
172    /// use serde_json::json;
173    ///
174    /// let invocation = InvocationBuilder::new()
175    ///     .payload(json!({"key": "value"}))
176    ///     .build()
177    ///     .expect("Failed to build invocation");
178    /// ```
179    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/// The status of an invocation.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
205pub enum InvocationStatus {
206    /// Invocation is queued and waiting to be processed.
207    Pending,
208
209    /// Invocation has been sent to the runtime.
210    InProgress,
211
212    /// Invocation completed successfully.
213    Success,
214
215    /// Invocation failed with an error.
216    Error,
217
218    /// Invocation timed out.
219    Timeout,
220}
221
222/// Response from a successful invocation.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct InvocationResponse {
225    /// The request ID this response is for.
226    pub request_id: String,
227
228    /// The response payload.
229    pub payload: Value,
230
231    /// Timestamp when the response was received.
232    pub received_at: DateTime<Utc>,
233}
234
235/// Error response from a failed invocation.
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct InvocationError {
238    /// The request ID this error is for.
239    pub request_id: String,
240
241    /// Error type.
242    pub error_type: String,
243
244    /// Error message.
245    pub error_message: String,
246
247    /// Stack trace if available.
248    pub stack_trace: Option<Vec<String>>,
249
250    /// Timestamp when the error was received.
251    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}