Skip to main content

turul_a2a_aws_lambda/
event.rs

1//! Event-shape classification for single-function Lambdas that
2//! receive more than one trigger type.
3//!
4//! Lambda invokes the binary with a raw JSON event; AWS does not
5//! surface the trigger type as a separate parameter. When a single
6//! function is wired to multiple event sources (HTTP plus SQS, HTTP
7//! plus EventBridge, HTTP plus DynamoDB streams, …) the binary has
8//! to classify the event shape up front and route accordingly.
9//!
10//! [`classify_event`] and [`LambdaEvent`] are the framework's
11//! classifier. They are available without the `sqs` feature so
12//! adopters with HTTP + a non-SQS third trigger (e.g. EventBridge
13//! Scheduler) can route in their own `main.rs` without re-deriving
14//! the shape heuristics.
15
16/// Coarse Lambda event-shape discriminator returned by
17/// [`classify_event`].
18///
19/// Adopters pattern-match on this in their own
20/// `lambda_runtime::run<serde_json::Value>` service function and
21/// dispatch each arm. The framework's own [`LambdaA2aHandler::run`],
22/// [`LambdaA2aHandler::run_http_only`],
23/// [`LambdaA2aHandler::run_http_and_sqs`], and
24/// [`LambdaA2aHandler::run_sqs_only`] use this internally.
25///
26/// [`LambdaA2aHandler::run`]: crate::LambdaA2aHandler::run
27/// [`LambdaA2aHandler::run_http_only`]: crate::LambdaA2aHandler::run_http_only
28/// [`LambdaA2aHandler::run_http_and_sqs`]: crate::LambdaA2aHandler::run_http_and_sqs
29/// [`LambdaA2aHandler::run_sqs_only`]: crate::LambdaA2aHandler::run_sqs_only
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum LambdaEvent {
32    /// HTTP event (API Gateway REST, HTTP API, Function URL, ALB).
33    Http,
34    /// SQS trigger event from an event source mapping.
35    Sqs,
36    /// Unrecognised shape (DynamoDB stream, EventBridge scheduler,
37    /// custom invocation payloads, etc.). Adopter-owned.
38    Unknown,
39}
40
41/// Classify a raw Lambda event JSON shape.
42///
43/// Heuristic:
44/// - SQS event JSON has a top-level `"Records"` array whose entries
45///   contain `"eventSource": "aws:sqs"`.
46/// - HTTP event JSON has one of `httpMethod`,
47///   `requestContext.http.method`, or `routeKey`.
48/// - Anything else is [`LambdaEvent::Unknown`].
49///
50/// Adopter usage for a composite Lambda (HTTP + SQS + EventBridge
51/// nightly schedule):
52///
53/// ```ignore
54/// lambda_runtime::run(lambda_runtime::service_fn(
55///     move |event: lambda_runtime::LambdaEvent<serde_json::Value>| {
56///         let handler = handler.clone();
57///         async move {
58///             let (value, _ctx) = event.into_parts();
59///             match turul_a2a_aws_lambda::classify_event(&value) {
60///                 turul_a2a_aws_lambda::LambdaEvent::Http => {
61///                     handler.handle_http_event_value(value).await
62///                 }
63///                 turul_a2a_aws_lambda::LambdaEvent::Sqs => {
64///                     let sqs = serde_json::from_value(value)?;
65///                     Ok(serde_json::to_value(handler.handle_sqs(sqs).await)?)
66///                 }
67///                 turul_a2a_aws_lambda::LambdaEvent::Unknown => {
68///                     // Adopter-owned — the framework does not know
69///                     // what this event means.
70///                     run_nightly_audit(/* adopter context */).await
71///                 }
72///             }
73///         }
74///     }
75/// ))
76/// ```
77pub fn classify_event(event: &serde_json::Value) -> LambdaEvent {
78    if is_sqs_event_shape(event) {
79        return LambdaEvent::Sqs;
80    }
81    if is_http_event_shape(event) {
82        return LambdaEvent::Http;
83    }
84    LambdaEvent::Unknown
85}
86
87fn is_sqs_event_shape(event: &serde_json::Value) -> bool {
88    event
89        .get("Records")
90        .and_then(|r| r.as_array())
91        .and_then(|arr| arr.first())
92        .and_then(|rec| rec.get("eventSource"))
93        .and_then(|s| s.as_str())
94        .map(|s| s == "aws:sqs")
95        .unwrap_or(false)
96}
97
98fn is_http_event_shape(event: &serde_json::Value) -> bool {
99    if event.get("httpMethod").is_some() {
100        return true;
101    }
102    if let Some(req_ctx) = event.get("requestContext") {
103        if req_ctx.pointer("/http/method").is_some() {
104            return true;
105        }
106    }
107    if event.get("routeKey").is_some() {
108        return true;
109    }
110    false
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn classify_apigw_v2_http_event() {
119        let v = serde_json::json!({
120            "version": "2.0",
121            "routeKey": "GET /.well-known/agent-card.json",
122            "requestContext": {
123                "http": {"method": "GET", "path": "/.well-known/agent-card.json"}
124            }
125        });
126        assert_eq!(classify_event(&v), LambdaEvent::Http);
127    }
128
129    #[test]
130    fn classify_apigw_v1_http_event() {
131        let v = serde_json::json!({
132            "httpMethod": "POST",
133            "path": "/message:send",
134            "resource": "/{proxy+}"
135        });
136        assert_eq!(classify_event(&v), LambdaEvent::Http);
137    }
138
139    #[test]
140    fn classify_sqs_event() {
141        let v = serde_json::json!({
142            "Records": [
143                {"eventSource": "aws:sqs", "messageId": "m1", "body": "{}"}
144            ]
145        });
146        assert_eq!(classify_event(&v), LambdaEvent::Sqs);
147    }
148
149    #[test]
150    fn classify_eventbridge_scheduled_is_unknown() {
151        let v = serde_json::json!({
152            "source": "aws.events",
153            "detail-type": "Scheduled Event",
154            "detail": {}
155        });
156        assert_eq!(classify_event(&v), LambdaEvent::Unknown);
157    }
158
159    #[test]
160    fn classify_dynamodb_stream_is_unknown_not_sqs() {
161        let v = serde_json::json!({
162            "Records": [
163                {"eventSource": "aws:dynamodb", "dynamodb": {}}
164            ]
165        });
166        assert_eq!(classify_event(&v), LambdaEvent::Unknown);
167    }
168
169    #[test]
170    fn classify_unknown_payload() {
171        let v = serde_json::json!({"custom": "invoke"});
172        assert_eq!(classify_event(&v), LambdaEvent::Unknown);
173    }
174}