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        // Preserve the mutable bookkeeping that PutRule must NOT clobber when it
724        // updates an existing rule: attached targets, tags, and the internal
725        // managed_by/created_by markers. Real AWS PutRule updates the rule's
726        // definition fields (pattern/schedule/state/etc.) but leaves targets
727        // attached and leaves tags untouched unless the request carries Tags
728        // (tags are otherwise managed via TagResource/UntagResource).
729        let (targets, existing_tags, existing_managed_by, existing_created_by) = state
730            .rules
731            .get(&key)
732            .map(|r| {
733                (
734                    r.targets.clone(),
735                    r.tags.clone(),
736                    r.managed_by.clone(),
737                    r.created_by.clone(),
738                )
739            })
740            .unwrap_or_default();
741
742        // Only replace tags when the request explicitly supplies a Tags array.
743        // A PutRule that omits Tags preserves whatever was already attached;
744        // previously this always overwrote tags with an empty map, silently
745        // dropping tags on every update.
746        let tags = if body.get("Tags").map(|t| t.is_array()).unwrap_or(false) {
747            parse_tags(&body)
748        } else {
749            existing_tags
750        };
751
752        let rule = EventRule {
753            name: name.clone(),
754            arn: arn.clone(),
755            event_bus_name,
756            event_pattern,
757            schedule_expression,
758            state: rule_state,
759            description,
760            role_arn,
761            managed_by: existing_managed_by,
762            created_by: existing_created_by,
763            targets,
764            tags,
765            last_fired: None,
766        };
767
768        state.rules.insert(key, rule);
769        Ok(AwsResponse::ok_json(json!({ "RuleArn": arn })))
770    }
771
772    fn delete_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
773        let body = req.json_body();
774        validate_required("Name", &body["Name"])?;
775        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
776        validate_string_length("name", name, 1, 64)?;
777        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
778        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
779
780        let mut accounts = self.state.write();
781        let state = accounts.get_or_create(&req.account_id);
782        let bus_name = state.resolve_bus_name(event_bus_name);
783        let key = (bus_name, name.to_string());
784
785        // Check if rule has targets
786        if let Some(rule) = state.rules.get(&key) {
787            if !rule.targets.is_empty() {
788                return Err(AwsServiceError::aws_error(
789                    StatusCode::BAD_REQUEST,
790                    "ValidationException",
791                    "Rule can't be deleted since it has targets.",
792                ));
793            }
794        }
795
796        state.rules.remove(&key);
797        Ok(AwsResponse::ok_json(json!({})))
798    }
799
800    fn list_rules(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
801        let body = req.json_body();
802        validate_optional_string_length("namePrefix", body["NamePrefix"].as_str(), 1, 64)?;
803        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
804        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 2048)?;
805        validate_optional_range_i64("limit", body["Limit"].as_i64(), 1, 100)?;
806        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
807        let name_prefix = body["NamePrefix"].as_str();
808        let limit = body["Limit"].as_u64().map(|n| n as usize);
809        let next_token = body["NextToken"].as_str();
810
811        let accounts = self.state.read();
812        let empty = EventBridgeState::new(&req.account_id, &req.region);
813        let state = accounts.get(&req.account_id).unwrap_or(&empty);
814        let bus_name = state.resolve_bus_name(event_bus_name);
815
816        let mut rules: Vec<&EventRule> = state
817            .rules
818            .values()
819            .filter(|r| r.event_bus_name == bus_name)
820            .filter(|r| match name_prefix {
821                Some(prefix) => r.name.starts_with(prefix),
822                None => true,
823            })
824            .collect();
825        rules.sort_by(|a, b| a.name.cmp(&b.name));
826
827        // Pagination
828        let start = next_token
829            .and_then(|t| t.parse::<usize>().ok())
830            .unwrap_or(0)
831            .min(rules.len());
832        let rules_slice = &rules[start..];
833
834        let (page, new_next_token) = if let Some(lim) = limit {
835            if rules_slice.len() > lim {
836                (&rules_slice[..lim], Some((start + lim).to_string()))
837            } else {
838                (rules_slice, None)
839            }
840        } else {
841            (rules_slice, None)
842        };
843
844        let rules_json: Vec<Value> = page
845            .iter()
846            .map(|r| {
847                let mut obj = json!({
848                    "Name": r.name,
849                    "Arn": r.arn,
850                    "EventBusName": r.event_bus_name,
851                    "State": r.state,
852                });
853                if let Some(ref desc) = r.description {
854                    obj["Description"] = json!(desc);
855                }
856                if let Some(ref ep) = r.event_pattern {
857                    obj["EventPattern"] = json!(ep);
858                }
859                if let Some(ref se) = r.schedule_expression {
860                    obj["ScheduleExpression"] = json!(se);
861                }
862                if let Some(ref role) = r.role_arn {
863                    obj["RoleArn"] = json!(role);
864                }
865                if let Some(ref mb) = r.managed_by {
866                    obj["ManagedBy"] = json!(mb);
867                }
868                obj
869            })
870            .collect();
871
872        let mut resp = json!({ "Rules": rules_json });
873        if let Some(token) = new_next_token {
874            resp["NextToken"] = json!(token);
875        }
876
877        Ok(AwsResponse::ok_json(resp))
878    }
879
880    fn describe_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
881        let body = req.json_body();
882        validate_required("Name", &body["Name"])?;
883        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
884        validate_string_length("name", name, 1, 64)?;
885        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
886        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
887
888        let accounts = self.state.read();
889        let empty = EventBridgeState::new(&req.account_id, &req.region);
890        let state = accounts.get(&req.account_id).unwrap_or(&empty);
891        let bus_name = state.resolve_bus_name(event_bus_name);
892        let key = (bus_name.clone(), name.to_string());
893
894        let rule = state.rules.get(&key).ok_or_else(|| {
895            AwsServiceError::aws_error(
896                StatusCode::BAD_REQUEST,
897                "ResourceNotFoundException",
898                format!("Rule {name} does not exist."),
899            )
900        })?;
901
902        let mut resp = json!({
903            "Name": rule.name,
904            "Arn": rule.arn,
905            "EventBusName": rule.event_bus_name,
906            "State": rule.state,
907        });
908
909        if let Some(ref desc) = rule.description {
910            resp["Description"] = json!(desc);
911        }
912        if let Some(ref ep) = rule.event_pattern {
913            resp["EventPattern"] = json!(ep);
914        }
915        if let Some(ref se) = rule.schedule_expression {
916            resp["ScheduleExpression"] = json!(se);
917        }
918        if let Some(ref role) = rule.role_arn {
919            resp["RoleArn"] = json!(role);
920        }
921        if let Some(ref mb) = rule.managed_by {
922            resp["ManagedBy"] = json!(mb);
923        }
924        if let Some(ref cb) = rule.created_by {
925            resp["CreatedBy"] = json!(cb);
926        }
927        // If non-default bus, set CreatedBy to account_id
928        if rule.event_bus_name != "default" && rule.created_by.is_none() {
929            resp["CreatedBy"] = json!(state.account_id);
930        }
931
932        Ok(AwsResponse::ok_json(resp))
933    }
934
935    fn enable_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
936        let body = req.json_body();
937        validate_required("Name", &body["Name"])?;
938        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
939        validate_string_length("name", name, 1, 64)?;
940        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
941        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
942
943        let mut accounts = self.state.write();
944        let state = accounts.get_or_create(&req.account_id);
945        let bus_name = state.resolve_bus_name(event_bus_name);
946        let key = (bus_name, name.to_string());
947
948        let rule = state.rules.get_mut(&key).ok_or_else(|| {
949            AwsServiceError::aws_error(
950                StatusCode::BAD_REQUEST,
951                "ResourceNotFoundException",
952                format!("Rule {name} does not exist."),
953            )
954        })?;
955
956        rule.state = "ENABLED".to_string();
957        Ok(AwsResponse::ok_json(json!({})))
958    }
959
960    fn disable_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
961        let body = req.json_body();
962        validate_required("Name", &body["Name"])?;
963        let name = body["Name"].as_str().ok_or_else(|| missing("Name"))?;
964        validate_string_length("name", name, 1, 64)?;
965        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
966        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
967
968        let mut accounts = self.state.write();
969        let state = accounts.get_or_create(&req.account_id);
970        let bus_name = state.resolve_bus_name(event_bus_name);
971        let key = (bus_name, name.to_string());
972
973        let rule = state.rules.get_mut(&key).ok_or_else(|| {
974            AwsServiceError::aws_error(
975                StatusCode::BAD_REQUEST,
976                "ResourceNotFoundException",
977                format!("Rule {name} does not exist."),
978            )
979        })?;
980
981        rule.state = "DISABLED".to_string();
982        Ok(AwsResponse::ok_json(json!({})))
983    }
984
985    // ─── Target Operations ──────────────────────────────────────────────
986
987    fn put_targets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
988        let body = req.json_body();
989        // Smithy PutTargetsRequest constraints (top-level shape):
990        //   Rule: RuleName @required length 1..=64
991        //   EventBusName: EventBusNameOrArn length 1..=1600
992        //   Targets: TargetList @required length 1..=100
993        // Per-target validation still flows through `FailedEntries` (matching
994        // AWS); only the top-level shape produces ValidationException.
995        validate_required("Rule", &body["Rule"])?;
996        let rule_name = body["Rule"].as_str().ok_or_else(|| missing("Rule"))?;
997        validate_string_length("Rule", rule_name, 1, 64)?;
998        validate_optional_string_length_value("EventBusName", &body["EventBusName"], 1, 1600)?;
999        // Targets is @required with TargetList length 1..=100 in the Smithy
1000        // model. Real AWS rejects empty / oversized lists with a
1001        // ValidationException; per-target validation continues to flow
1002        // through FailedEntries (matching AWS).
1003        validate_required("Targets", &body["Targets"])?;
1004        let targets_array = body["Targets"].as_array().ok_or_else(|| {
1005            AwsServiceError::aws_error(
1006                StatusCode::BAD_REQUEST,
1007                "ValidationException",
1008                "Targets must be a list",
1009            )
1010        })?;
1011        if targets_array.is_empty() || targets_array.len() > 100 {
1012            return Err(AwsServiceError::aws_error(
1013                StatusCode::BAD_REQUEST,
1014                "ValidationException",
1015                "Value at 'Targets' failed to satisfy constraint: \
1016                 Member must have length between 1 and 100",
1017            ));
1018        }
1019        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
1020        let targets: Vec<Value> = targets_array.clone();
1021
1022        let mut accounts = self.state.write();
1023        let state = accounts.get_or_create(&req.account_id);
1024        let bus_name = state.resolve_bus_name(event_bus_name);
1025        let key = (bus_name.clone(), rule_name.to_string());
1026
1027        let rule = state.rules.get_mut(&key).ok_or_else(|| {
1028            AwsServiceError::aws_error(
1029                StatusCode::BAD_REQUEST,
1030                "ResourceNotFoundException",
1031                format!("Rule {rule_name} does not exist on EventBus {bus_name}."),
1032            )
1033        })?;
1034
1035        let mut failed_entries: Vec<Value> = Vec::new();
1036        for target in &targets {
1037            let target_id = target["Id"].as_str().unwrap_or("").to_string();
1038            let target_arn = target["Arn"].as_str().unwrap_or("");
1039
1040            if target_arn.ends_with(".fifo") && target.get("SqsParameters").is_none() {
1041                failed_entries.push(json!({
1042                    "TargetId": target_id,
1043                    "ErrorCode": "ValidationException",
1044                    "ErrorMessage": format!(
1045                        "Parameter(s) SqsParameters must be specified for target: {target_id}."
1046                    ),
1047                }));
1048                continue;
1049            }
1050            if !target_arn.starts_with("arn:") {
1051                failed_entries.push(json!({
1052                    "TargetId": target_id,
1053                    "ErrorCode": "ValidationException",
1054                    "ErrorMessage": format!(
1055                        "Parameter {target_arn} is not valid. Reason: Provided Arn is not in correct format."
1056                    ),
1057                }));
1058                continue;
1059            }
1060
1061            // Input, InputPath, and InputTransformer are mutually exclusive on a
1062            // single target. AWS surfaces a violation as a per-entry
1063            // FailedEntry (not a top-level ValidationException), which
1064            // increments FailedEntryCount. Previously all three were accepted
1065            // silently, letting a malformed target through and under-reporting
1066            // the failed count.
1067            let input_fields = ["Input", "InputPath", "InputTransformer"]
1068                .iter()
1069                .filter(|k| !target[**k].is_null())
1070                .count();
1071            if input_fields > 1 {
1072                failed_entries.push(json!({
1073                    "TargetId": target_id,
1074                    "ErrorCode": "ValidationException",
1075                    "ErrorMessage": format!(
1076                        "Only one of Input, InputPath, or InputTransformer can be \
1077                         specified for target: {target_id}."
1078                    ),
1079                }));
1080                continue;
1081            }
1082
1083            let et = parse_target(target);
1084            rule.targets.retain(|t| t.id != et.id);
1085            rule.targets.push(et);
1086        }
1087
1088        Ok(AwsResponse::ok_json(json!({
1089            "FailedEntryCount": failed_entries.len(),
1090            "FailedEntries": failed_entries,
1091        })))
1092    }
1093
1094    fn remove_targets(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1095        let body = req.json_body();
1096        validate_required("Rule", &body["Rule"])?;
1097        let rule_name = body["Rule"].as_str().ok_or_else(|| missing("Rule"))?;
1098        validate_string_length("rule", rule_name, 1, 64)?;
1099        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
1100        validate_required("Ids", &body["Ids"])?;
1101        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
1102        let ids = body["Ids"].as_array().ok_or_else(|| missing("Ids"))?;
1103
1104        let target_ids: Vec<String> = ids
1105            .iter()
1106            .filter_map(|v| v.as_str().map(|s| s.to_string()))
1107            .collect();
1108
1109        let mut accounts = self.state.write();
1110        let state = accounts.get_or_create(&req.account_id);
1111        let bus_name = state.resolve_bus_name(event_bus_name);
1112        let key = (bus_name.clone(), rule_name.to_string());
1113
1114        let rule = state.rules.get_mut(&key).ok_or_else(|| {
1115            AwsServiceError::aws_error(
1116                StatusCode::BAD_REQUEST,
1117                "ResourceNotFoundException",
1118                format!("Rule {rule_name} does not exist on EventBus {bus_name}."),
1119            )
1120        })?;
1121
1122        rule.targets.retain(|t| !target_ids.contains(&t.id));
1123
1124        Ok(AwsResponse::ok_json(json!({
1125            "FailedEntryCount": 0,
1126            "FailedEntries": [],
1127        })))
1128    }
1129
1130    fn list_targets_by_rule(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1131        let body = req.json_body();
1132        validate_required("Rule", &body["Rule"])?;
1133        let rule_name = body["Rule"].as_str().ok_or_else(|| missing("Rule"))?;
1134        validate_string_length("rule", rule_name, 1, 64)?;
1135        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
1136        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 2048)?;
1137        validate_optional_range_i64("limit", body["Limit"].as_i64(), 1, 100)?;
1138        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
1139        let limit = body["Limit"].as_u64().map(|n| n as usize);
1140        let next_token = body["NextToken"].as_str();
1141
1142        let accounts = self.state.read();
1143        let empty = EventBridgeState::new(&req.account_id, &req.region);
1144        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1145        let bus_name = state.resolve_bus_name(event_bus_name);
1146        let key = (bus_name, rule_name.to_string());
1147
1148        let rule = state.rules.get(&key).ok_or_else(|| {
1149            AwsServiceError::aws_error(
1150                StatusCode::BAD_REQUEST,
1151                "ResourceNotFoundException",
1152                format!("Rule {rule_name} does not exist."),
1153            )
1154        })?;
1155
1156        let all_targets = &rule.targets;
1157        let start = next_token
1158            .and_then(|t| t.parse::<usize>().ok())
1159            .unwrap_or(0)
1160            .min(all_targets.len());
1161        let slice = &all_targets[start..];
1162
1163        let (page, new_next_token) = if let Some(lim) = limit {
1164            if slice.len() > lim {
1165                (&slice[..lim], Some((start + lim).to_string()))
1166            } else {
1167                (slice, None)
1168            }
1169        } else {
1170            (slice, None)
1171        };
1172
1173        let targets: Vec<Value> = page.iter().map(target_to_json).collect();
1174
1175        let mut resp = json!({ "Targets": targets });
1176        if let Some(token) = new_next_token {
1177            resp["NextToken"] = json!(token);
1178        }
1179
1180        Ok(AwsResponse::ok_json(resp))
1181    }
1182
1183    fn list_rule_names_by_target(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1184        let body = req.json_body();
1185        validate_required("TargetArn", &body["TargetArn"])?;
1186        let target_arn = body["TargetArn"]
1187            .as_str()
1188            .ok_or_else(|| missing("TargetArn"))?;
1189        validate_string_length("targetArn", target_arn, 1, 1600)?;
1190        validate_optional_string_length("eventBusName", body["EventBusName"].as_str(), 1, 1600)?;
1191        validate_optional_string_length("nextToken", body["NextToken"].as_str(), 1, 2048)?;
1192        validate_optional_range_i64("limit", body["Limit"].as_i64(), 1, 100)?;
1193        let event_bus_name = body["EventBusName"].as_str().unwrap_or("default");
1194        let limit = body["Limit"].as_u64().map(|n| n as usize);
1195        let next_token = body["NextToken"].as_str();
1196
1197        let accounts = self.state.read();
1198        let empty = EventBridgeState::new(&req.account_id, &req.region);
1199        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1200        let bus_name = state.resolve_bus_name(event_bus_name);
1201
1202        // Deduplicate rule names
1203        let mut rule_names: Vec<String> = Vec::new();
1204        for rule in state.rules.values() {
1205            if rule.event_bus_name == bus_name
1206                && rule.targets.iter().any(|t| t.arn == target_arn)
1207                && !rule_names.contains(&rule.name)
1208            {
1209                rule_names.push(rule.name.clone());
1210            }
1211        }
1212        rule_names.sort();
1213
1214        let start = next_token
1215            .and_then(|t| t.parse::<usize>().ok())
1216            .unwrap_or(0)
1217            .min(rule_names.len());
1218        let slice = &rule_names[start..];
1219
1220        let (page, new_next_token) = if let Some(lim) = limit {
1221            if slice.len() > lim {
1222                (&slice[..lim], Some((start + lim).to_string()))
1223            } else {
1224                (slice, None)
1225            }
1226        } else {
1227            (slice, None)
1228        };
1229
1230        let mut resp = json!({ "RuleNames": page });
1231        if let Some(token) = new_next_token {
1232            resp["NextToken"] = json!(token);
1233        }
1234
1235        Ok(AwsResponse::ok_json(resp))
1236    }
1237
1238    // ─── Partner Event Sources ────────────���───────────────────────────
1239
1240    fn test_event_pattern(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1241        let body = req.json_body();
1242        validate_required("EventPattern", &body["EventPattern"])?;
1243        validate_required("Event", &body["Event"])?;
1244        let event_pattern = body["EventPattern"]
1245            .as_str()
1246            .ok_or_else(|| missing("EventPattern"))?;
1247        let event_str = body["Event"].as_str().ok_or_else(|| missing("Event"))?;
1248
1249        // Parse the event JSON
1250        let event: Value = serde_json::from_str(event_str).map_err(|_| {
1251            AwsServiceError::aws_error(
1252                StatusCode::BAD_REQUEST,
1253                "InvalidEventPatternException",
1254                "Event is not valid JSON.",
1255            )
1256        })?;
1257
1258        // Parse the pattern JSON
1259        let pattern: Value = serde_json::from_str(event_pattern).map_err(|_| {
1260            AwsServiceError::aws_error(
1261                StatusCode::BAD_REQUEST,
1262                "InvalidEventPatternException",
1263                "Event pattern is not valid JSON.",
1264            )
1265        })?;
1266
1267        // Reject an invalid pattern (scalar leaf, malformed numeric matcher,
1268        // etc.) with InvalidEventPatternException instead of silently
1269        // returning {"Result": false}.
1270        validate_pattern_values(&pattern, "")?;
1271
1272        // Match directly against the caller-supplied event so all of its
1273        // fields — including id / time / version — are matchable, rather than
1274        // reconstructing a partial synthetic event that dropped them.
1275        let result = matches_value(&pattern, &event);
1276
1277        Ok(AwsResponse::ok_json(json!({ "Result": result })))
1278    }
1279
1280    // ─── UpdateEventBus ─────────────────────────────────────────────────
1281
1282    fn update_event_bus(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1283        let body = req.json_body();
1284        validate_optional_string_length("description", body["Description"].as_str(), 0, 512)?;
1285        validate_optional_string_length(
1286            "kmsKeyIdentifier",
1287            body["KmsKeyIdentifier"].as_str(),
1288            0,
1289            2048,
1290        )?;
1291        let name = body["Name"].as_str().unwrap_or("default");
1292
1293        let mut accounts = self.state.write();
1294        let state = accounts.get_or_create(&req.account_id);
1295        let bus = state.buses.get_mut(name).ok_or_else(|| {
1296            AwsServiceError::aws_error(
1297                StatusCode::BAD_REQUEST,
1298                "ResourceNotFoundException",
1299                format!("Event bus {name} does not exist."),
1300            )
1301        })?;
1302
1303        if let Some(desc) = body["Description"].as_str() {
1304            bus.description = Some(desc.to_string());
1305        }
1306        if let Some(kms) = body["KmsKeyIdentifier"].as_str() {
1307            bus.kms_key_identifier = Some(kms.to_string());
1308        }
1309        if let Some(dlc) = body.get("DeadLetterConfig") {
1310            bus.dead_letter_config = Some(dlc.clone());
1311        }
1312        bus.last_modified_time = Utc::now();
1313
1314        let arn = bus.arn.clone();
1315        let bus_name = bus.name.clone();
1316
1317        Ok(AwsResponse::ok_json(json!({
1318            "Arn": arn,
1319            "Name": bus_name,
1320        })))
1321    }
1322
1323    // ─── Endpoint Operations ────────────────────────────────────────────
1324
1325    fn put_events(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1326        let body = req.json_body();
1327        // Smithy PutEventsRequest constraints:
1328        //   EndpointId: length 1..=50
1329        //   Entries: @required, length 1..=10
1330        validate_optional_string_length_value("EndpointId", &body["EndpointId"], 1, 50)?;
1331        validate_required("Entries", &body["Entries"])?;
1332        let entries_array = body["Entries"].as_array().ok_or_else(|| {
1333            AwsServiceError::aws_error(
1334                StatusCode::BAD_REQUEST,
1335                "ValidationException",
1336                "Entries must be a list",
1337            )
1338        })?;
1339        if entries_array.is_empty() || entries_array.len() > 10 {
1340            return Err(AwsServiceError::aws_error(
1341                StatusCode::BAD_REQUEST,
1342                "ValidationException",
1343                "Value at 'Entries' failed to satisfy constraint: \
1344                 Member must have length between 1 and 10",
1345            ));
1346        }
1347        let entries: Vec<Value> = entries_array.clone();
1348        let entries = &entries;
1349
1350        let mut accounts = self.state.write();
1351        let state = accounts.get_or_create(&req.account_id);
1352        let mut result_entries = Vec::new();
1353        let mut events_to_deliver = Vec::new();
1354        let mut failed_count = 0;
1355
1356        for entry in entries {
1357            let source = entry["Source"].as_str().unwrap_or("").to_string();
1358            let detail_type = entry["DetailType"].as_str().unwrap_or("").to_string();
1359            let detail = entry["Detail"].as_str().unwrap_or("").to_string();
1360
1361            if let Err(error) = validate_put_events_entry(&source, &detail_type, &detail) {
1362                failed_count += 1;
1363                result_entries.push(error);
1364                continue;
1365            }
1366
1367            let event_id = uuid::Uuid::new_v4().to_string();
1368            let raw_bus = entry["EventBusName"]
1369                .as_str()
1370                .unwrap_or("default")
1371                .to_string();
1372            let event_bus_name = state.resolve_bus_name(&raw_bus);
1373
1374            // Bus resource-policy gate. AWS evaluates the bus's
1375            // resource policy against cross-account callers; same-account
1376            // callers always have access. The policy itself is JSON
1377            // stored as serde_json::Value so the IAM evaluator parses
1378            // it the same way it parses an S3 bucket policy.
1379            let caller_account = req
1380                .principal
1381                .as_ref()
1382                .map(|p| p.account_id.as_str())
1383                .unwrap_or(req.account_id.as_str());
1384            if caller_account != req.account_id {
1385                let bus_policy_value = state
1386                    .buses
1387                    .get(&event_bus_name)
1388                    .and_then(|b| b.policy.clone());
1389                if let Some(policy_value) = bus_policy_value {
1390                    let policy_json = serde_json::to_string(&policy_value).unwrap_or_default();
1391                    let policy_doc = fakecloud_iam::evaluator::PolicyDocument::parse(&policy_json);
1392                    let bus_arn = state
1393                        .buses
1394                        .get(&event_bus_name)
1395                        .map(|b| b.arn.clone())
1396                        .unwrap_or_default();
1397                    let principal =
1398                        req.principal
1399                            .clone()
1400                            .unwrap_or_else(|| fakecloud_core::auth::Principal {
1401                                arn: Arn::global("iam", caller_account, "root").to_string(),
1402                                user_id: caller_account.to_string(),
1403                                account_id: caller_account.to_string(),
1404                                principal_type: fakecloud_core::auth::PrincipalType::Root,
1405                                source_identity: None,
1406                                tags: None,
1407                            });
1408                    let context = fakecloud_iam::evaluator::RequestContext {
1409                        aws_principal_arn: Some(principal.arn.clone()),
1410                        aws_principal_account: Some(principal.account_id.clone()),
1411                        ..Default::default()
1412                    };
1413                    let eval_req = fakecloud_iam::evaluator::EvalRequest {
1414                        principal: &principal,
1415                        action: "events:PutEvents".to_string(),
1416                        resource: bus_arn,
1417                        context,
1418                    };
1419                    let decision = fakecloud_iam::evaluator::evaluate_resource_policy_only(
1420                        &policy_doc,
1421                        &eval_req,
1422                    );
1423                    if !matches!(decision, fakecloud_iam::evaluator::Decision::Allow) {
1424                        failed_count += 1;
1425                        result_entries.push(json!({
1426                            "ErrorCode": "AccessDeniedException",
1427                            "ErrorMessage": format!(
1428                                "User '{}' is not authorized to put events on event bus '{}'",
1429                                principal.arn, event_bus_name
1430                            ),
1431                        }));
1432                        continue;
1433                    }
1434                }
1435            }
1436
1437            let time = parse_put_events_time(&entry["Time"]);
1438            let resources: Vec<String> = entry["Resources"]
1439                .as_array()
1440                .map(|arr| {
1441                    arr.iter()
1442                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
1443                        .collect()
1444                })
1445                .unwrap_or_default();
1446
1447            let event = PutEvent {
1448                event_id: event_id.clone(),
1449                source: source.clone(),
1450                detail_type: detail_type.clone(),
1451                detail: detail.clone(),
1452                event_bus_name: event_bus_name.clone(),
1453                time,
1454                resources: resources.clone(),
1455            };
1456
1457            archive_matching_event(
1458                state,
1459                &event,
1460                &event_bus_name,
1461                &source,
1462                &detail_type,
1463                &detail,
1464                &req.account_id,
1465                &req.region,
1466                &resources,
1467            );
1468
1469            state.events.push(event);
1470
1471            // Find matching rules and their targets, carrying each rule's ARN
1472            // so the InputTransformer can resolve `<aws.events.rule-arn>`.
1473            let matching_targets: Vec<(String, EventTarget)> = state
1474                .rules
1475                .values()
1476                .filter(|r| {
1477                    r.event_bus_name == event_bus_name
1478                        && r.state == "ENABLED"
1479                        && matches_pattern(
1480                            r.event_pattern.as_deref(),
1481                            &source,
1482                            &detail_type,
1483                            &detail,
1484                            &req.account_id,
1485                            &req.region,
1486                            &resources,
1487                            &event_id,
1488                            &time.to_rfc3339(),
1489                        )
1490                })
1491                .flat_map(|r| r.targets.iter().map(|t| (r.arn.clone(), t.clone())))
1492                .collect();
1493
1494            if !matching_targets.is_empty() {
1495                events_to_deliver.push((
1496                    event_id.clone(),
1497                    source,
1498                    detail_type,
1499                    detail,
1500                    time,
1501                    resources,
1502                    matching_targets,
1503                ));
1504            }
1505
1506            result_entries.push(json!({ "EventId": event_id }));
1507        }
1508
1509        // Drop the lock before delivering
1510        drop(accounts);
1511
1512        // Deliver to targets — single-target dispatch lives in the
1513        // shared helper so cross-service callers (delivery.rs) honor the
1514        // same target shape (SQS/SNS/Lambda/Logs/Kinesis/StepFunctions/
1515        // ApiDestination/HTTP) and the same InputTransformer rules.
1516        for (event_id, source, detail_type, detail, time, resources, targets) in events_to_deliver {
1517            let detail_value: Value = serde_json::from_str(&detail).unwrap_or(json!({}));
1518            let event_json = json!({
1519                "version": "0",
1520                "id": event_id,
1521                "source": source,
1522                "account": req.account_id,
1523                "detail-type": detail_type,
1524                "detail": detail_value,
1525                "time": time.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
1526                "region": req.region,
1527                "resources": resources,
1528            });
1529
1530            let ctx = EventDispatchContext {
1531                state: &self.state,
1532                delivery: &self.delivery,
1533                lambda_state: self.lambda_state.as_ref(),
1534                logs_state: self.logs_state.as_ref(),
1535                container_runtime: &self.container_runtime,
1536                account_id: &req.account_id,
1537                region: &req.region,
1538            };
1539            for (rule_arn, target) in targets {
1540                dispatch_event_target(
1541                    &ctx,
1542                    &target,
1543                    &event_json,
1544                    &event_id,
1545                    &detail_type,
1546                    Some(&rule_arn),
1547                );
1548            }
1549        }
1550
1551        let resp = json!({
1552            "FailedEntryCount": failed_count,
1553            "Entries": result_entries,
1554        });
1555
1556        Ok(AwsResponse::ok_json(resp))
1557    }
1558
1559    // ─── Tagging ────────────────────────────────────────────────────────
1560
1561    fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1562        let body = req.json_body();
1563        validate_required("ResourceARN", &body["ResourceARN"])?;
1564        let arn = body["ResourceARN"]
1565            .as_str()
1566            .ok_or_else(|| missing("ResourceARN"))?;
1567        validate_string_length("resourceARN", arn, 1, 1600)?;
1568        validate_required("Tags", &body["Tags"])?;
1569
1570        let mut accounts = self.state.write();
1571        let state = accounts.get_or_create(&req.account_id);
1572        let tag_map = find_tags_mut(state, arn)?;
1573
1574        fakecloud_core::tags::apply_tags(tag_map, &body, "Tags", "Key", "Value").map_err(|f| {
1575            AwsServiceError::aws_error(
1576                StatusCode::BAD_REQUEST,
1577                "ValidationException",
1578                format!("{f} must be a list"),
1579            )
1580        })?;
1581
1582        Ok(AwsResponse::ok_json(json!({})))
1583    }
1584
1585    fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1586        let body = req.json_body();
1587        validate_required("ResourceARN", &body["ResourceARN"])?;
1588        let arn = body["ResourceARN"]
1589            .as_str()
1590            .ok_or_else(|| missing("ResourceARN"))?;
1591        validate_string_length("resourceARN", arn, 1, 1600)?;
1592        validate_required("TagKeys", &body["TagKeys"])?;
1593
1594        let mut accounts = self.state.write();
1595        let state = accounts.get_or_create(&req.account_id);
1596        let tag_map = find_tags_mut(state, arn)?;
1597
1598        fakecloud_core::tags::remove_tags(tag_map, &body, "TagKeys").map_err(|f| {
1599            AwsServiceError::aws_error(
1600                StatusCode::BAD_REQUEST,
1601                "ValidationException",
1602                format!("{f} must be a list"),
1603            )
1604        })?;
1605
1606        Ok(AwsResponse::ok_json(json!({})))
1607    }
1608
1609    fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1610        let body = req.json_body();
1611        validate_required("ResourceARN", &body["ResourceARN"])?;
1612        let arn = body["ResourceARN"]
1613            .as_str()
1614            .ok_or_else(|| missing("ResourceARN"))?;
1615        validate_string_length("resourceARN", arn, 1, 1600)?;
1616
1617        let accounts = self.state.read();
1618        let empty = EventBridgeState::new(&req.account_id, &req.region);
1619        let state = accounts.get(&req.account_id).unwrap_or(&empty);
1620        let tag_map = find_tags(state, arn)?;
1621
1622        let tags = fakecloud_core::tags::tags_to_json(tag_map, "Key", "Value");
1623
1624        Ok(AwsResponse::ok_json(json!({ "Tags": tags })))
1625    }
1626
1627    // ─── Archive Operations ─────────────────────────────────────────────
1628}
1629
1630// ─── Tag Lookup Helpers ─────────────────────────────────────────────────
1631
1632// ─── Event Pattern Validation ────────────────────────────────────────
1633
1634// ─── Connection Auth Params Response Builder ────────────────────────
1635
1636// ─── Event Pattern Matching ─────────────────────────────────────────
1637
1638/// Parsed + validated inputs for `StartReplay`.
1639struct StartReplayInput {
1640    name: String,
1641    description: Option<String>,
1642    event_source_arn: String,
1643    destination: Value,
1644    destination_arn: String,
1645    event_start_time: DateTime<Utc>,
1646    event_end_time: DateTime<Utc>,
1647}
1648
1649impl StartReplayInput {
1650    fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
1651        // StartReplay's Smithy model declares ResourceNotFound,
1652        // ResourceAlreadyExists, InvalidEventPattern, LimitExceeded, and
1653        // Internal — but not ValidationException. Per-field constraints are
1654        // enforced client-side by the SDK; we surface only declared errors
1655        // here. Missing required inputs default to empty strings and the
1656        // downstream bus/archive lookups produce ResourceNotFound for the
1657        // ones that matter.
1658        let name = body["ReplayName"].as_str().unwrap_or("").to_string();
1659        let description = body["Description"].as_str().map(|s| s.to_string());
1660        let event_source_arn = body["EventSourceArn"].as_str().unwrap_or("").to_string();
1661        let destination = body["Destination"].clone();
1662
1663        let event_start_time = body["EventStartTime"]
1664            .as_f64()
1665            .and_then(|f| DateTime::from_timestamp(f as i64, 0))
1666            .unwrap_or_else(Utc::now);
1667        let event_end_time = body["EventEndTime"]
1668            .as_f64()
1669            .and_then(|f| DateTime::from_timestamp(f as i64, 0))
1670            .unwrap_or_else(Utc::now);
1671
1672        let destination_arn = destination["Arn"].as_str().unwrap_or("").to_string();
1673        if !destination_arn.contains(":event-bus/") {
1674            // Missing/invalid destination cannot be resolved to a bus —
1675            // surface as ResourceNotFound rather than the undeclared
1676            // ValidationException.
1677            return Err(AwsServiceError::aws_error(
1678                StatusCode::BAD_REQUEST,
1679                "ResourceNotFoundException",
1680                format!("Destination.Arn {destination_arn} does not point to an event bus."),
1681            ));
1682        }
1683
1684        Ok(Self {
1685            name,
1686            description,
1687            event_source_arn,
1688            destination,
1689            destination_arn,
1690            event_start_time,
1691            event_end_time,
1692        })
1693    }
1694}
1695
1696#[path = "service_archives_replays.rs"]
1697mod service_archives_replays;
1698#[path = "service_connections_apidests.rs"]
1699mod service_connections_apidests;
1700#[path = "service_endpoints.rs"]
1701mod service_endpoints;
1702#[path = "service_partner_sources.rs"]
1703mod service_partner_sources;
1704
1705#[path = "helpers.rs"]
1706pub(crate) mod helpers;
1707pub(crate) use helpers::*;
1708
1709#[cfg(test)]
1710#[path = "service_tests.rs"]
1711mod tests;
1712
1713#[cfg(test)]
1714mod pagination_reject_test {
1715    #[test]
1716    fn paginate_checked_rejects_invalid_token() {
1717        use fakecloud_core::pagination::paginate_checked;
1718        let items: Vec<i32> = (0..5).collect();
1719        assert!(paginate_checked(&items, Some("bad"), 3).is_err());
1720        assert!(paginate_checked(&items, Some("2"), 3).is_ok());
1721    }
1722}