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}