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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum LambdaEvent {
30    /// HTTP event (API Gateway REST, HTTP API, Function URL, ALB).
31    Http,
32    /// SQS trigger event from an event source mapping.
33    Sqs,
34    /// Unrecognised shape (DynamoDB stream, EventBridge scheduler,
35    /// custom invocation payloads, etc.). Adopter-owned.
36    Unknown,
37}
38
39/// Classify a raw Lambda event JSON shape.
40///
41/// Heuristic:
42/// - SQS event JSON has a top-level `"Records"` array whose entries
43///   contain `"eventSource": "aws:sqs"`.
44/// - HTTP event JSON has one of `httpMethod`,
45///   `requestContext.http.method`, or `routeKey`.
46/// - Anything else is [`LambdaEvent::Unknown`].
47///
48/// Adopter usage for a composite Lambda (HTTP + SQS + EventBridge
49/// nightly schedule):
50///
51/// ```ignore
52/// lambda_runtime::run(lambda_runtime::service_fn(
53///     move |event: lambda_runtime::LambdaEvent<serde_json::Value>| {
54///         let handler = handler.clone();
55///         async move {
56///             let (value, _ctx) = event.into_parts();
57///             match turul_a2a_aws_lambda::classify_event(&value) {
58///                 turul_a2a_aws_lambda::LambdaEvent::Http => {
59///                     handler.handle_http_event_value(value).await
60///                 }
61///                 turul_a2a_aws_lambda::LambdaEvent::Sqs => {
62///                     let sqs = serde_json::from_value(value)?;
63///                     Ok(serde_json::to_value(handler.handle_sqs(sqs).await)?)
64///                 }
65///                 turul_a2a_aws_lambda::LambdaEvent::Unknown => {
66///                     // Adopter-owned — the framework does not know
67///                     // what this event means.
68///                     run_nightly_audit(/* adopter context */).await
69///                 }
70///             }
71///         }
72///     }
73/// ))
74/// ```
75pub fn classify_event(event: &serde_json::Value) -> LambdaEvent {
76    if is_sqs_event_shape(event) {
77        return LambdaEvent::Sqs;
78    }
79    if is_http_event_shape(event) {
80        return LambdaEvent::Http;
81    }
82    LambdaEvent::Unknown
83}
84
85fn is_sqs_event_shape(event: &serde_json::Value) -> bool {
86    event
87        .get("Records")
88        .and_then(|r| r.as_array())
89        .and_then(|arr| arr.first())
90        .and_then(|rec| rec.get("eventSource"))
91        .and_then(|s| s.as_str())
92        .map(|s| s == "aws:sqs")
93        .unwrap_or(false)
94}
95
96fn is_http_event_shape(event: &serde_json::Value) -> bool {
97    event.get("httpMethod").is_some()
98        || event
99            .get("requestContext")
100            .is_some_and(|req_ctx| req_ctx.pointer("/http/method").is_some())
101        || event.get("routeKey").is_some()
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn classify_apigw_v2_http_event() {
110        let v = serde_json::json!({
111            "version": "2.0",
112            "routeKey": "GET /.well-known/agent-card.json",
113            "requestContext": {
114                "http": {"method": "GET", "path": "/.well-known/agent-card.json"}
115            }
116        });
117        assert_eq!(classify_event(&v), LambdaEvent::Http);
118    }
119
120    #[test]
121    fn classify_apigw_v1_http_event() {
122        let v = serde_json::json!({
123            "httpMethod": "POST",
124            "path": "/message:send",
125            "resource": "/{proxy+}"
126        });
127        assert_eq!(classify_event(&v), LambdaEvent::Http);
128    }
129
130    #[test]
131    fn classify_sqs_event() {
132        let v = serde_json::json!({
133            "Records": [
134                {"eventSource": "aws:sqs", "messageId": "m1", "body": "{}"}
135            ]
136        });
137        assert_eq!(classify_event(&v), LambdaEvent::Sqs);
138    }
139
140    #[test]
141    fn classify_eventbridge_scheduled_is_unknown() {
142        let v = serde_json::json!({
143            "source": "aws.events",
144            "detail-type": "Scheduled Event",
145            "detail": {}
146        });
147        assert_eq!(classify_event(&v), LambdaEvent::Unknown);
148    }
149
150    #[test]
151    fn classify_dynamodb_stream_is_unknown_not_sqs() {
152        let v = serde_json::json!({
153            "Records": [
154                {"eventSource": "aws:dynamodb", "dynamodb": {}}
155            ]
156        });
157        assert_eq!(classify_event(&v), LambdaEvent::Unknown);
158    }
159
160    #[test]
161    fn classify_unknown_payload() {
162        let v = serde_json::json!({"custom": "invoke"});
163        assert_eq!(classify_event(&v), LambdaEvent::Unknown);
164    }
165}