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