Skip to main content

fakecloud_eventbridge/
service.rs

1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use http::StatusCode;
4use serde_json::{json, Value};
5
6use std::collections::{BTreeMap, HashMap};
7use std::sync::Arc;
8
9use tokio::sync::Mutex as AsyncMutex;
10
11use fakecloud_aws::arn::Arn;
12use fakecloud_core::delivery::DeliveryBus;
13use fakecloud_core::pagination::paginate;
14use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
15use fakecloud_core::validation::*;
16use fakecloud_persistence::SnapshotStore;
17
18use fakecloud_lambda::runtime::ContainerRuntime;
19use fakecloud_lambda::{LambdaInvocation, SharedLambdaState};
20use fakecloud_logs::SharedLogsState;
21
22use crate::state::{
23    ApiDestination, Archive, Connection, Endpoint, EventBridgeSnapshot, EventBridgeState, EventBus,
24    EventRule, EventTarget, PartnerEventSource, PutEvent, Replay, SharedEventBridgeState,
25    EVENTBRIDGE_SNAPSHOT_SCHEMA_VERSION,
26};
27
28pub struct EventBridgeService {
29    state: SharedEventBridgeState,
30    delivery: Arc<DeliveryBus>,
31    lambda_state: Option<SharedLambdaState>,
32    logs_state: Option<SharedLogsState>,
33    container_runtime: Option<Arc<ContainerRuntime>>,
34    snapshot_store: Option<Arc<dyn SnapshotStore>>,
35    snapshot_lock: Arc<AsyncMutex<()>>,
36}
37
38impl EventBridgeService {
39    pub fn new(state: SharedEventBridgeState, delivery: Arc<DeliveryBus>) -> Self {
40        Self {
41            state,
42            delivery,
43            lambda_state: None,
44            logs_state: None,
45            container_runtime: None,
46            snapshot_store: None,
47            snapshot_lock: Arc::new(AsyncMutex::new(())),
48        }
49    }
50
51    pub fn with_lambda(mut self, lambda_state: SharedLambdaState) -> Self {
52        self.lambda_state = Some(lambda_state);
53        self
54    }
55
56    pub fn with_logs(mut self, logs_state: SharedLogsState) -> Self {
57        self.logs_state = Some(logs_state);
58        self
59    }
60
61    pub fn with_runtime(mut self, runtime: Arc<ContainerRuntime>) -> Self {
62        self.container_runtime = Some(runtime);
63        self
64    }
65
66    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
67        self.snapshot_store = Some(store);
68        self
69    }
70
71    /// Persist current state as a snapshot. Held across the
72    /// clone-serialize-write sequence to prevent stale-last writes,
73    /// with serde + file I/O offloaded to the blocking pool.
74    async fn save_snapshot(&self) {
75        save_eventbridge_snapshot(
76            &self.state,
77            self.snapshot_store.clone(),
78            &self.snapshot_lock,
79        )
80        .await;
81    }
82
83    /// Build a hook that persists the current EventBridge state when invoked, or
84    /// `None` in memory mode (no snapshot store). The CloudFormation provisioner
85    /// mutates `state` directly and uses this to write a CFN-provisioned
86    /// resource through to disk, the same way a direct mutating API call would.
87    pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
88        let store = self.snapshot_store.clone()?;
89        let state = self.state.clone();
90        let lock = self.snapshot_lock.clone();
91        Some(Arc::new(move || {
92            let state = state.clone();
93            let store = store.clone();
94            let lock = lock.clone();
95            Box::pin(async move {
96                save_eventbridge_snapshot(&state, Some(store), &lock).await;
97            })
98        }))
99    }
100}
101
102/// Persist the current EventBridge state as a snapshot. Offloads the serde +
103/// blocking file write to the Tokio blocking pool. Noop when `store` is `None`
104/// (memory mode). Shared by `EventBridgeService::save_snapshot` and the
105/// CloudFormation provisioner's post-provision persist hook so both route
106/// through the same serialize-and-write path.
107pub async fn save_eventbridge_snapshot(
108    state: &SharedEventBridgeState,
109    store: Option<Arc<dyn SnapshotStore>>,
110    lock: &AsyncMutex<()>,
111) {
112    let Some(store) = store else {
113        return;
114    };
115    let _guard = lock.lock().await;
116    let snapshot = EventBridgeSnapshot {
117        schema_version: EVENTBRIDGE_SNAPSHOT_SCHEMA_VERSION,
118        accounts: Some(state.read().clone()),
119        state: None,
120    };
121    let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
122        let bytes = serde_json::to_vec(&snapshot)
123            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
124        store.save(&bytes)
125    })
126    .await;
127    match join {
128        Ok(Ok(())) => {}
129        Ok(Err(err)) => tracing::error!(%err, "failed to write eventbridge snapshot"),
130        Err(err) => tracing::error!(%err, "eventbridge snapshot task panicked"),
131    }
132}
133
134#[async_trait]
135impl AwsService for EventBridgeService {
136    fn service_name(&self) -> &str {
137        "events"
138    }
139
140    async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
141        let mutates = is_mutating_action(req.action.as_str());
142        let result = match req.action.as_str() {
143            "CreateEventBus" => self.create_event_bus(&req),
144            "DeleteEventBus" => self.delete_event_bus(&req),
145            "ListEventBuses" => self.list_event_buses(&req),
146            "DescribeEventBus" => self.describe_event_bus(&req),
147            "PutRule" => self.put_rule(&req),
148            "DeleteRule" => self.delete_rule(&req),
149            "ListRules" => self.list_rules(&req),
150            "DescribeRule" => self.describe_rule(&req),
151            "EnableRule" => self.enable_rule(&req),
152            "DisableRule" => self.disable_rule(&req),
153            "PutTargets" => self.put_targets(&req),
154            "RemoveTargets" => self.remove_targets(&req),
155            "ListTargetsByRule" => self.list_targets_by_rule(&req),
156            "ListRuleNamesByTarget" => self.list_rule_names_by_target(&req),
157            "PutEvents" => self.put_events(&req),
158            "PutPermission" => self.put_permission(&req),
159            "RemovePermission" => self.remove_permission(&req),
160            "TagResource" => self.tag_resource(&req),
161            "UntagResource" => self.untag_resource(&req),
162            "ListTagsForResource" => self.list_tags_for_resource(&req),
163            "CreateArchive" => self.create_archive(&req),
164            "DescribeArchive" => self.describe_archive(&req),
165            "ListArchives" => self.list_archives(&req),
166            "UpdateArchive" => self.update_archive(&req),
167            "DeleteArchive" => self.delete_archive(&req),
168            "CreateConnection" => self.create_connection(&req),
169            "DescribeConnection" => self.describe_connection(&req),
170            "ListConnections" => self.list_connections(&req),
171            "UpdateConnection" => self.update_connection(&req),
172            "DeleteConnection" => self.delete_connection(&req),
173            "CreateApiDestination" => self.create_api_destination(&req),
174            "DescribeApiDestination" => self.describe_api_destination(&req),
175            "ListApiDestinations" => self.list_api_destinations(&req),
176            "UpdateApiDestination" => self.update_api_destination(&req),
177            "DeleteApiDestination" => self.delete_api_destination(&req),
178            "StartReplay" => self.start_replay(&req),
179            "DescribeReplay" => self.describe_replay(&req),
180            "ListReplays" => self.list_replays(&req),
181            "CancelReplay" => self.cancel_replay(&req),
182            "CreatePartnerEventSource" => self.create_partner_event_source(&req),
183            "DeletePartnerEventSource" => self.delete_partner_event_source(&req),
184            "DescribePartnerEventSource" => self.describe_partner_event_source(&req),
185            "ListPartnerEventSources" => self.list_partner_event_sources(&req),
186            "ListPartnerEventSourceAccounts" => self.list_partner_event_source_accounts(&req),
187            "ActivateEventSource" => self.activate_event_source(&req),
188            "DeactivateEventSource" => self.deactivate_event_source(&req),
189            "DescribeEventSource" => self.describe_event_source(&req),
190            "ListEventSources" => self.list_event_sources(&req),
191            "PutPartnerEvents" => self.put_partner_events(&req),
192            "TestEventPattern" => self.test_event_pattern(&req),
193            "UpdateEventBus" => self.update_event_bus(&req),
194            "CreateEndpoint" => self.create_endpoint(&req),
195            "DeleteEndpoint" => self.delete_endpoint(&req),
196            "DescribeEndpoint" => self.describe_endpoint(&req),
197            "ListEndpoints" => self.list_endpoints(&req),
198            "UpdateEndpoint" => self.update_endpoint(&req),
199            "DeauthorizeConnection" => self.deauthorize_connection(&req),
200            _ => Err(AwsServiceError::action_not_implemented(
201                "events",
202                &req.action,
203            )),
204        };
205        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
206            self.save_snapshot().await;
207        }
208        result
209    }
210
211    fn supported_actions(&self) -> &[&str] {
212        &[
213            "CreateEventBus",
214            "DeleteEventBus",
215            "ListEventBuses",
216            "DescribeEventBus",
217            "PutRule",
218            "DeleteRule",
219            "ListRules",
220            "DescribeRule",
221            "EnableRule",
222            "DisableRule",
223            "PutTargets",
224            "RemoveTargets",
225            "ListTargetsByRule",
226            "ListRuleNamesByTarget",
227            "PutEvents",
228            "PutPermission",
229            "RemovePermission",
230            "TagResource",
231            "UntagResource",
232            "ListTagsForResource",
233            "CreateArchive",
234            "DescribeArchive",
235            "ListArchives",
236            "UpdateArchive",
237            "DeleteArchive",
238            "CreateConnection",
239            "DescribeConnection",
240            "ListConnections",
241            "UpdateConnection",
242            "DeleteConnection",
243            "CreateApiDestination",
244            "DescribeApiDestination",
245            "ListApiDestinations",
246            "UpdateApiDestination",
247            "DeleteApiDestination",
248            "StartReplay",
249            "DescribeReplay",
250            "ListReplays",
251            "CancelReplay",
252            "CreatePartnerEventSource",
253            "DeletePartnerEventSource",
254            "DescribePartnerEventSource",
255            "ListPartnerEventSources",
256            "ListPartnerEventSourceAccounts",
257            "ActivateEventSource",
258            "DeactivateEventSource",
259            "DescribeEventSource",
260            "ListEventSources",
261            "PutPartnerEvents",
262            "TestEventPattern",
263            "UpdateEventBus",
264            "CreateEndpoint",
265            "DeleteEndpoint",
266            "DescribeEndpoint",
267            "ListEndpoints",
268            "UpdateEndpoint",
269            "DeauthorizeConnection",
270        ]
271    }
272}
273
274// ─── Event Bus Operations ───────────────────────────────────────────
275impl EventBridgeService {
276    fn create_event_bus(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
277        let body = req.json_body();
278        validate_required("Name", &body["Name"])?;
279        let name = body["Name"]
280            .as_str()
281            .ok_or_else(|| missing("Name"))?
282            .to_string();
283        validate_string_length("name", &name, 1, 256)?;
284        validate_optional_string_length(
285            "eventSourceName",
286            body["EventSourceName"].as_str(),
287            1,
288            256,
289        )?;
290        validate_optional_string_length("description", body["Description"].as_str(), 0, 512)?;
291        validate_optional_string_length(
292            "kmsKeyIdentifier",
293            body["KmsKeyIdentifier"].as_str(),
294            0,
295            2048,
296        )?;
297
298        // Validate name doesn't contain '/' (unless partner bus)
299        if name.contains('/') && !name.starts_with("aws.partner/") {
300            return Err(AwsServiceError::aws_error(
301                StatusCode::BAD_REQUEST,
302                "ValidationException",
303                "Event bus name must not contain '/'.",
304            ));
305        }
306
307        // Partner event bus validation
308        if name.starts_with("aws.partner/") {
309            let event_source = body["EventSourceName"].as_str().unwrap_or("");
310            let accounts_r = self.state.read();
311            let empty_r = EventBridgeState::new(&req.account_id, &req.region);
312            let state_r = accounts_r.get(&req.account_id).unwrap_or(&empty_r);
313            let has_source = state_r.partner_event_sources.contains_key(event_source);
314            drop(accounts_r);
315            if !has_source {
316                return Err(AwsServiceError::aws_error(
317                    StatusCode::BAD_REQUEST,
318                    "ResourceNotFoundException",
319                    format!("Event source {event_source} does not exist."),
320                ));
321            }
322        }
323
324        let mut accounts = self.state.write();
325        let state = accounts.get_or_create(&req.account_id);
326
327        if state.buses.contains_key(&name) {
328            return Err(AwsServiceError::aws_error(
329                StatusCode::BAD_REQUEST,
330                "ResourceAlreadyExistsException",
331                format!("Event bus {name} already exists."),
332            ));
333        }
334
335        let arn = format!(
336            "arn:aws:events:{}:{}:event-bus/{}",
337            req.region, state.account_id, name
338        );
339        let now = Utc::now();
340        let description = body["Description"].as_str().map(|s| s.to_string());
341        let kms_key_identifier = body["KmsKeyIdentifier"].as_str().map(|s| s.to_string());
342        let dead_letter_config = body.get("DeadLetterConfig").cloned();
343
344        let tags = parse_tags(&body);
345
346        let bus = EventBus {
347            name: name.clone(),
348            arn: arn.clone(),
349            tags,
350            policy: None,
351            description,
352            kms_key_identifier,
353            dead_letter_config,
354            creation_time: now,
355            last_modified_time: now,
356        };
357        state.buses.insert(name, bus);
358
359        Ok(AwsResponse::ok_json(json!({ "EventBusArn": arn })))
360    }
361
362    fn delete_event_bus(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
363        let body = req.json_body();
364        validate_required("Name", &body["Name"])?;
365        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
366        validate_string_length("name", name, 1, 256)?;
367
368        if name == "default" {
369            return Err(AwsServiceError::aws_error(
370                StatusCode::BAD_REQUEST,
371                "ValidationException",
372                format!("Cannot delete event bus {name}."),
373            ));
374        }
375
376        let mut accounts = self.state.write();
377        let state = accounts.get_or_create(&req.account_id);
378        state.buses.remove(name);
379        state.rules.retain(|k, _| k.0 != name);
380
381        Ok(AwsResponse::ok_json(json!({})))
382    }
383
384    fn list_event_buses(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
385        let body = req.json_body();
386        // Smithy ListEventBusesRequest constraints:
387        //   NamePrefix: EventBusName length 1..=256
388        //   NextToken: length 1..=2048
389        //   Limit: LimitMax100 range 1..=100
390        // Unrecognised pagination tokens still fall back to the start of the
391        // list — `InvalidNextTokenException` only fires when the token shape
392        // itself is wrong, not when it points at a vanished cursor.
393        validate_optional_string_length_value("NamePrefix", &body["NamePrefix"], 1, 256)?;
394        validate_optional_string_length_value("NextToken", &body["NextToken"], 1, 2048)?;
395        validate_optional_json_range("Limit", &body["Limit"], 1, 100)?;
396        let name_prefix = body["NamePrefix"].as_str();
397        let limit = body["Limit"].as_i64().unwrap_or(100).clamp(1, 100) as usize;
398
399        let accounts = self.state.read();
400        let empty = EventBridgeState::new(&req.account_id, &req.region);
401        let state = accounts.get(&req.account_id).unwrap_or(&empty);
402        let filtered: Vec<&_> = state
403            .buses
404            .values()
405            .filter(|b| match name_prefix {
406                Some(prefix) => b.name.starts_with(prefix),
407                None => true,
408            })
409            .collect();
410
411        let (page, next_token) = paginate(&filtered, body["NextToken"].as_str(), limit);
412        let buses: Vec<Value> = page
413            .iter()
414            .map(|b| {
415                // Mirror DescribeEventBus: ListEventBuses also reports the
416                // bus creation/last-modified times, which the Terraform
417                // aws_cloudwatch_event_buses data source expects to be set.
418                json!({
419                    "Name": b.name,
420                    "Arn": b.arn,
421                    "CreationTime": b.creation_time.timestamp() as f64,
422                    "LastModifiedTime": b.last_modified_time.timestamp() as f64,
423                })
424            })
425            .collect();
426        let mut resp = json!({ "EventBuses": buses });
427        if let Some(token) = next_token {
428            resp["NextToken"] = json!(token);
429        }
430
431        Ok(AwsResponse::ok_json(resp))
432    }
433
434    fn describe_event_bus(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
435        let body = req.json_body();
436        validate_optional_string_length("name", body["Name"].as_str(), 1, 1600)?;
437        let name = body["Name"].as_str().unwrap_or("default");
438
439        let accounts = self.state.read();
440        let empty = EventBridgeState::new(&req.account_id, &req.region);
441        let state = accounts.get(&req.account_id).unwrap_or(&empty);
442        let bus = state.buses.get(name).ok_or_else(|| {
443            AwsServiceError::aws_error(
444                StatusCode::BAD_REQUEST,
445                "ResourceNotFoundException",
446                format!("Event bus {name} does not exist."),
447            )
448        })?;
449
450        let mut resp = json!({
451            "Name": bus.name,
452            "Arn": bus.arn,
453            "CreationTime": bus.creation_time.timestamp() as f64,
454            "LastModifiedTime": bus.last_modified_time.timestamp() as f64,
455        });
456
457        if let Some(ref policy) = bus.policy {
458            resp["Policy"] = Value::String(serde_json::to_string(policy).unwrap());
459        }
460        if let Some(ref desc) = bus.description {
461            resp["Description"] = json!(desc);
462        }
463        if let Some(ref kms) = bus.kms_key_identifier {
464            resp["KmsKeyIdentifier"] = json!(kms);
465        }
466        if let Some(ref dlc) = bus.dead_letter_config {
467            resp["DeadLetterConfig"] = dlc.clone();
468        }
469
470        Ok(AwsResponse::ok_json(resp))
471    }
472
473    // ─── Permission Operations ──────────────────────────────────────────
474
475    fn put_permission(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
476        let body = req.json_body();
477        // Smithy PutPermissionRequest constraints (optional members but each
478        // carries a `@length` trait that must be honoured when present):
479        //   EventBusName: NonPartnerEventBusName length 1..=256
480        //   Action: length 1..=64
481        //   Principal: length 1..=12 (12-digit AWS account or `*`)
482        //   StatementId: length 1..=64
483        validate_optional_string_length_value("EventBusName", &body["EventBusName"], 1, 256)?;
484        validate_optional_string_length_value("Action", &body["Action"], 1, 64)?;
485        validate_optional_string_length_value("Principal", &body["Principal"], 1, 12)?;
486        validate_optional_string_length_value("StatementId", &body["StatementId"], 1, 64)?;
487        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
488
489        let mut accounts = self.state.write();
490        let state = accounts.get_or_create(&req.account_id);
491
492        let bus = state.buses.get_mut(event_bus_name).ok_or_else(|| {
493            AwsServiceError::aws_error(
494                StatusCode::BAD_REQUEST,
495                "ResourceNotFoundException",
496                format!("Event bus {event_bus_name} does not exist."),
497            )
498        })?;
499
500        // Check if Policy is provided (new-style)
501        if let Some(policy_str) = body["Policy"].as_str() {
502            if let Ok(policy) = serde_json::from_str::<Value>(policy_str) {
503                bus.policy = Some(policy);
504                return Ok(AwsResponse::ok_json(json!({})));
505            }
506        }
507
508        // Old-style: Action, Principal, StatementId. All are @optional in the
509        // Smithy model; non-string values were already rejected above with
510        // SerializationException, so reaching here means each is either a
511        // valid string or absent. Fall back to "" to preserve current behavior
512        // — recording an empty-statement policy entry is harmless since it can
513        // never match a real action/principal pair.
514        let action = body["Action"].as_str().unwrap_or("");
515        let principal = body["Principal"].as_str().unwrap_or("");
516        let statement_id = body["StatementId"].as_str().unwrap_or("");
517
518        // Note: real AWS does enforce a small allow-list on `Action`, but
519        // PutPermission's Smithy model only declares ResourceNotFoundException,
520        // PolicyLengthExceededException, ConcurrentModificationException,
521        // OperationDisabledException, and InternalException. We accept any
522        // action string and just record the statement.
523        // A `*` principal means "any account" and is stored verbatim, not as
524        // an account-root ARN. The Terraform aws_cloudwatch_event_permission
525        // resource reads the principal back and asserts it is exactly "*".
526        let principal_value = if principal == "*" {
527            json!("*")
528        } else {
529            json!({ "AWS": Arn::global("iam", principal, "root").to_string() })
530        };
531        let statement = json!({
532            "Sid": statement_id,
533            "Effect": "Allow",
534            "Principal": principal_value,
535            "Action": action,
536            "Resource": bus.arn,
537        });
538
539        let policy = bus.policy.get_or_insert_with(|| {
540            json!({
541                "Version": "2012-10-17",
542                "Statement": [],
543            })
544        });
545
546        if let Some(stmts) = policy["Statement"].as_array_mut() {
547            // PutPermission with an existing StatementId replaces that
548            // statement (e.g. changing its principal), it does not stack a
549            // second one. Drop any prior statement with the same Sid first.
550            stmts.retain(|s| s["Sid"].as_str() != Some(statement_id));
551            stmts.push(statement);
552        }
553
554        Ok(AwsResponse::ok_json(json!({})))
555    }
556
557    fn remove_permission(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
558        let body = req.json_body();
559        validate_optional_string_length("statementId", body["StatementId"].as_str(), 1, 64)?;
560        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 256)?;
561        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
562        let statement_id = body["StatementId"].as_str().unwrap_or("");
563        let remove_all = body["RemoveAllPermissions"].as_bool().unwrap_or(false);
564
565        let mut accounts = self.state.write();
566        let state = accounts.get_or_create(&req.account_id);
567
568        let bus = state.buses.get_mut(event_bus_name).ok_or_else(|| {
569            AwsServiceError::aws_error(
570                StatusCode::BAD_REQUEST,
571                "ResourceNotFoundException",
572                format!("Event bus {event_bus_name} does not exist."),
573            )
574        })?;
575
576        if remove_all {
577            bus.policy = None;
578            return Ok(AwsResponse::ok_json(json!({})));
579        }
580
581        let policy = bus.policy.as_mut().ok_or_else(|| {
582            AwsServiceError::aws_error(
583                StatusCode::BAD_REQUEST,
584                "ResourceNotFoundException",
585                "EventBus does not have a policy.",
586            )
587        })?;
588
589        if let Some(stmts) = policy["Statement"].as_array_mut() {
590            let before = stmts.len();
591            stmts.retain(|s| s["Sid"].as_str() != Some(statement_id));
592            if stmts.len() == before {
593                return Err(AwsServiceError::aws_error(
594                    StatusCode::BAD_REQUEST,
595                    "ResourceNotFoundException",
596                    "Statement with the provided id does not exist.",
597                ));
598            }
599            if stmts.is_empty() {
600                bus.policy = None;
601            }
602        }
603
604        Ok(AwsResponse::ok_json(json!({})))
605    }
606
607    // ─── Rule Operations ────────────────────────────────────────────────
608
609    fn put_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
610        let body = req.json_body();
611        // Smithy PutRuleRequest constraints:
612        //   Name: RuleName length 1..=64, @required
613        //   ScheduleExpression: length 0..=256
614        //   EventPattern: length 0..=4096 (raw JSON, separate
615        //     InvalidEventPatternException still applies to syntax)
616        //   State: RuleState enum {ENABLED, DISABLED,
617        //          ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS}
618        //   Description: RuleDescription length 0..=512
619        //   RoleArn: length 1..=1600
620        //   EventBusName: EventBusNameOrArn length 1..=1600
621        validate_required("Name", &body["Name"])?;
622        let name = body["Name"]
623            .as_str()
624            .ok_or_else(|| missing("Name"))?
625            .to_string();
626        validate_string_length("Name", &name, 1, 64)?;
627        validate_optional_string_length_value(
628            "ScheduleExpression",
629            &body["ScheduleExpression"],
630            0,
631            256,
632        )?;
633        validate_optional_string_length_value("EventPattern", &body["EventPattern"], 0, 4096)?;
634        // Reject a syntactically invalid EventPattern at PutRule time, like AWS.
635        // Previously only the length was checked, so an invalid pattern was
636        // stored and PutEvents silently never matched it.
637        if let Some(pattern) = body["EventPattern"].as_str().filter(|s| !s.is_empty()) {
638            validate_event_pattern(pattern)?;
639        }
640        // Reject a malformed ScheduleExpression up front, like AWS. Previously
641        // only the length was checked, so a rule with e.g. `rate(5 minute)` or
642        // `cron(bad)` was stored and simply never fired.
643        if let Some(schedule) = body["ScheduleExpression"]
644            .as_str()
645            .filter(|s| !s.is_empty())
646        {
647            if !crate::scheduler::is_valid_schedule_expression(schedule) {
648                return Err(AwsServiceError::aws_error(
649                    StatusCode::BAD_REQUEST,
650                    "ValidationException",
651                    format!(
652                        "Parameter ScheduleExpression is not valid. Reason: {schedule} is not a valid expression."
653                    ),
654                ));
655            }
656        }
657        validate_optional_enum_value(
658            "State",
659            &body["State"],
660            &[
661                "ENABLED",
662                "DISABLED",
663                "ENABLED_WITH_ALL_CLOUDTRAIL_MANAGEMENT_EVENTS",
664            ],
665        )?;
666        validate_optional_string_length_value("Description", &body["Description"], 0, 512)?;
667        validate_optional_string_length_value("RoleArn", &body["RoleArn"], 1, 1600)?;
668        validate_optional_string_length_value("EventBusName", &body["EventBusName"], 1, 1600)?;
669
670        let raw_bus = body["EventBusName"]
671            .as_str()
672            .unwrap_or("default")
673            .to_string();
674
675        let mut accounts = self.state.write();
676        let state = accounts.get_or_create(&req.account_id);
677        let event_bus_name = state.resolve_bus_name(&raw_bus);
678
679        let event_pattern = body["EventPattern"].as_str().and_then(|s| {
680            if s.is_empty() {
681                None
682            } else {
683                Some(s.to_string())
684            }
685        });
686        let schedule_expression = body["ScheduleExpression"].as_str().and_then(|s| {
687            if s.is_empty() {
688                None
689            } else {
690                Some(s.to_string())
691            }
692        });
693        let description = body["Description"].as_str().map(|s| s.to_string());
694        let role_arn = body["RoleArn"].as_str().map(|s| s.to_string());
695        let rule_state = body["State"].as_str().unwrap_or("ENABLED").to_string();
696
697        // Note: real AWS rejects ScheduleExpression on a non-default bus, but
698        // PutRule's Smithy model only declares InvalidEventPatternException
699        // for input-shape problems, not ValidationException. We accept the
700        // value and let the scheduler ignore it on non-default buses.
701
702        if !state.buses.contains_key(&event_bus_name) {
703            return Err(AwsServiceError::aws_error(
704                StatusCode::BAD_REQUEST,
705                "ResourceNotFoundException",
706                format!("Event bus {event_bus_name} does not exist."),
707            ));
708        }
709
710        let arn = if event_bus_name == "default" {
711            format!(
712                "arn:aws:events:{}:{}:rule/{}",
713                req.region, state.account_id, name
714            )
715        } else {
716            format!(
717                "arn:aws:events:{}:{}:rule/{}/{}",
718                req.region, state.account_id, event_bus_name, name
719            )
720        };
721
722        let key = (event_bus_name.clone(), name.clone());
723        let targets = state
724            .rules
725            .get(&key)
726            .map(|r| r.targets.clone())
727            .unwrap_or_default();
728
729        let tags = parse_tags(&body);
730
731        let rule = EventRule {
732            name: name.clone(),
733            arn: arn.clone(),
734            event_bus_name,
735            event_pattern,
736            schedule_expression,
737            state: rule_state,
738            description,
739            role_arn,
740            managed_by: None,
741            created_by: None,
742            targets,
743            tags,
744            last_fired: None,
745        };
746
747        state.rules.insert(key, rule);
748        Ok(AwsResponse::ok_json(json!({ "RuleArn": arn })))
749    }
750
751    fn delete_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
752        let body = req.json_body();
753        validate_required("Name", &body["Name"])?;
754        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
755        validate_string_length("name", name, 1, 64)?;
756        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
757        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
758
759        let mut accounts = self.state.write();
760        let state = accounts.get_or_create(&req.account_id);
761        let bus_name = state.resolve_bus_name(event_bus_name);
762        let key = (bus_name, name.to_string());
763
764        // Check if rule has targets
765        if let Some(rule) = state.rules.get(&key) {
766            if !rule.targets.is_empty() {
767                return Err(AwsServiceError::aws_error(
768                    StatusCode::BAD_REQUEST,
769                    "ValidationException",
770                    "Rule can't be deleted since it has targets.",
771                ));
772            }
773        }
774
775        state.rules.remove(&key);
776        Ok(AwsResponse::ok_json(json!({})))
777    }
778
779    fn list_rules(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
780        let body = req.json_body();
781        validate_optional_string_length("namePrefix", body["NamePrefix"].as_str(), 1, 64)?;
782        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
783        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 2048)?;
784        validate_optional_range_i64("limit", body["Limit"].as_i64(), 1, 100)?;
785        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
786        let name_prefix = body["NamePrefix"].as_str();
787        let limit = body["Limit"].as_u64().map(|n| n as usize);
788        let next_token = body["NextToken"].as_str();
789
790        let accounts = self.state.read();
791        let empty = EventBridgeState::new(&req.account_id, &req.region);
792        let state = accounts.get(&req.account_id).unwrap_or(&empty);
793        let bus_name = state.resolve_bus_name(event_bus_name);
794
795        let mut rules: Vec<&EventRule> = state
796            .rules
797            .values()
798            .filter(|r| r.event_bus_name == bus_name)
799            .filter(|r| match name_prefix {
800                Some(prefix) => r.name.starts_with(prefix),
801                None => true,
802            })
803            .collect();
804        rules.sort_by(|a, b| a.name.cmp(&b.name));
805
806        // Pagination
807        let start = next_token
808            .and_then(|t| t.parse::<usize>().ok())
809            .unwrap_or(0)
810            .min(rules.len());
811        let rules_slice = &rules[start..];
812
813        let (page, new_next_token) = if let Some(lim) = limit {
814            if rules_slice.len() > lim {
815                (&rules_slice[..lim], Some((start + lim).to_string()))
816            } else {
817                (rules_slice, None)
818            }
819        } else {
820            (rules_slice, None)
821        };
822
823        let rules_json: Vec<Value> = page
824            .iter()
825            .map(|r| {
826                let mut obj = json!({
827                    "Name": r.name,
828                    "Arn": r.arn,
829                    "EventBusName": r.event_bus_name,
830                    "State": r.state,
831                });
832                if let Some(ref desc) = r.description {
833                    obj["Description"] = json!(desc);
834                }
835                if let Some(ref ep) = r.event_pattern {
836                    obj["EventPattern"] = json!(ep);
837                }
838                if let Some(ref se) = r.schedule_expression {
839                    obj["ScheduleExpression"] = json!(se);
840                }
841                if let Some(ref role) = r.role_arn {
842                    obj["RoleArn"] = json!(role);
843                }
844                if let Some(ref mb) = r.managed_by {
845                    obj["ManagedBy"] = json!(mb);
846                }
847                obj
848            })
849            .collect();
850
851        let mut resp = json!({ "Rules": rules_json });
852        if let Some(token) = new_next_token {
853            resp["NextToken"] = json!(token);
854        }
855
856        Ok(AwsResponse::ok_json(resp))
857    }
858
859    fn describe_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
860        let body = req.json_body();
861        validate_required("Name", &body["Name"])?;
862        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
863        validate_string_length("name", name, 1, 64)?;
864        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
865        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
866
867        let accounts = self.state.read();
868        let empty = EventBridgeState::new(&req.account_id, &req.region);
869        let state = accounts.get(&req.account_id).unwrap_or(&empty);
870        let bus_name = state.resolve_bus_name(event_bus_name);
871        let key = (bus_name.clone(), name.to_string());
872
873        let rule = state.rules.get(&key).ok_or_else(|| {
874            AwsServiceError::aws_error(
875                StatusCode::BAD_REQUEST,
876                "ResourceNotFoundException",
877                format!("Rule {name} does not exist."),
878            )
879        })?;
880
881        let mut resp = json!({
882            "Name": rule.name,
883            "Arn": rule.arn,
884            "EventBusName": rule.event_bus_name,
885            "State": rule.state,
886        });
887
888        if let Some(ref desc) = rule.description {
889            resp["Description"] = json!(desc);
890        }
891        if let Some(ref ep) = rule.event_pattern {
892            resp["EventPattern"] = json!(ep);
893        }
894        if let Some(ref se) = rule.schedule_expression {
895            resp["ScheduleExpression"] = json!(se);
896        }
897        if let Some(ref role) = rule.role_arn {
898            resp["RoleArn"] = json!(role);
899        }
900        if let Some(ref mb) = rule.managed_by {
901            resp["ManagedBy"] = json!(mb);
902        }
903        if let Some(ref cb) = rule.created_by {
904            resp["CreatedBy"] = json!(cb);
905        }
906        // If non-default bus, set CreatedBy to account_id
907        if rule.event_bus_name != "default" && rule.created_by.is_none() {
908            resp["CreatedBy"] = json!(state.account_id);
909        }
910
911        Ok(AwsResponse::ok_json(resp))
912    }
913
914    fn enable_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
915        let body = req.json_body();
916        validate_required("Name", &body["Name"])?;
917        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
918        validate_string_length("name", name, 1, 64)?;
919        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
920        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
921
922        let mut accounts = self.state.write();
923        let state = accounts.get_or_create(&req.account_id);
924        let bus_name = state.resolve_bus_name(event_bus_name);
925        let key = (bus_name, name.to_string());
926
927        let rule = state.rules.get_mut(&key).ok_or_else(|| {
928            AwsServiceError::aws_error(
929                StatusCode::BAD_REQUEST,
930                "ResourceNotFoundException",
931                format!("Rule {name} does not exist."),
932            )
933        })?;
934
935        rule.state = "ENABLED".to_string();
936        Ok(AwsResponse::ok_json(json!({})))
937    }
938
939    fn disable_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
940        let body = req.json_body();
941        validate_required("Name", &body["Name"])?;
942        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
943        validate_string_length("name", name, 1, 64)?;
944        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
945        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
946
947        let mut accounts = self.state.write();
948        let state = accounts.get_or_create(&req.account_id);
949        let bus_name = state.resolve_bus_name(event_bus_name);
950        let key = (bus_name, name.to_string());
951
952        let rule = state.rules.get_mut(&key).ok_or_else(|| {
953            AwsServiceError::aws_error(
954                StatusCode::BAD_REQUEST,
955                "ResourceNotFoundException",
956                format!("Rule {name} does not exist."),
957            )
958        })?;
959
960        rule.state = "DISABLED".to_string();
961        Ok(AwsResponse::ok_json(json!({})))
962    }
963
964    // ─── Target Operations ──────────────────────────────────────────────
965
966    fn put_targets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
967        let body = req.json_body();
968        // Smithy PutTargetsRequest constraints (top-level shape):
969        //   Rule: RuleName @required length 1..=64
970        //   EventBusName: EventBusNameOrArn length 1..=1600
971        //   Targets: TargetList @required length 1..=100
972        // Per-target validation still flows through `FailedEntries` (matching
973        // AWS); only the top-level shape produces ValidationException.
974        validate_required("Rule", &body["Rule"])?;
975        let rule_name = body["Rule"].as_str().ok_or_else(|| missing("Rule"))?;
976        validate_string_length("Rule", rule_name, 1, 64)?;
977        validate_optional_string_length_value("EventBusName", &body["EventBusName"], 1, 1600)?;
978        // Targets is @required with TargetList length 1..=100 in the Smithy
979        // model. Real AWS rejects empty / oversized lists with a
980        // ValidationException; per-target validation continues to flow
981        // through FailedEntries (matching AWS).
982        validate_required("Targets", &body["Targets"])?;
983        let targets_array = body["Targets"].as_array().ok_or_else(|| {
984            AwsServiceError::aws_error(
985                StatusCode::BAD_REQUEST,
986                "ValidationException",
987                "Targets must be a list",
988            )
989        })?;
990        if targets_array.is_empty() || targets_array.len() > 100 {
991            return Err(AwsServiceError::aws_error(
992                StatusCode::BAD_REQUEST,
993                "ValidationException",
994                "Value at 'Targets' failed to satisfy constraint: \
995                 Member must have length between 1 and 100",
996            ));
997        }
998        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
999        let targets: Vec<Value> = targets_array.clone();
1000
1001        let mut accounts = self.state.write();
1002        let state = accounts.get_or_create(&req.account_id);
1003        let bus_name = state.resolve_bus_name(event_bus_name);
1004        let key = (bus_name.clone(), rule_name.to_string());
1005
1006        let rule = state.rules.get_mut(&key).ok_or_else(|| {
1007            AwsServiceError::aws_error(
1008                StatusCode::BAD_REQUEST,
1009                "ResourceNotFoundException",
1010                format!("Rule {rule_name} does not exist on EventBus {bus_name}."),
1011            )
1012        })?;
1013
1014        let mut failed_entries: Vec<Value> = Vec::new();
1015        for target in &targets {
1016            let target_id = target["Id"].as_str().unwrap_or("").to_string();
1017            let target_arn = target["Arn"].as_str().unwrap_or("");
1018
1019            if target_arn.ends_with(".fifo") && target.get("SqsParameters").is_none() {
1020                failed_entries.push(json!({
1021                    "TargetId": target_id,
1022                    "ErrorCode": "ValidationException",
1023                    "ErrorMessage": format!(
1024                        "Parameter(s) SqsParameters must be specified for target: {target_id}."
1025                    ),
1026                }));
1027                continue;
1028            }
1029            if !target_arn.starts_with("arn:") {
1030                failed_entries.push(json!({
1031                    "TargetId": target_id,
1032                    "ErrorCode": "ValidationException",
1033                    "ErrorMessage": format!(
1034                        "Parameter {target_arn} is not valid. Reason: Provided Arn is not in correct format."
1035                    ),
1036                }));
1037                continue;
1038            }
1039
1040            let et = parse_target(target);
1041            rule.targets.retain(|t| t.id != et.id);
1042            rule.targets.push(et);
1043        }
1044
1045        Ok(AwsResponse::ok_json(json!({
1046            "FailedEntryCount": failed_entries.len(),
1047            "FailedEntries": failed_entries,
1048        })))
1049    }
1050
1051    fn remove_targets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1052        let body = req.json_body();
1053        validate_required("Rule", &body["Rule"])?;
1054        let rule_name = body["Rule"].as_str().ok_or_else(|| missing("Rule"))?;
1055        validate_string_length("rule", rule_name, 1, 64)?;
1056        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
1057        validate_required("Ids", &body["Ids"])?;
1058        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
1059        let ids = body["Ids"].as_array().ok_or_else(|| missing("Ids"))?;
1060
1061        let target_ids: Vec<String> = ids
1062            .iter()
1063            .filter_map(|v| v.as_str().map(|s| s.to_string()))
1064            .collect();
1065
1066        let mut accounts = self.state.write();
1067        let state = accounts.get_or_create(&req.account_id);
1068        let bus_name = state.resolve_bus_name(event_bus_name);
1069        let key = (bus_name.clone(), rule_name.to_string());
1070
1071        let rule = state.rules.get_mut(&key).ok_or_else(|| {
1072            AwsServiceError::aws_error(
1073                StatusCode::BAD_REQUEST,
1074                "ResourceNotFoundException",
1075                format!("Rule {rule_name} does not exist on EventBus {bus_name}."),
1076            )
1077        })?;
1078
1079        rule.targets.retain(|t| !target_ids.contains(&t.id));
1080
1081        Ok(AwsResponse::ok_json(json!({
1082            "FailedEntryCount": 0,
1083            "FailedEntries": [],
1084        })))
1085    }
1086
1087    fn list_targets_by_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1088        let body = req.json_body();
1089        validate_required("Rule", &body["Rule"])?;
1090        let rule_name = body["Rule"].as_str().ok_or_else(|| missing("Rule"))?;
1091        validate_string_length("rule", rule_name, 1, 64)?;
1092        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
1093        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 2048)?;
1094        validate_optional_range_i64("limit", body["Limit"].as_i64(), 1, 100)?;
1095        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
1096        let limit = body["Limit"].as_u64().map(|n| n as usize);
1097        let next_token = body["NextToken"].as_str();
1098
1099        let accounts = self.state.read();
1100        let empty = EventBridgeState::new(&req.account_id, &req.region);
1101        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1102        let bus_name = state.resolve_bus_name(event_bus_name);
1103        let key = (bus_name, rule_name.to_string());
1104
1105        let rule = state.rules.get(&key).ok_or_else(|| {
1106            AwsServiceError::aws_error(
1107                StatusCode::BAD_REQUEST,
1108                "ResourceNotFoundException",
1109                format!("Rule {rule_name} does not exist."),
1110            )
1111        })?;
1112
1113        let all_targets = &rule.targets;
1114        let start = next_token
1115            .and_then(|t| t.parse::<usize>().ok())
1116            .unwrap_or(0)
1117            .min(all_targets.len());
1118        let slice = &all_targets[start..];
1119
1120        let (page, new_next_token) = if let Some(lim) = limit {
1121            if slice.len() > lim {
1122                (&slice[..lim], Some((start + lim).to_string()))
1123            } else {
1124                (slice, None)
1125            }
1126        } else {
1127            (slice, None)
1128        };
1129
1130        let targets: Vec<Value> = page.iter().map(target_to_json).collect();
1131
1132        let mut resp = json!({ "Targets": targets });
1133        if let Some(token) = new_next_token {
1134            resp["NextToken"] = json!(token);
1135        }
1136
1137        Ok(AwsResponse::ok_json(resp))
1138    }
1139
1140    fn list_rule_names_by_target(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1141        let body = req.json_body();
1142        validate_required("TargetArn", &body["TargetArn"])?;
1143        let target_arn = body["TargetArn"]
1144            .as_str()
1145            .ok_or_else(|| missing("TargetArn"))?;
1146        validate_string_length("targetArn", target_arn, 1, 1600)?;
1147        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
1148        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 2048)?;
1149        validate_optional_range_i64("limit", body["Limit"].as_i64(), 1, 100)?;
1150        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
1151        let limit = body["Limit"].as_u64().map(|n| n as usize);
1152        let next_token = body["NextToken"].as_str();
1153
1154        let accounts = self.state.read();
1155        let empty = EventBridgeState::new(&req.account_id, &req.region);
1156        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1157        let bus_name = state.resolve_bus_name(event_bus_name);
1158
1159        // Deduplicate rule names
1160        let mut rule_names: Vec<String> = Vec::new();
1161        for rule in state.rules.values() {
1162            if rule.event_bus_name == bus_name
1163                && rule.targets.iter().any(|t| t.arn == target_arn)
1164                && !rule_names.contains(&rule.name)
1165            {
1166                rule_names.push(rule.name.clone());
1167            }
1168        }
1169        rule_names.sort();
1170
1171        let start = next_token
1172            .and_then(|t| t.parse::<usize>().ok())
1173            .unwrap_or(0)
1174            .min(rule_names.len());
1175        let slice = &rule_names[start..];
1176
1177        let (page, new_next_token) = if let Some(lim) = limit {
1178            if slice.len() > lim {
1179                (&slice[..lim], Some((start + lim).to_string()))
1180            } else {
1181                (slice, None)
1182            }
1183        } else {
1184            (slice, None)
1185        };
1186
1187        let mut resp = json!({ "RuleNames": page });
1188        if let Some(token) = new_next_token {
1189            resp["NextToken"] = json!(token);
1190        }
1191
1192        Ok(AwsResponse::ok_json(resp))
1193    }
1194
1195    // ─── Partner Event Sources ────────────���───────────────────────────
1196
1197    fn test_event_pattern(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1198        let body = req.json_body();
1199        validate_required("EventPattern", &body["EventPattern"])?;
1200        validate_required("Event", &body["Event"])?;
1201        let event_pattern = body["EventPattern"]
1202            .as_str()
1203            .ok_or_else(|| missing("EventPattern"))?;
1204        let event_str = body["Event"].as_str().ok_or_else(|| missing("Event"))?;
1205
1206        // Parse the event JSON
1207        let event: Value = serde_json::from_str(event_str).map_err(|_| {
1208            AwsServiceError::aws_error(
1209                StatusCode::BAD_REQUEST,
1210                "InvalidEventPatternException",
1211                "Event is not valid JSON.",
1212            )
1213        })?;
1214
1215        // Parse the pattern JSON
1216        let pattern: Value = serde_json::from_str(event_pattern).map_err(|_| {
1217            AwsServiceError::aws_error(
1218                StatusCode::BAD_REQUEST,
1219                "InvalidEventPatternException",
1220                "Event pattern is not valid JSON.",
1221            )
1222        })?;
1223
1224        // Reject an invalid pattern (scalar leaf, malformed numeric matcher,
1225        // etc.) with InvalidEventPatternException instead of silently
1226        // returning {"Result": false}.
1227        validate_pattern_values(&pattern, "")?;
1228
1229        // Match directly against the caller-supplied event so all of its
1230        // fields — including id / time / version — are matchable, rather than
1231        // reconstructing a partial synthetic event that dropped them.
1232        let result = matches_value(&pattern, &event);
1233
1234        Ok(AwsResponse::ok_json(json!({ "Result": result })))
1235    }
1236
1237    // ─── UpdateEventBus ─────────────────────────────────────────────────
1238
1239    fn update_event_bus(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1240        let body = req.json_body();
1241        validate_optional_string_length("description", body["Description"].as_str(), 0, 512)?;
1242        validate_optional_string_length(
1243            "kmsKeyIdentifier",
1244            body["KmsKeyIdentifier"].as_str(),
1245            0,
1246            2048,
1247        )?;
1248        let name = body["Name"].as_str().unwrap_or("default");
1249
1250        let mut accounts = self.state.write();
1251        let state = accounts.get_or_create(&req.account_id);
1252        let bus = state.buses.get_mut(name).ok_or_else(|| {
1253            AwsServiceError::aws_error(
1254                StatusCode::BAD_REQUEST,
1255                "ResourceNotFoundException",
1256                format!("Event bus {name} does not exist."),
1257            )
1258        })?;
1259
1260        if let Some(desc) = body["Description"].as_str() {
1261            bus.description = Some(desc.to_string());
1262        }
1263        if let Some(kms) = body["KmsKeyIdentifier"].as_str() {
1264            bus.kms_key_identifier = Some(kms.to_string());
1265        }
1266        if let Some(dlc) = body.get("DeadLetterConfig") {
1267            bus.dead_letter_config = Some(dlc.clone());
1268        }
1269        bus.last_modified_time = Utc::now();
1270
1271        let arn = bus.arn.clone();
1272        let bus_name = bus.name.clone();
1273
1274        Ok(AwsResponse::ok_json(json!({
1275            "Arn": arn,
1276            "Name": bus_name,
1277        })))
1278    }
1279
1280    // ─── Endpoint Operations ────────────────────────────────────────────
1281
1282    fn put_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1283        let body = req.json_body();
1284        // Smithy PutEventsRequest constraints:
1285        //   EndpointId: length 1..=50
1286        //   Entries: @required, length 1..=10
1287        validate_optional_string_length_value("EndpointId", &body["EndpointId"], 1, 50)?;
1288        validate_required("Entries", &body["Entries"])?;
1289        let entries_array = body["Entries"].as_array().ok_or_else(|| {
1290            AwsServiceError::aws_error(
1291                StatusCode::BAD_REQUEST,
1292                "ValidationException",
1293                "Entries must be a list",
1294            )
1295        })?;
1296        if entries_array.is_empty() || entries_array.len() > 10 {
1297            return Err(AwsServiceError::aws_error(
1298                StatusCode::BAD_REQUEST,
1299                "ValidationException",
1300                "Value at 'Entries' failed to satisfy constraint: \
1301                 Member must have length between 1 and 10",
1302            ));
1303        }
1304        let entries: Vec<Value> = entries_array.clone();
1305        let entries = &entries;
1306
1307        let mut accounts = self.state.write();
1308        let state = accounts.get_or_create(&req.account_id);
1309        let mut result_entries = Vec::new();
1310        let mut events_to_deliver = Vec::new();
1311        let mut failed_count = 0;
1312
1313        for entry in entries {
1314            let source = entry["Source"].as_str().unwrap_or("").to_string();
1315            let detail_type = entry["DetailType"].as_str().unwrap_or("").to_string();
1316            let detail = entry["Detail"].as_str().unwrap_or("").to_string();
1317
1318            if let Err(error) = validate_put_events_entry(&source, &detail_type, &detail) {
1319                failed_count += 1;
1320                result_entries.push(error);
1321                continue;
1322            }
1323
1324            let event_id = uuid::Uuid::new_v4().to_string();
1325            let raw_bus = entry["EventBusName"]
1326                .as_str()
1327                .unwrap_or("default")
1328                .to_string();
1329            let event_bus_name = state.resolve_bus_name(&raw_bus);
1330
1331            // Bus resource-policy gate. AWS evaluates the bus's
1332            // resource policy against cross-account callers; same-account
1333            // callers always have access. The policy itself is JSON
1334            // stored as serde_json::Value so the IAM evaluator parses
1335            // it the same way it parses an S3 bucket policy.
1336            let caller_account = req
1337                .principal
1338                .as_ref()
1339                .map(|p| p.account_id.as_str())
1340                .unwrap_or(req.account_id.as_str());
1341            if caller_account != req.account_id {
1342                let bus_policy_value = state
1343                    .buses
1344                    .get(&event_bus_name)
1345                    .and_then(|b| b.policy.clone());
1346                if let Some(policy_value) = bus_policy_value {
1347                    let policy_json = serde_json::to_string(&policy_value).unwrap_or_default();
1348                    let policy_doc = fakecloud_iam::evaluator::PolicyDocument::parse(&policy_json);
1349                    let bus_arn = state
1350                        .buses
1351                        .get(&event_bus_name)
1352                        .map(|b| b.arn.clone())
1353                        .unwrap_or_default();
1354                    let principal =
1355                        req.principal
1356                            .clone()
1357                            .unwrap_or_else(|| fakecloud_core::auth::Principal {
1358                                arn: Arn::global("iam", caller_account, "root").to_string(),
1359                                user_id: caller_account.to_string(),
1360                                account_id: caller_account.to_string(),
1361                                principal_type: fakecloud_core::auth::PrincipalType::Root,
1362                                source_identity: None,
1363                                tags: None,
1364                            });
1365                    let context = fakecloud_iam::evaluator::RequestContext {
1366                        aws_principal_arn: Some(principal.arn.clone()),
1367                        aws_principal_account: Some(principal.account_id.clone()),
1368                        ..Default::default()
1369                    };
1370                    let eval_req = fakecloud_iam::evaluator::EvalRequest {
1371                        principal: &principal,
1372                        action: "events:PutEvents".to_string(),
1373                        resource: bus_arn,
1374                        context,
1375                    };
1376                    let decision = fakecloud_iam::evaluator::evaluate_resource_policy_only(
1377                        &policy_doc,
1378                        &eval_req,
1379                    );
1380                    if !matches!(decision, fakecloud_iam::evaluator::Decision::Allow) {
1381                        failed_count += 1;
1382                        result_entries.push(json!({
1383                            "ErrorCode": "AccessDeniedException",
1384                            "ErrorMessage": format!(
1385                                "User '{}' is not authorized to put events on event bus '{}'",
1386                                principal.arn, event_bus_name
1387                            ),
1388                        }));
1389                        continue;
1390                    }
1391                }
1392            }
1393
1394            let time = parse_put_events_time(&entry["Time"]);
1395            let resources: Vec<String> = entry["Resources"]
1396                .as_array()
1397                .map(|arr| {
1398                    arr.iter()
1399                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
1400                        .collect()
1401                })
1402                .unwrap_or_default();
1403
1404            let event = PutEvent {
1405                event_id: event_id.clone(),
1406                source: source.clone(),
1407                detail_type: detail_type.clone(),
1408                detail: detail.clone(),
1409                event_bus_name: event_bus_name.clone(),
1410                time,
1411                resources: resources.clone(),
1412            };
1413
1414            archive_matching_event(
1415                state,
1416                &event,
1417                &event_bus_name,
1418                &source,
1419                &detail_type,
1420                &detail,
1421                &req.account_id,
1422                &req.region,
1423                &resources,
1424            );
1425
1426            state.events.push(event);
1427
1428            // Find matching rules and their targets, carrying each rule's ARN
1429            // so the InputTransformer can resolve `<aws.events.rule-arn>`.
1430            let matching_targets: Vec<(String, EventTarget)> = state
1431                .rules
1432                .values()
1433                .filter(|r| {
1434                    r.event_bus_name == event_bus_name
1435                        && r.state == "ENABLED"
1436                        && matches_pattern(
1437                            r.event_pattern.as_deref(),
1438                            &source,
1439                            &detail_type,
1440                            &detail,
1441                            &req.account_id,
1442                            &req.region,
1443                            &resources,
1444                            &event_id,
1445                            &time.to_rfc3339(),
1446                        )
1447                })
1448                .flat_map(|r| r.targets.iter().map(|t| (r.arn.clone(), t.clone())))
1449                .collect();
1450
1451            if !matching_targets.is_empty() {
1452                events_to_deliver.push((
1453                    event_id.clone(),
1454                    source,
1455                    detail_type,
1456                    detail,
1457                    time,
1458                    resources,
1459                    matching_targets,
1460                ));
1461            }
1462
1463            result_entries.push(json!({ "EventId": event_id }));
1464        }
1465
1466        // Drop the lock before delivering
1467        drop(accounts);
1468
1469        // Deliver to targets — single-target dispatch lives in the
1470        // shared helper so cross-service callers (delivery.rs) honor the
1471        // same target shape (SQS/SNS/Lambda/Logs/Kinesis/StepFunctions/
1472        // ApiDestination/HTTP) and the same InputTransformer rules.
1473        for (event_id, source, detail_type, detail, time, resources, targets) in events_to_deliver {
1474            let detail_value: Value = serde_json::from_str(&detail).unwrap_or(json!({}));
1475            let event_json = json!({
1476                "version": "0",
1477                "id": event_id,
1478                "source": source,
1479                "account": req.account_id,
1480                "detail-type": detail_type,
1481                "detail": detail_value,
1482                "time": time.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
1483                "region": req.region,
1484                "resources": resources,
1485            });
1486
1487            let ctx = EventDispatchContext {
1488                state: &self.state,
1489                delivery: &self.delivery,
1490                lambda_state: self.lambda_state.as_ref(),
1491                logs_state: self.logs_state.as_ref(),
1492                container_runtime: &self.container_runtime,
1493                account_id: &req.account_id,
1494                region: &req.region,
1495            };
1496            for (rule_arn, target) in targets {
1497                dispatch_event_target(
1498                    &ctx,
1499                    &target,
1500                    &event_json,
1501                    &event_id,
1502                    &detail_type,
1503                    Some(&rule_arn),
1504                );
1505            }
1506        }
1507
1508        let resp = json!({
1509            "FailedEntryCount": failed_count,
1510            "Entries": result_entries,
1511        });
1512
1513        Ok(AwsResponse::ok_json(resp))
1514    }
1515
1516    // ─── Tagging ────────────────────────────────────────────────────────
1517
1518    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1519        let body = req.json_body();
1520        validate_required("ResourceARN", &body["ResourceARN"])?;
1521        let arn = body["ResourceARN"]
1522            .as_str()
1523            .ok_or_else(|| missing("ResourceARN"))?;
1524        validate_string_length("resourceARN", arn, 1, 1600)?;
1525        validate_required("Tags", &body["Tags"])?;
1526
1527        let mut accounts = self.state.write();
1528        let state = accounts.get_or_create(&req.account_id);
1529        let tag_map = find_tags_mut(state, arn)?;
1530
1531        fakecloud_core::tags::apply_tags(tag_map, &body, "Tags", "Key", "Value").map_err(|f| {
1532            AwsServiceError::aws_error(
1533                StatusCode::BAD_REQUEST,
1534                "ValidationException",
1535                format!("{f} must be a list"),
1536            )
1537        })?;
1538
1539        Ok(AwsResponse::ok_json(json!({})))
1540    }
1541
1542    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1543        let body = req.json_body();
1544        validate_required("ResourceARN", &body["ResourceARN"])?;
1545        let arn = body["ResourceARN"]
1546            .as_str()
1547            .ok_or_else(|| missing("ResourceARN"))?;
1548        validate_string_length("resourceARN", arn, 1, 1600)?;
1549        validate_required("TagKeys", &body["TagKeys"])?;
1550
1551        let mut accounts = self.state.write();
1552        let state = accounts.get_or_create(&req.account_id);
1553        let tag_map = find_tags_mut(state, arn)?;
1554
1555        fakecloud_core::tags::remove_tags(tag_map, &body, "TagKeys").map_err(|f| {
1556            AwsServiceError::aws_error(
1557                StatusCode::BAD_REQUEST,
1558                "ValidationException",
1559                format!("{f} must be a list"),
1560            )
1561        })?;
1562
1563        Ok(AwsResponse::ok_json(json!({})))
1564    }
1565
1566    fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1567        let body = req.json_body();
1568        validate_required("ResourceARN", &body["ResourceARN"])?;
1569        let arn = body["ResourceARN"]
1570            .as_str()
1571            .ok_or_else(|| missing("ResourceARN"))?;
1572        validate_string_length("resourceARN", arn, 1, 1600)?;
1573
1574        let accounts = self.state.read();
1575        let empty = EventBridgeState::new(&req.account_id, &req.region);
1576        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1577        let tag_map = find_tags(state, arn)?;
1578
1579        let tags = fakecloud_core::tags::tags_to_json(tag_map, "Key", "Value");
1580
1581        Ok(AwsResponse::ok_json(json!({ "Tags": tags })))
1582    }
1583
1584    // ─── Archive Operations ─────────────────────────────────────────────
1585}
1586
1587// ─── Tag Lookup Helpers ─────────────────────────────────────────────────
1588
1589// ─── Event Pattern Validation ────────────────────────────────────────
1590
1591// ─── Connection Auth Params Response Builder ────────────────────────
1592
1593// ─── Event Pattern Matching ─────────────────────────────────────────
1594
1595/// Parsed + validated inputs for `StartReplay`.
1596struct StartReplayInput {
1597    name: String,
1598    description: Option<String>,
1599    event_source_arn: String,
1600    destination: Value,
1601    destination_arn: String,
1602    event_start_time: DateTime<Utc>,
1603    event_end_time: DateTime<Utc>,
1604}
1605
1606impl StartReplayInput {
1607    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
1608        // StartReplay's Smithy model declares ResourceNotFound,
1609        // ResourceAlreadyExists, InvalidEventPattern, LimitExceeded, and
1610        // Internal — but not ValidationException. Per-field constraints are
1611        // enforced client-side by the SDK; we surface only declared errors
1612        // here. Missing required inputs default to empty strings and the
1613        // downstream bus/archive lookups produce ResourceNotFound for the
1614        // ones that matter.
1615        let name = body["ReplayName"].as_str().unwrap_or("").to_string();
1616        let description = body["Description"].as_str().map(|s| s.to_string());
1617        let event_source_arn = body["EventSourceArn"].as_str().unwrap_or("").to_string();
1618        let destination = body["Destination"].clone();
1619
1620        let event_start_time = body["EventStartTime"]
1621            .as_f64()
1622            .and_then(|f| DateTime::from_timestamp(f as i64, 0))
1623            .unwrap_or_else(Utc::now);
1624        let event_end_time = body["EventEndTime"]
1625            .as_f64()
1626            .and_then(|f| DateTime::from_timestamp(f as i64, 0))
1627            .unwrap_or_else(Utc::now);
1628
1629        let destination_arn = destination["Arn"].as_str().unwrap_or("").to_string();
1630        if !destination_arn.contains(":event-bus/") {
1631            // Missing/invalid destination cannot be resolved to a bus —
1632            // surface as ResourceNotFound rather than the undeclared
1633            // ValidationException.
1634            return Err(AwsServiceError::aws_error(
1635                StatusCode::BAD_REQUEST,
1636                "ResourceNotFoundException",
1637                format!("Destination.Arn {destination_arn} does not point to an event bus."),
1638            ));
1639        }
1640
1641        Ok(Self {
1642            name,
1643            description,
1644            event_source_arn,
1645            destination,
1646            destination_arn,
1647            event_start_time,
1648            event_end_time,
1649        })
1650    }
1651}
1652
1653#[path = "service_archives_replays.rs"]
1654mod service_archives_replays;
1655#[path = "service_connections_apidests.rs"]
1656mod service_connections_apidests;
1657#[path = "service_endpoints.rs"]
1658mod service_endpoints;
1659#[path = "service_partner_sources.rs"]
1660mod service_partner_sources;
1661
1662#[path = "helpers.rs"]
1663pub(crate) mod helpers;
1664pub(crate) use helpers::*;
1665
1666#[cfg(test)]
1667#[path = "service_tests.rs"]
1668mod tests;
1669
1670#[cfg(test)]
1671mod pagination_reject_test {
1672    #[test]
1673    fn paginate_checked_rejects_invalid_token() {
1674        use fakecloud_core::pagination::paginate_checked;
1675        let items: Vec<i32> = (0..5).collect();
1676        assert!(paginate_checked(&items, Some("bad"), 3).is_err());
1677        assert!(paginate_checked(&items, Some("2"), 3).is_ok());
1678    }
1679}