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}