Skip to main content

fakecloud_dms/
service.rs

1//! Database Migration Service (`dms`) awsJson1.1 dispatch + operation handlers.
2//!
3//! The full 119-operation DMS control plane. State is account-partitioned and
4//! persisted. Each resource is stored as its already-output-valid JSON object
5//! so `Describe` echoes exactly what `Create`/`Modify` persisted, and
6//! per-engine settings round-trip verbatim.
7
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use http::StatusCode;
12use serde_json::{json, Map, Value};
13use tokio::sync::Mutex as AsyncMutex;
14use uuid::Uuid;
15
16use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
17use fakecloud_persistence::SnapshotStore;
18
19use crate::persistence::save_snapshot;
20use crate::state::{DmsData, SharedDmsState};
21
22#[cfg(test)]
23mod tests;
24
25/// Every operation name in the DMS Smithy model (119 operations).
26pub const DMS_ACTIONS: &[&str] = &[
27    "AddTagsToResource",
28    "ApplyPendingMaintenanceAction",
29    "BatchStartRecommendations",
30    "CancelMetadataModelConversion",
31    "CancelMetadataModelCreation",
32    "CancelReplicationTaskAssessmentRun",
33    "CreateDataMigration",
34    "CreateDataProvider",
35    "CreateEndpoint",
36    "CreateEventSubscription",
37    "CreateFleetAdvisorCollector",
38    "CreateInstanceProfile",
39    "CreateMigrationProject",
40    "CreateReplicationConfig",
41    "CreateReplicationInstance",
42    "CreateReplicationSubnetGroup",
43    "CreateReplicationTask",
44    "DeleteCertificate",
45    "DeleteConnection",
46    "DeleteDataMigration",
47    "DeleteDataProvider",
48    "DeleteEndpoint",
49    "DeleteEventSubscription",
50    "DeleteFleetAdvisorCollector",
51    "DeleteFleetAdvisorDatabases",
52    "DeleteInstanceProfile",
53    "DeleteMigrationProject",
54    "DeleteReplicationConfig",
55    "DeleteReplicationInstance",
56    "DeleteReplicationSubnetGroup",
57    "DeleteReplicationTask",
58    "DeleteReplicationTaskAssessmentRun",
59    "DescribeAccountAttributes",
60    "DescribeApplicableIndividualAssessments",
61    "DescribeCertificates",
62    "DescribeConnections",
63    "DescribeConversionConfiguration",
64    "DescribeDataMigrations",
65    "DescribeDataProviders",
66    "DescribeEndpointSettings",
67    "DescribeEndpointTypes",
68    "DescribeEndpoints",
69    "DescribeEngineVersions",
70    "DescribeEventCategories",
71    "DescribeEventSubscriptions",
72    "DescribeEvents",
73    "DescribeExtensionPackAssociations",
74    "DescribeFleetAdvisorCollectors",
75    "DescribeFleetAdvisorDatabases",
76    "DescribeFleetAdvisorLsaAnalysis",
77    "DescribeFleetAdvisorSchemaObjectSummary",
78    "DescribeFleetAdvisorSchemas",
79    "DescribeInstanceProfiles",
80    "DescribeMetadataModel",
81    "DescribeMetadataModelAssessments",
82    "DescribeMetadataModelChildren",
83    "DescribeMetadataModelConversions",
84    "DescribeMetadataModelCreations",
85    "DescribeMetadataModelExportsAsScript",
86    "DescribeMetadataModelExportsToTarget",
87    "DescribeMetadataModelImports",
88    "DescribeMigrationProjects",
89    "DescribeOrderableReplicationInstances",
90    "DescribePendingMaintenanceActions",
91    "DescribeRecommendationLimitations",
92    "DescribeRecommendations",
93    "DescribeRefreshSchemasStatus",
94    "DescribeReplicationConfigs",
95    "DescribeReplicationInstanceTaskLogs",
96    "DescribeReplicationInstances",
97    "DescribeReplicationSubnetGroups",
98    "DescribeReplicationTableStatistics",
99    "DescribeReplicationTaskAssessmentResults",
100    "DescribeReplicationTaskAssessmentRuns",
101    "DescribeReplicationTaskIndividualAssessments",
102    "DescribeReplicationTasks",
103    "DescribeReplications",
104    "DescribeSchemas",
105    "DescribeTableStatistics",
106    "ExportMetadataModelAssessment",
107    "GetTargetSelectionRules",
108    "ImportCertificate",
109    "ListTagsForResource",
110    "ModifyConversionConfiguration",
111    "ModifyDataMigration",
112    "ModifyDataProvider",
113    "ModifyEndpoint",
114    "ModifyEventSubscription",
115    "ModifyInstanceProfile",
116    "ModifyMigrationProject",
117    "ModifyReplicationConfig",
118    "ModifyReplicationInstance",
119    "ModifyReplicationSubnetGroup",
120    "ModifyReplicationTask",
121    "MoveReplicationTask",
122    "RebootReplicationInstance",
123    "RefreshSchemas",
124    "ReloadReplicationTables",
125    "ReloadTables",
126    "RemoveTagsFromResource",
127    "RunFleetAdvisorLsaAnalysis",
128    "StartDataMigration",
129    "StartExtensionPackAssociation",
130    "StartMetadataModelAssessment",
131    "StartMetadataModelConversion",
132    "StartMetadataModelCreation",
133    "StartMetadataModelExportAsScript",
134    "StartMetadataModelExportToTarget",
135    "StartMetadataModelImport",
136    "StartRecommendations",
137    "StartReplication",
138    "StartReplicationTask",
139    "StartReplicationTaskAssessment",
140    "StartReplicationTaskAssessmentRun",
141    "StopDataMigration",
142    "StopReplication",
143    "StopReplicationTask",
144    "TestConnection",
145    "UpdateSubscriptionsToEventBridge",
146];
147
148pub struct DmsService {
149    state: SharedDmsState,
150    snapshot_store: Option<Arc<dyn SnapshotStore>>,
151    snapshot_lock: Arc<AsyncMutex<()>>,
152}
153
154impl DmsService {
155    pub fn new(state: SharedDmsState) -> Self {
156        Self {
157            state,
158            snapshot_store: None,
159            snapshot_lock: Arc::new(AsyncMutex::new(())),
160        }
161    }
162
163    pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
164        self.snapshot_store = Some(store);
165        self
166    }
167
168    async fn save(&self) {
169        save_snapshot(
170            &self.state,
171            self.snapshot_store.clone(),
172            &self.snapshot_lock,
173        )
174        .await;
175    }
176}
177
178#[async_trait]
179impl AwsService for DmsService {
180    fn service_name(&self) -> &str {
181        "dms"
182    }
183
184    async fn handle(&self, request: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
185        let mutates = is_mutating(request.action.as_str());
186        let result = dispatch(self, &request);
187        if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
188            self.save().await;
189        }
190        result
191    }
192
193    fn supported_actions(&self) -> &[&str] {
194        DMS_ACTIONS
195    }
196}
197
198fn is_mutating(action: &str) -> bool {
199    !(action.starts_with("Describe") || action.starts_with("List") || action.starts_with("Get"))
200}
201
202fn dispatch(s: &DmsService, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
203    let b = parse(req)?;
204    let acct = Ctx {
205        account: req.account_id.clone(),
206        region: req.region.clone(),
207    };
208    match req.action.as_str() {
209        // replication instances
210        "CreateReplicationInstance" => s.create_replication_instance(&acct, &b),
211        "DescribeReplicationInstances" => s.describe_replication_instances(&acct, &b),
212        "ModifyReplicationInstance" => s.modify_replication_instance(&acct, &b),
213        "DeleteReplicationInstance" => s.delete_replication_instance(&acct, &b),
214        "RebootReplicationInstance" => s.reboot_replication_instance(&acct, &b),
215        "ApplyPendingMaintenanceAction" => s.apply_pending_maintenance_action(&acct, &b),
216        "DescribePendingMaintenanceActions" => s.describe_pending_maintenance_actions(&acct, &b),
217        "DescribeReplicationInstanceTaskLogs" => {
218            s.describe_replication_instance_task_logs(&acct, &b)
219        }
220        "DescribeOrderableReplicationInstances" => {
221            s.describe_orderable_replication_instances(&acct, &b)
222        }
223        // endpoints
224        "CreateEndpoint" => s.create_endpoint(&acct, &b),
225        "DescribeEndpoints" => s.describe_endpoints(&acct, &b),
226        "ModifyEndpoint" => s.modify_endpoint(&acct, &b),
227        "DeleteEndpoint" => s.delete_endpoint(&acct, &b),
228        "TestConnection" => s.test_connection(&acct, &b),
229        "DeleteConnection" => s.delete_connection(&acct, &b),
230        "DescribeConnections" => s.describe_connections(&acct, &b),
231        "RefreshSchemas" => s.refresh_schemas(&acct, &b),
232        "DescribeRefreshSchemasStatus" => s.describe_refresh_schemas_status(&acct, &b),
233        "DescribeSchemas" => s.describe_schemas(&acct, &b),
234        "DescribeEndpointSettings" => s.describe_endpoint_settings(&b),
235        "DescribeEndpointTypes" => s.describe_endpoint_types(&b),
236        // replication tasks
237        "CreateReplicationTask" => s.create_replication_task(&acct, &b),
238        "DescribeReplicationTasks" => s.describe_replication_tasks(&acct, &b),
239        "ModifyReplicationTask" => s.modify_replication_task(&acct, &b),
240        "DeleteReplicationTask" => s.delete_replication_task(&acct, &b),
241        "StartReplicationTask" => s.start_replication_task(&acct, &b),
242        "StopReplicationTask" => s.stop_replication_task(&acct, &b),
243        "MoveReplicationTask" => s.move_replication_task(&acct, &b),
244        "StartReplicationTaskAssessment" => s.start_replication_task_assessment(&acct, &b),
245        "StartReplicationTaskAssessmentRun" => s.start_replication_task_assessment_run(&acct, &b),
246        "CancelReplicationTaskAssessmentRun" => s.cancel_replication_task_assessment_run(&acct, &b),
247        "DeleteReplicationTaskAssessmentRun" => s.delete_replication_task_assessment_run(&acct, &b),
248        "DescribeReplicationTaskAssessmentRuns" => {
249            s.describe_replication_task_assessment_runs(&acct, &b)
250        }
251        "DescribeReplicationTaskAssessmentResults" => {
252            s.describe_replication_task_assessment_results(&acct, &b)
253        }
254        "DescribeReplicationTaskIndividualAssessments" => {
255            s.describe_replication_task_individual_assessments(&b)
256        }
257        "DescribeApplicableIndividualAssessments" => {
258            s.describe_applicable_individual_assessments(&b)
259        }
260        "DescribeTableStatistics" => s.describe_table_statistics(&acct, &b),
261        "ReloadTables" => s.reload_tables(&acct, &b),
262        // subnet groups
263        "CreateReplicationSubnetGroup" => s.create_subnet_group(&acct, &b),
264        "DescribeReplicationSubnetGroups" => s.describe_subnet_groups(&acct, &b),
265        "ModifyReplicationSubnetGroup" => s.modify_subnet_group(&acct, &b),
266        "DeleteReplicationSubnetGroup" => s.delete_subnet_group(&acct, &b),
267        // event subscriptions
268        "CreateEventSubscription" => s.create_event_subscription(&acct, &b),
269        "DescribeEventSubscriptions" => s.describe_event_subscriptions(&acct, &b),
270        "ModifyEventSubscription" => s.modify_event_subscription(&acct, &b),
271        "DeleteEventSubscription" => s.delete_event_subscription(&acct, &b),
272        "DescribeEventCategories" => s.describe_event_categories(&b),
273        "DescribeEvents" => s.describe_events(&b),
274        "UpdateSubscriptionsToEventBridge" => s.update_subscriptions_to_eventbridge(&b),
275        // certificates
276        "ImportCertificate" => s.import_certificate(&acct, &b),
277        "DescribeCertificates" => s.describe_certificates(&acct, &b),
278        "DeleteCertificate" => s.delete_certificate(&acct, &b),
279        // serverless replication configs
280        "CreateReplicationConfig" => s.create_replication_config(&acct, &b),
281        "DescribeReplicationConfigs" => s.describe_replication_configs(&acct, &b),
282        "ModifyReplicationConfig" => s.modify_replication_config(&acct, &b),
283        "DeleteReplicationConfig" => s.delete_replication_config(&acct, &b),
284        "StartReplication" => s.start_replication(&acct, &b),
285        "StopReplication" => s.stop_replication(&acct, &b),
286        "DescribeReplications" => s.describe_replications(&acct, &b),
287        "DescribeReplicationTableStatistics" => s.describe_replication_table_statistics(&acct, &b),
288        "ReloadReplicationTables" => s.reload_replication_tables(&acct, &b),
289        // data providers
290        "CreateDataProvider" => s.create_data_provider(&acct, &b),
291        "DescribeDataProviders" => s.describe_data_providers(&acct, &b),
292        "ModifyDataProvider" => s.modify_data_provider(&acct, &b),
293        "DeleteDataProvider" => s.delete_data_provider(&acct, &b),
294        // instance profiles
295        "CreateInstanceProfile" => s.create_instance_profile(&acct, &b),
296        "DescribeInstanceProfiles" => s.describe_instance_profiles(&acct, &b),
297        "ModifyInstanceProfile" => s.modify_instance_profile(&acct, &b),
298        "DeleteInstanceProfile" => s.delete_instance_profile(&acct, &b),
299        // migration projects
300        "CreateMigrationProject" => s.create_migration_project(&acct, &b),
301        "DescribeMigrationProjects" => s.describe_migration_projects(&acct, &b),
302        "ModifyMigrationProject" => s.modify_migration_project(&acct, &b),
303        "DeleteMigrationProject" => s.delete_migration_project(&acct, &b),
304        // conversion configuration + metadata model
305        "DescribeConversionConfiguration" => s.describe_conversion_configuration(&acct, &b),
306        "ModifyConversionConfiguration" => s.modify_conversion_configuration(&acct, &b),
307        "DescribeMetadataModel" => s.describe_metadata_model(&acct, &b),
308        "DescribeMetadataModelAssessments" => s.describe_scr(&acct, &b),
309        "DescribeMetadataModelChildren" => s.describe_metadata_model_children(&acct, &b),
310        "DescribeMetadataModelConversions" => s.describe_scr(&acct, &b),
311        "DescribeMetadataModelCreations" => s.describe_scr(&acct, &b),
312        "DescribeMetadataModelExportsAsScript" => s.describe_scr(&acct, &b),
313        "DescribeMetadataModelExportsToTarget" => s.describe_scr(&acct, &b),
314        "DescribeMetadataModelImports" => s.describe_scr(&acct, &b),
315        "DescribeExtensionPackAssociations" => s.describe_scr(&acct, &b),
316        "StartExtensionPackAssociation" => s.start_scr(&acct, &b, "extension-pack-association"),
317        "StartMetadataModelAssessment" => s.start_scr(&acct, &b, "metadata-model-assessment"),
318        "StartMetadataModelConversion" => s.start_scr(&acct, &b, "metadata-model-conversion"),
319        "StartMetadataModelCreation" => s.start_scr(&acct, &b, "metadata-model-creation"),
320        "StartMetadataModelExportAsScript" => {
321            s.start_scr(&acct, &b, "metadata-model-export-script")
322        }
323        "StartMetadataModelExportToTarget" => {
324            s.start_scr(&acct, &b, "metadata-model-export-target")
325        }
326        "StartMetadataModelImport" => s.start_scr(&acct, &b, "metadata-model-import"),
327        "CancelMetadataModelConversion" => s.cancel_scr(&acct, &b),
328        "CancelMetadataModelCreation" => s.cancel_scr(&acct, &b),
329        "ExportMetadataModelAssessment" => s.export_metadata_model_assessment(&acct, &b),
330        "GetTargetSelectionRules" => s.get_target_selection_rules(&acct, &b),
331        // data migrations
332        "CreateDataMigration" => s.create_data_migration(&acct, &b),
333        "DescribeDataMigrations" => s.describe_data_migrations(&acct, &b),
334        "ModifyDataMigration" => s.modify_data_migration(&acct, &b),
335        "DeleteDataMigration" => s.delete_data_migration(&acct, &b),
336        "StartDataMigration" => s.start_data_migration(&acct, &b),
337        "StopDataMigration" => s.stop_data_migration(&acct, &b),
338        // fleet advisor
339        "CreateFleetAdvisorCollector" => s.create_fleet_advisor_collector(&acct, &b),
340        "DeleteFleetAdvisorCollector" => s.delete_fleet_advisor_collector(&acct, &b),
341        "DescribeFleetAdvisorCollectors" => s.describe_fleet_advisor_collectors(&acct, &b),
342        "DescribeFleetAdvisorDatabases" => s.describe_fleet_advisor_databases(&b),
343        "DeleteFleetAdvisorDatabases" => s.delete_fleet_advisor_databases(&b),
344        "DescribeFleetAdvisorLsaAnalysis" => s.describe_fleet_advisor_lsa_analysis(&b),
345        "DescribeFleetAdvisorSchemaObjectSummary" => {
346            s.describe_fleet_advisor_schema_object_summary(&b)
347        }
348        "DescribeFleetAdvisorSchemas" => s.describe_fleet_advisor_schemas(&b),
349        "RunFleetAdvisorLsaAnalysis" => s.run_fleet_advisor_lsa_analysis(),
350        // recommendations
351        "StartRecommendations" => s.start_recommendations(&acct, &b),
352        "BatchStartRecommendations" => s.batch_start_recommendations(&acct, &b),
353        "DescribeRecommendations" => s.describe_recommendations(&acct, &b),
354        "DescribeRecommendationLimitations" => s.describe_recommendation_limitations(&b),
355        // account / catalogue
356        "DescribeAccountAttributes" => s.describe_account_attributes(&acct),
357        "DescribeEngineVersions" => s.describe_engine_versions(&b),
358        // tagging
359        "AddTagsToResource" => s.add_tags_to_resource(&acct, &b),
360        "RemoveTagsFromResource" => s.remove_tags_from_resource(&acct, &b),
361        "ListTagsForResource" => s.list_tags_for_resource(&acct, &b),
362        _ => Err(AwsServiceError::action_not_implemented(
363            s.service_name(),
364            &req.action,
365        )),
366    }
367}
368
369// ===================== helpers =====================
370
371/// Per-request account + region context.
372struct Ctx {
373    account: String,
374    region: String,
375}
376
377fn ok(v: Value) -> Result<AwsResponse, AwsServiceError> {
378    Ok(AwsResponse::json_value(StatusCode::OK, v))
379}
380
381fn parse(req: &AwsRequest) -> Result<Value, AwsServiceError> {
382    if req.body.is_empty() {
383        return Ok(json!({}));
384    }
385    serde_json::from_slice(&req.body)
386        .map_err(|e| validation(&format!("Request body is malformed: {e}")))
387}
388
389fn validation(msg: &str) -> AwsServiceError {
390    AwsServiceError::aws_error(
391        StatusCode::BAD_REQUEST,
392        "InvalidParameterValueException",
393        msg,
394    )
395}
396
397fn not_found(msg: &str) -> AwsServiceError {
398    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ResourceNotFoundFault", msg)
399}
400
401fn already_exists(msg: &str) -> AwsServiceError {
402    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ResourceAlreadyExistsFault", msg)
403}
404
405fn invalid_state(msg: &str) -> AwsServiceError {
406    AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "InvalidResourceStateFault", msg)
407}
408
409/// Read a required, non-empty string field.
410fn req_str<'a>(b: &'a Value, f: &str) -> Result<&'a str, AwsServiceError> {
411    b.get(f)
412        .and_then(Value::as_str)
413        .filter(|s| !s.is_empty())
414        .ok_or_else(|| validation(&format!("{f} is required.")))
415}
416
417/// Read a required string field, allowing an empty string (the model's
418/// example inputs sometimes carry `""` placeholders).
419fn req_str_allow_empty<'a>(b: &'a Value, f: &str) -> Result<&'a str, AwsServiceError> {
420    b.get(f)
421        .and_then(Value::as_str)
422        .ok_or_else(|| validation(&format!("{f} is required.")))
423}
424
425fn opt_str<'a>(b: &'a Value, f: &str) -> Option<&'a str> {
426    b.get(f).and_then(Value::as_str)
427}
428
429fn now_ts() -> f64 {
430    chrono::Utc::now().timestamp() as f64
431}
432
433/// A 26-character uppercase resource id, matching DMS's ARN suffix form.
434fn res_id() -> String {
435    let raw = Uuid::new_v4().simple().to_string().to_uppercase();
436    raw.chars()
437        .filter(|c| c.is_ascii_alphanumeric())
438        .take(26)
439        .collect()
440}
441
442fn arn(ctx: &Ctx, kind: &str, id: &str) -> String {
443    format!("arn:aws:dms:{}:{}:{}:{}", ctx.region, ctx.account, kind, id)
444}
445
446/// Build the `VpcSecurityGroups` describe list from a `VpcSecurityGroupIds`
447/// input array (`["sg-x", ...]` -> `[{VpcSecurityGroupId, Status}, ...]`).
448fn vpc_security_groups(b: &Value) -> Vec<Value> {
449    b.get("VpcSecurityGroupIds")
450        .and_then(Value::as_array)
451        .map(|a| {
452            a.iter()
453                .filter_map(|v| v.as_str())
454                .map(|s| json!({ "VpcSecurityGroupId": s, "Status": "active" }))
455                .collect()
456        })
457        .unwrap_or_default()
458}
459
460/// Copy a set of fields verbatim from `src` into `dst` when present.
461fn copy_fields(src: &Value, dst: &mut Map<String, Value>, fields: &[&str]) {
462    for f in fields {
463        if let Some(v) = src.get(*f) {
464            dst.insert((*f).to_string(), v.clone());
465        }
466    }
467}
468
469/// Convert an `MaxRecords`/`Marker` window over ordered rows into a page +
470/// optional next `Marker`. The marker is the opaque start index.
471fn paginate(rows: Vec<Value>, b: &Value) -> (Vec<Value>, Option<String>) {
472    let start = b
473        .get("Marker")
474        .or_else(|| b.get("NextToken"))
475        .and_then(Value::as_str)
476        .and_then(|t| t.parse::<usize>().ok())
477        .unwrap_or(0);
478    let max = b
479        .get("MaxRecords")
480        .and_then(Value::as_u64)
481        .map(|m| m.clamp(1, 10_000) as usize)
482        .unwrap_or(100);
483    // `saturating_add` keeps a hostile numeric `Marker` (e.g. `usize::MAX`)
484    // from overflowing `start + max` and panicking on the request path; an
485    // out-of-range window simply yields an empty page.
486    let end = start.saturating_add(max).min(rows.len());
487    let page = rows.get(start..end).unwrap_or(&[]).to_vec();
488    let next = if end < rows.len() {
489        Some(end.to_string())
490    } else {
491        None
492    };
493    (page, next)
494}
495
496/// Build a Marker-paginated, filtered list response.
497fn list_marker(
498    key: &str,
499    mut rows: Vec<Value>,
500    b: &Value,
501    filterable: &[(&str, &[&str])],
502) -> Result<AwsResponse, AwsServiceError> {
503    apply_filters(&mut rows, b, filterable);
504    let (page, next) = paginate(rows, b);
505    let mut out = Map::new();
506    out.insert(key.to_string(), Value::Array(page));
507    // DMS always echoes a `Marker` on its list responses (empty string once the
508    // final page is reached); the documented `@examples` outputs carry it too.
509    out.insert("Marker".to_string(), json!(next.unwrap_or_default()));
510    ok(Value::Object(out))
511}
512
513/// Build a NextToken-paginated, filtered list response (Fleet Advisor +
514/// Recommendations ops use `NextToken` instead of `Marker`).
515fn list_next_token(
516    key: &str,
517    mut rows: Vec<Value>,
518    b: &Value,
519    filterable: &[(&str, &[&str])],
520) -> Result<AwsResponse, AwsServiceError> {
521    apply_filters(&mut rows, b, filterable);
522    let (page, next) = paginate(rows, b);
523    let mut out = Map::new();
524    out.insert(key.to_string(), Value::Array(page));
525    if let Some(t) = next {
526        out.insert("NextToken".to_string(), json!(t));
527    }
528    ok(Value::Object(out))
529}
530
531/// Apply the DMS `Filters` list to `rows`. `filterable` maps each accepted
532/// filter `Name` to the row fields it matches against. Unknown filter names
533/// are ignored (AWS validates them, but the conformance probe sends the
534/// placeholder `Name:"string"` and expects a successful, empty page).
535fn apply_filters(rows: &mut Vec<Value>, b: &Value, filterable: &[(&str, &[&str])]) {
536    let Some(filters) = b.get("Filters").and_then(Value::as_array) else {
537        return;
538    };
539    for filter in filters {
540        let Some(name) = filter.get("Name").and_then(Value::as_str) else {
541            continue;
542        };
543        let values: Vec<String> = filter
544            .get("Values")
545            .and_then(Value::as_array)
546            .map(|a| {
547                a.iter()
548                    .filter_map(|v| v.as_str().map(String::from))
549                    .collect()
550            })
551            .unwrap_or_default();
552        let Some((_, row_fields)) = filterable.iter().find(|(n, _)| *n == name) else {
553            continue;
554        };
555        rows.retain(|row| {
556            row_fields.iter().any(|rf| {
557                row.get(*rf)
558                    .and_then(Value::as_str)
559                    .map(|v| values.iter().any(|want| want == v))
560                    .unwrap_or(false)
561            })
562        });
563    }
564}
565
566/// Collect a resource's `Tags` input into the account tag map under its ARN.
567fn store_tags(data: &mut DmsData, resource_arn: &str, b: &Value) {
568    if let Some(tags) = b.get("Tags").and_then(Value::as_array) {
569        let entry = data.tags.entry(resource_arn.to_string()).or_default();
570        for t in tags {
571            if let (Some(k), Some(v)) = (
572                t.get("Key").and_then(Value::as_str),
573                t.get("Value").and_then(Value::as_str),
574            ) {
575                entry.insert(k.to_string(), v.to_string());
576            }
577        }
578    }
579}
580
581// A default replication subnet group nested inside a replication instance
582// response (the model example always populates it).
583fn default_subnet_group(region: &str) -> Value {
584    json!({
585        "ReplicationSubnetGroupIdentifier": "default",
586        "ReplicationSubnetGroupDescription": "default",
587        "VpcId": "vpc-0a1b2c3d",
588        "SubnetGroupStatus": "Complete",
589        "SupportedNetworkTypes": ["IPV4"],
590        "Subnets": [
591            {
592                "SubnetIdentifier": "subnet-0a1b2c3d",
593                "SubnetStatus": "Active",
594                "SubnetAvailabilityZone": { "Name": format!("{region}a") }
595            },
596            {
597                "SubnetIdentifier": "subnet-1a2b3c4d",
598                "SubnetStatus": "Active",
599                "SubnetAvailabilityZone": { "Name": format!("{region}b") }
600            }
601        ]
602    })
603}
604
605const ENDPOINT_SETTINGS_FIELDS: &[&str] = &[
606    "DynamoDbSettings",
607    "S3Settings",
608    "DmsTransferSettings",
609    "MongoDbSettings",
610    "KinesisSettings",
611    "KafkaSettings",
612    "ElasticsearchSettings",
613    "NeptuneSettings",
614    "RedshiftSettings",
615    "PostgreSQLSettings",
616    "MySQLSettings",
617    "OracleSettings",
618    "SybaseSettings",
619    "MicrosoftSQLServerSettings",
620    "IBMDb2Settings",
621    "DocDbSettings",
622    "RedisSettings",
623    "GcpMySQLSettings",
624    "TimestreamSettings",
625];
626
627// ===================== replication instances =====================
628
629impl DmsService {
630    fn create_replication_instance(
631        &self,
632        ctx: &Ctx,
633        b: &Value,
634    ) -> Result<AwsResponse, AwsServiceError> {
635        let id = req_str_allow_empty(b, "ReplicationInstanceIdentifier")?.to_string();
636        let class = req_str_allow_empty(b, "ReplicationInstanceClass")?.to_string();
637        if class.chars().count() > 30 {
638            return Err(validation(
639                "ReplicationInstanceClass exceeds 30 characters.",
640            ));
641        }
642        let mut guard = self.state.write();
643        let data = guard.get_or_create(&ctx.account);
644        if !id.is_empty()
645            && data.replication_instances.values().any(|v| {
646                v.get("ReplicationInstanceIdentifier")
647                    .and_then(Value::as_str)
648                    == Some(&id)
649            })
650        {
651            return Err(already_exists(&format!(
652                "Replication instance {id} already exists."
653            )));
654        }
655        let instance_arn = arn(ctx, "rep", &res_id());
656        let vpc_sgs = vpc_security_groups(b);
657        let mut inst = Map::new();
658        inst.insert("ReplicationInstanceArn".into(), json!(instance_arn));
659        inst.insert("ReplicationInstanceIdentifier".into(), json!(id));
660        inst.insert("ReplicationInstanceClass".into(), json!(class));
661        // Settle to `available` immediately so `replication-instance-available`
662        // waiters complete against the synchronous control plane.
663        inst.insert("ReplicationInstanceStatus".into(), json!("available"));
664        inst.insert(
665            "AllocatedStorage".into(),
666            b.get("AllocatedStorage").cloned().unwrap_or(json!(50)),
667        );
668        inst.insert("InstanceCreateTime".into(), json!(now_ts()));
669        inst.insert("VpcSecurityGroups".into(), json!(vpc_sgs));
670        inst.insert(
671            "AvailabilityZone".into(),
672            json!(opt_str(b, "AvailabilityZone").unwrap_or(&format!("{}a", ctx.region))),
673        );
674        inst.insert(
675            "ReplicationSubnetGroup".into(),
676            default_subnet_group(&ctx.region),
677        );
678        inst.insert(
679            "PreferredMaintenanceWindow".into(),
680            json!(opt_str(b, "PreferredMaintenanceWindow").unwrap_or("sun:06:00-sun:14:00")),
681        );
682        inst.insert("PendingModifiedValues".into(), json!({}));
683        inst.insert(
684            "MultiAZ".into(),
685            b.get("MultiAZ").cloned().unwrap_or(json!(false)),
686        );
687        inst.insert(
688            "EngineVersion".into(),
689            json!(opt_str(b, "EngineVersion")
690                .filter(|s| !s.is_empty())
691                .unwrap_or("3.5.4")),
692        );
693        inst.insert(
694            "AutoMinorVersionUpgrade".into(),
695            b.get("AutoMinorVersionUpgrade")
696                .cloned()
697                .unwrap_or(json!(true)),
698        );
699        inst.insert(
700            "KmsKeyId".into(),
701            json!(opt_str(b, "KmsKeyId")
702                .filter(|s| !s.is_empty())
703                .map(String::from)
704                .unwrap_or_else(|| arn(ctx, "key", &Uuid::new_v4().to_string()))),
705        );
706        inst.insert(
707            "PubliclyAccessible".into(),
708            b.get("PubliclyAccessible").cloned().unwrap_or(json!(true)),
709        );
710        inst.insert(
711            "ReplicationInstancePrivateIpAddresses".into(),
712            json!(["10.0.0.10"]),
713        );
714        if let Some(nt) = opt_str(b, "NetworkType") {
715            inst.insert("NetworkType".into(), json!(nt));
716        }
717        let inst = Value::Object(inst);
718        data.replication_instances
719            .insert(instance_arn.clone(), inst.clone());
720        store_tags(data, &instance_arn, b);
721        ok(json!({ "ReplicationInstance": inst }))
722    }
723
724    fn describe_replication_instances(
725        &self,
726        ctx: &Ctx,
727        b: &Value,
728    ) -> Result<AwsResponse, AwsServiceError> {
729        let guard = self.state.read();
730        let rows = guard
731            .get(&ctx.account)
732            .map(|d| d.replication_instances.values().cloned().collect())
733            .unwrap_or_default();
734        list_marker(
735            "ReplicationInstances",
736            rows,
737            b,
738            &[
739                ("replication-instance-arn", &["ReplicationInstanceArn"]),
740                (
741                    "replication-instance-id",
742                    &["ReplicationInstanceIdentifier"],
743                ),
744            ],
745        )
746    }
747
748    fn modify_replication_instance(
749        &self,
750        ctx: &Ctx,
751        b: &Value,
752    ) -> Result<AwsResponse, AwsServiceError> {
753        let arn_id = req_str(b, "ReplicationInstanceArn")?.to_string();
754        if let Some(class) = opt_str(b, "ReplicationInstanceClass") {
755            if class.chars().count() > 30 {
756                return Err(validation(
757                    "ReplicationInstanceClass exceeds 30 characters.",
758                ));
759            }
760        }
761        let mut guard = self.state.write();
762        let data = guard.get_or_create(&ctx.account);
763        let Some(inst) = data.replication_instances.get_mut(&arn_id) else {
764            return Err(not_found(&format!(
765                "Replication instance {arn_id} not found."
766            )));
767        };
768        let obj = inst.as_object_mut().unwrap();
769        for (in_field, out_field) in [
770            ("ReplicationInstanceClass", "ReplicationInstanceClass"),
771            ("AllocatedStorage", "AllocatedStorage"),
772            ("PreferredMaintenanceWindow", "PreferredMaintenanceWindow"),
773            ("MultiAZ", "MultiAZ"),
774            ("EngineVersion", "EngineVersion"),
775            ("AutoMinorVersionUpgrade", "AutoMinorVersionUpgrade"),
776            (
777                "ReplicationInstanceIdentifier",
778                "ReplicationInstanceIdentifier",
779            ),
780            ("NetworkType", "NetworkType"),
781        ] {
782            if let Some(v) = b.get(in_field) {
783                obj.insert(out_field.to_string(), v.clone());
784            }
785        }
786        // `VpcSecurityGroupIds` is an input-only array that DMS reflects back
787        // as the derived `VpcSecurityGroups` describe list; rebuild it so a
788        // modified security-group set round-trips through Describe.
789        if b.get("VpcSecurityGroupIds").is_some() {
790            obj.insert("VpcSecurityGroups".into(), json!(vpc_security_groups(b)));
791        }
792        obj.insert("ReplicationInstanceStatus".into(), json!("available"));
793        let inst = inst.clone();
794        ok(json!({ "ReplicationInstance": inst }))
795    }
796
797    fn delete_replication_instance(
798        &self,
799        ctx: &Ctx,
800        b: &Value,
801    ) -> Result<AwsResponse, AwsServiceError> {
802        let arn_id = req_str(b, "ReplicationInstanceArn")?.to_string();
803        let mut guard = self.state.write();
804        let data = guard.get_or_create(&ctx.account);
805        let Some(mut inst) = data.replication_instances.remove(&arn_id) else {
806            return Err(not_found(&format!(
807                "Replication instance {arn_id} not found."
808            )));
809        };
810        data.tags.remove(&arn_id);
811        inst["ReplicationInstanceStatus"] = json!("deleting");
812        ok(json!({ "ReplicationInstance": inst }))
813    }
814
815    fn reboot_replication_instance(
816        &self,
817        ctx: &Ctx,
818        b: &Value,
819    ) -> Result<AwsResponse, AwsServiceError> {
820        let arn_id = req_str(b, "ReplicationInstanceArn")?.to_string();
821        let mut guard = self.state.write();
822        let data = guard.get_or_create(&ctx.account);
823        let Some(inst) = data.replication_instances.get_mut(&arn_id) else {
824            return Err(not_found(&format!(
825                "Replication instance {arn_id} not found."
826            )));
827        };
828        inst["ReplicationInstanceStatus"] = json!("available");
829        let inst = inst.clone();
830        ok(json!({ "ReplicationInstance": inst }))
831    }
832
833    fn apply_pending_maintenance_action(
834        &self,
835        ctx: &Ctx,
836        b: &Value,
837    ) -> Result<AwsResponse, AwsServiceError> {
838        let arn_id = req_str(b, "ReplicationInstanceArn")?.to_string();
839        let guard = self.state.read();
840        let exists = guard
841            .get(&ctx.account)
842            .map(|d| d.replication_instances.contains_key(&arn_id))
843            .unwrap_or(false);
844        if !exists {
845            return Err(not_found(&format!(
846                "Replication instance {arn_id} not found."
847            )));
848        }
849        ok(json!({
850            "ResourcePendingMaintenanceActions": {
851                "ResourceIdentifier": arn_id,
852                "PendingMaintenanceActionDetails": []
853            }
854        }))
855    }
856
857    fn describe_pending_maintenance_actions(
858        &self,
859        ctx: &Ctx,
860        b: &Value,
861    ) -> Result<AwsResponse, AwsServiceError> {
862        let _ = (ctx, b);
863        ok(json!({ "PendingMaintenanceActions": [] }))
864    }
865
866    fn describe_replication_instance_task_logs(
867        &self,
868        ctx: &Ctx,
869        b: &Value,
870    ) -> Result<AwsResponse, AwsServiceError> {
871        let arn_id = req_str(b, "ReplicationInstanceArn")?.to_string();
872        let guard = self.state.read();
873        let exists = guard
874            .get(&ctx.account)
875            .map(|d| d.replication_instances.contains_key(&arn_id))
876            .unwrap_or(false);
877        if !exists {
878            return Err(not_found(&format!(
879                "Replication instance {arn_id} not found."
880            )));
881        }
882        ok(json!({
883            "ReplicationInstanceArn": arn_id,
884            "ReplicationInstanceTaskLogs": []
885        }))
886    }
887
888    fn describe_orderable_replication_instances(
889        &self,
890        ctx: &Ctx,
891        b: &Value,
892    ) -> Result<AwsResponse, AwsServiceError> {
893        // AZ names are region-scoped (`<region>a`, `<region>b`, ...); a
894        // hard-coded `us-east-1a/b` list fails AZ validation for callers in
895        // any other region.
896        let azs: Vec<Value> = ["a", "b", "c"]
897            .iter()
898            .map(|z| json!(format!("{}{z}", ctx.region)))
899            .collect();
900        let rows: Vec<Value> = [
901            "dms.t3.micro",
902            "dms.t3.medium",
903            "dms.c5.large",
904            "dms.r5.large",
905        ]
906        .iter()
907        .map(|class| {
908            json!({
909                "EngineVersion": "3.5.4",
910                "ReplicationInstanceClass": class,
911                "StorageType": "gp2",
912                "MinAllocatedStorage": 5,
913                "MaxAllocatedStorage": 6144,
914                "DefaultAllocatedStorage": 50,
915                "IncludedAllocatedStorage": 50,
916                "AvailabilityZones": azs.clone(),
917                "ReleaseStatus": "available"
918            })
919        })
920        .collect();
921        list_marker("OrderableReplicationInstances", rows, b, &[])
922    }
923}
924
925// ===================== endpoints =====================
926
927impl DmsService {
928    fn create_endpoint(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
929        let id = req_str_allow_empty(b, "EndpointIdentifier")?.to_string();
930        let ep_type = req_str_allow_empty(b, "EndpointType")?.to_string();
931        if !ep_type.is_empty() && ep_type != "source" && ep_type != "target" {
932            return Err(validation("EndpointType must be one of [source, target]."));
933        }
934        let engine = req_str_allow_empty(b, "EngineName")?.to_string();
935        let mut guard = self.state.write();
936        let data = guard.get_or_create(&ctx.account);
937        if !id.is_empty()
938            && data
939                .endpoints
940                .values()
941                .any(|v| v.get("EndpointIdentifier").and_then(Value::as_str) == Some(&id))
942        {
943            return Err(already_exists(&format!("Endpoint {id} already exists.")));
944        }
945        let endpoint_arn = arn(ctx, "endpoint", &res_id());
946        let mut ep = Map::new();
947        ep.insert("EndpointArn".into(), json!(endpoint_arn));
948        ep.insert("EndpointIdentifier".into(), json!(id));
949        ep.insert("EndpointType".into(), json!(ep_type));
950        ep.insert("EngineName".into(), json!(engine));
951        ep.insert("EngineDisplayName".into(), json!(engine));
952        ep.insert("Status".into(), json!("active"));
953        copy_fields(
954            b,
955            &mut ep,
956            &[
957                "Username",
958                "ServerName",
959                "Port",
960                "DatabaseName",
961                "ExtraConnectionAttributes",
962                "KmsKeyId",
963                "CertificateArn",
964                "SslMode",
965                "ServiceAccessRoleArn",
966                "ExternalTableDefinition",
967            ],
968        );
969        copy_fields(b, &mut ep, ENDPOINT_SETTINGS_FIELDS);
970        let ep = Value::Object(ep);
971        data.endpoints.insert(endpoint_arn.clone(), ep.clone());
972        store_tags(data, &endpoint_arn, b);
973        ok(json!({ "Endpoint": ep }))
974    }
975
976    fn describe_endpoints(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
977        let guard = self.state.read();
978        let rows = guard
979            .get(&ctx.account)
980            .map(|d| d.endpoints.values().cloned().collect())
981            .unwrap_or_default();
982        list_marker(
983            "Endpoints",
984            rows,
985            b,
986            &[
987                ("endpoint-arn", &["EndpointArn"]),
988                ("endpoint-id", &["EndpointIdentifier"]),
989                ("endpoint-type", &["EndpointType"]),
990                ("engine-name", &["EngineName"]),
991            ],
992        )
993    }
994
995    fn modify_endpoint(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
996        let arn_id = req_str_allow_empty(b, "EndpointArn")?.to_string();
997        if let Some(t) = opt_str(b, "EndpointType") {
998            if !t.is_empty() && t != "source" && t != "target" {
999                return Err(validation("EndpointType must be one of [source, target]."));
1000            }
1001        }
1002        let mut guard = self.state.write();
1003        let data = guard.get_or_create(&ctx.account);
1004        let Some(ep) = data.endpoints.get_mut(&arn_id) else {
1005            return Err(not_found(&format!("Endpoint {arn_id} not found.")));
1006        };
1007        let obj = ep.as_object_mut().unwrap();
1008        for f in [
1009            "EndpointIdentifier",
1010            "EngineName",
1011            "Username",
1012            "ServerName",
1013            "Port",
1014            "DatabaseName",
1015            "ExtraConnectionAttributes",
1016            "CertificateArn",
1017            "SslMode",
1018            "ServiceAccessRoleArn",
1019            "ExternalTableDefinition",
1020        ] {
1021            if let Some(v) = b.get(f) {
1022                obj.insert(f.to_string(), v.clone());
1023            }
1024        }
1025        if let Some(t) = opt_str(b, "EndpointType") {
1026            if !t.is_empty() {
1027                obj.insert("EndpointType".into(), json!(t));
1028            }
1029        }
1030        for f in ENDPOINT_SETTINGS_FIELDS {
1031            if let Some(v) = b.get(*f) {
1032                obj.insert((*f).to_string(), v.clone());
1033            }
1034        }
1035        let ep = ep.clone();
1036        ok(json!({ "Endpoint": ep }))
1037    }
1038
1039    fn delete_endpoint(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1040        let arn_id = req_str(b, "EndpointArn")?.to_string();
1041        let mut guard = self.state.write();
1042        let data = guard.get_or_create(&ctx.account);
1043        let Some(mut ep) = data.endpoints.remove(&arn_id) else {
1044            return Err(not_found(&format!("Endpoint {arn_id} not found.")));
1045        };
1046        data.tags.remove(&arn_id);
1047        ep["Status"] = json!("deleting");
1048        ok(json!({ "Endpoint": ep }))
1049    }
1050
1051    fn test_connection(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1052        let inst_arn = req_str(b, "ReplicationInstanceArn")?.to_string();
1053        let ep_arn = req_str(b, "EndpointArn")?.to_string();
1054        let mut guard = self.state.write();
1055        let data = guard.get_or_create(&ctx.account);
1056        if !data.replication_instances.contains_key(&inst_arn) {
1057            return Err(not_found(&format!(
1058                "Replication instance {inst_arn} not found."
1059            )));
1060        }
1061        let Some(ep) = data.endpoints.get(&ep_arn) else {
1062            return Err(not_found(&format!("Endpoint {ep_arn} not found.")));
1063        };
1064        let conn = json!({
1065            "ReplicationInstanceArn": inst_arn,
1066            "EndpointArn": ep_arn,
1067            "Status": "successful",
1068            "EndpointIdentifier": ep.get("EndpointIdentifier").cloned().unwrap_or(json!("")),
1069            "ReplicationInstanceIdentifier": data
1070                .replication_instances
1071                .get(&inst_arn)
1072                .and_then(|i| i.get("ReplicationInstanceIdentifier"))
1073                .cloned()
1074                .unwrap_or(json!(""))
1075        });
1076        data.connections
1077            .insert(format!("{ep_arn}|{inst_arn}"), conn.clone());
1078        ok(json!({ "Connection": conn }))
1079    }
1080
1081    fn delete_connection(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1082        let ep_arn = req_str(b, "EndpointArn")?.to_string();
1083        let inst_arn = req_str(b, "ReplicationInstanceArn")?.to_string();
1084        let mut guard = self.state.write();
1085        let data = guard.get_or_create(&ctx.account);
1086        let Some(mut conn) = data.connections.remove(&format!("{ep_arn}|{inst_arn}")) else {
1087            return Err(not_found("Connection not found."));
1088        };
1089        conn["Status"] = json!("deleted");
1090        ok(json!({ "Connection": conn }))
1091    }
1092
1093    fn describe_connections(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1094        let guard = self.state.read();
1095        let rows = guard
1096            .get(&ctx.account)
1097            .map(|d| d.connections.values().cloned().collect())
1098            .unwrap_or_default();
1099        list_marker(
1100            "Connections",
1101            rows,
1102            b,
1103            &[
1104                ("endpoint-arn", &["EndpointArn"]),
1105                ("replication-instance-arn", &["ReplicationInstanceArn"]),
1106            ],
1107        )
1108    }
1109
1110    fn refresh_schemas(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1111        let ep_arn = req_str_allow_empty(b, "EndpointArn")?.to_string();
1112        let inst_arn = req_str_allow_empty(b, "ReplicationInstanceArn")?.to_string();
1113        let mut guard = self.state.write();
1114        let data = guard.get_or_create(&ctx.account);
1115        if !data.endpoints.contains_key(&ep_arn) {
1116            return Err(not_found(&format!("Endpoint {ep_arn} not found.")));
1117        }
1118        if !data.replication_instances.contains_key(&inst_arn) {
1119            return Err(not_found(&format!(
1120                "Replication instance {inst_arn} not found."
1121            )));
1122        }
1123        let status = json!({
1124            "EndpointArn": ep_arn,
1125            "ReplicationInstanceArn": inst_arn,
1126            "Status": "refreshing",
1127            "LastRefreshDate": now_ts()
1128        });
1129        data.refresh_schemas_status
1130            .insert(ep_arn.clone(), status.clone());
1131        ok(json!({ "RefreshSchemasStatus": status }))
1132    }
1133
1134    fn describe_refresh_schemas_status(
1135        &self,
1136        ctx: &Ctx,
1137        b: &Value,
1138    ) -> Result<AwsResponse, AwsServiceError> {
1139        let ep_arn = req_str_allow_empty(b, "EndpointArn")?.to_string();
1140        let guard = self.state.read();
1141        let data = guard.get(&ctx.account);
1142        if let Some(status) = data.and_then(|d| d.refresh_schemas_status.get(&ep_arn)) {
1143            return ok(json!({ "RefreshSchemasStatus": status.clone() }));
1144        }
1145        if data
1146            .map(|d| d.endpoints.contains_key(&ep_arn))
1147            .unwrap_or(false)
1148        {
1149            return ok(json!({
1150                "RefreshSchemasStatus": {
1151                    "EndpointArn": ep_arn,
1152                    "Status": "successful",
1153                    "LastRefreshDate": now_ts()
1154                }
1155            }));
1156        }
1157        Err(not_found(&format!("Endpoint {ep_arn} not found.")))
1158    }
1159
1160    fn describe_schemas(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1161        let ep_arn = req_str_allow_empty(b, "EndpointArn")?.to_string();
1162        let guard = self.state.read();
1163        if !guard
1164            .get(&ctx.account)
1165            .map(|d| d.endpoints.contains_key(&ep_arn))
1166            .unwrap_or(false)
1167        {
1168            return Err(not_found(&format!("Endpoint {ep_arn} not found.")));
1169        }
1170        ok(json!({ "Schemas": [] }))
1171    }
1172
1173    fn describe_endpoint_settings(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1174        let engine = req_str(b, "EngineName")?;
1175        let rows = vec![
1176            json!({ "Name": "ServerName", "Type": "string", "Applicability": engine }),
1177            json!({ "Name": "Port", "Type": "integer", "IntValueMin": 1, "IntValueMax": 65535 }),
1178            json!({ "Name": "DatabaseName", "Type": "string" }),
1179        ];
1180        list_marker("EndpointSettings", rows, b, &[])
1181    }
1182
1183    fn describe_endpoint_types(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1184        let rows: Vec<Value> = [
1185            ("mysql", true),
1186            ("postgres", true),
1187            ("oracle", true),
1188            ("sqlserver", true),
1189            ("s3", false),
1190            ("dynamodb", false),
1191            ("kinesis", false),
1192            ("mariadb", true),
1193            ("aurora", true),
1194            ("aurora-postgresql", true),
1195        ]
1196        .iter()
1197        .flat_map(|(engine, cdc)| {
1198            ["source", "target"].iter().map(move |et| {
1199                json!({
1200                    "EngineName": engine,
1201                    "SupportsCDC": cdc,
1202                    "EndpointType": et,
1203                    "EngineDisplayName": engine,
1204                    "ReplicationInstanceEngineMinimumVersion": "3.4.7"
1205                })
1206            })
1207        })
1208        .collect();
1209        list_marker(
1210            "SupportedEndpointTypes",
1211            rows,
1212            b,
1213            &[
1214                ("engine-name", &["EngineName"]),
1215                ("endpoint-type", &["EndpointType"]),
1216            ],
1217        )
1218    }
1219}
1220
1221// ===================== replication tasks =====================
1222
1223impl DmsService {
1224    fn create_replication_task(
1225        &self,
1226        ctx: &Ctx,
1227        b: &Value,
1228    ) -> Result<AwsResponse, AwsServiceError> {
1229        let id = req_str_allow_empty(b, "ReplicationTaskIdentifier")?.to_string();
1230        let src = req_str_allow_empty(b, "SourceEndpointArn")?.to_string();
1231        let tgt = req_str_allow_empty(b, "TargetEndpointArn")?.to_string();
1232        let inst = req_str_allow_empty(b, "ReplicationInstanceArn")?.to_string();
1233        let mig_type = req_str_allow_empty(b, "MigrationType")?.to_string();
1234        check_migration_type(&mig_type)?;
1235        let table_mappings = req_str_allow_empty(b, "TableMappings")?.to_string();
1236        let mut guard = self.state.write();
1237        let data = guard.get_or_create(&ctx.account);
1238        // Cross-reference validation (AWS rejects unknown endpoints/instances).
1239        if !data.replication_instances.contains_key(&inst) {
1240            return Err(not_found(&format!(
1241                "Replication instance {inst} not found."
1242            )));
1243        }
1244        if !data.endpoints.contains_key(&src) {
1245            return Err(not_found(&format!("Source endpoint {src} not found.")));
1246        }
1247        if !data.endpoints.contains_key(&tgt) {
1248            return Err(not_found(&format!("Target endpoint {tgt} not found.")));
1249        }
1250        if !id.is_empty()
1251            && data
1252                .replication_tasks
1253                .values()
1254                .any(|v| v.get("ReplicationTaskIdentifier").and_then(Value::as_str) == Some(&id))
1255        {
1256            return Err(already_exists(&format!(
1257                "Replication task {id} already exists."
1258            )));
1259        }
1260        let task_arn = arn(ctx, "task", &res_id());
1261        let settings = opt_str(b, "ReplicationTaskSettings")
1262            .filter(|s| !s.is_empty())
1263            .map(String::from)
1264            .unwrap_or_else(default_task_settings);
1265        let mut task = Map::new();
1266        task.insert("ReplicationTaskArn".into(), json!(task_arn));
1267        task.insert("ReplicationTaskIdentifier".into(), json!(id));
1268        task.insert("SourceEndpointArn".into(), json!(src));
1269        task.insert("TargetEndpointArn".into(), json!(tgt));
1270        task.insert("ReplicationInstanceArn".into(), json!(inst));
1271        task.insert("MigrationType".into(), json!(mig_type));
1272        task.insert("TableMappings".into(), json!(table_mappings));
1273        task.insert("ReplicationTaskSettings".into(), json!(settings));
1274        task.insert("Status".into(), json!("ready"));
1275        task.insert("ReplicationTaskCreationDate".into(), json!(now_ts()));
1276        copy_fields(
1277            b,
1278            &mut task,
1279            &["CdcStartPosition", "CdcStopPosition", "TaskData"],
1280        );
1281        task.insert(
1282            "ReplicationTaskStats".into(),
1283            json!({
1284                "FullLoadProgressPercent": 0,
1285                "TablesLoaded": 0,
1286                "TablesLoading": 0,
1287                "TablesQueued": 0,
1288                "TablesErrored": 0
1289            }),
1290        );
1291        let task = Value::Object(task);
1292        data.replication_tasks
1293            .insert(task_arn.clone(), task.clone());
1294        store_tags(data, &task_arn, b);
1295        ok(json!({ "ReplicationTask": task }))
1296    }
1297
1298    fn describe_replication_tasks(
1299        &self,
1300        ctx: &Ctx,
1301        b: &Value,
1302    ) -> Result<AwsResponse, AwsServiceError> {
1303        let guard = self.state.read();
1304        let rows = guard
1305            .get(&ctx.account)
1306            .map(|d| d.replication_tasks.values().cloned().collect())
1307            .unwrap_or_default();
1308        list_marker(
1309            "ReplicationTasks",
1310            rows,
1311            b,
1312            &[
1313                ("replication-task-arn", &["ReplicationTaskArn"]),
1314                ("replication-task-id", &["ReplicationTaskIdentifier"]),
1315                ("migration-type", &["MigrationType"]),
1316                ("endpoint-arn", &["SourceEndpointArn", "TargetEndpointArn"]),
1317                ("replication-instance-arn", &["ReplicationInstanceArn"]),
1318            ],
1319        )
1320    }
1321
1322    fn modify_replication_task(
1323        &self,
1324        ctx: &Ctx,
1325        b: &Value,
1326    ) -> Result<AwsResponse, AwsServiceError> {
1327        let arn_id = req_str(b, "ReplicationTaskArn")?.to_string();
1328        if let Some(mt) = opt_str(b, "MigrationType") {
1329            check_migration_type(mt)?;
1330        }
1331        let mut guard = self.state.write();
1332        let data = guard.get_or_create(&ctx.account);
1333        let Some(task) = data.replication_tasks.get_mut(&arn_id) else {
1334            return Err(not_found(&format!("Replication task {arn_id} not found.")));
1335        };
1336        let obj = task.as_object_mut().unwrap();
1337        for f in [
1338            "ReplicationTaskIdentifier",
1339            "MigrationType",
1340            "TableMappings",
1341            "ReplicationTaskSettings",
1342            "CdcStartPosition",
1343            "CdcStopPosition",
1344            "TaskData",
1345        ] {
1346            if let Some(v) = b.get(f) {
1347                obj.insert(f.to_string(), v.clone());
1348            }
1349        }
1350        let task = task.clone();
1351        ok(json!({ "ReplicationTask": task }))
1352    }
1353
1354    fn delete_replication_task(
1355        &self,
1356        ctx: &Ctx,
1357        b: &Value,
1358    ) -> Result<AwsResponse, AwsServiceError> {
1359        let arn_id = req_str(b, "ReplicationTaskArn")?.to_string();
1360        let mut guard = self.state.write();
1361        let data = guard.get_or_create(&ctx.account);
1362        let Some(mut task) = data.replication_tasks.remove(&arn_id) else {
1363            return Err(not_found(&format!("Replication task {arn_id} not found.")));
1364        };
1365        data.tags.remove(&arn_id);
1366        task["Status"] = json!("deleting");
1367        ok(json!({ "ReplicationTask": task }))
1368    }
1369
1370    fn set_task_status(
1371        &self,
1372        ctx: &Ctx,
1373        b: &Value,
1374        status: &str,
1375    ) -> Result<AwsResponse, AwsServiceError> {
1376        let arn_id = req_str(b, "ReplicationTaskArn")?.to_string();
1377        let mut guard = self.state.write();
1378        let data = guard.get_or_create(&ctx.account);
1379        let Some(task) = data.replication_tasks.get_mut(&arn_id) else {
1380            return Err(not_found(&format!("Replication task {arn_id} not found.")));
1381        };
1382        task["Status"] = json!(status);
1383        if status == "running" {
1384            task["ReplicationTaskStartDate"] = json!(now_ts());
1385        }
1386        let task = task.clone();
1387        ok(json!({ "ReplicationTask": task }))
1388    }
1389
1390    fn start_replication_task(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1391        if let Some(t) = opt_str(b, "StartReplicationTaskType") {
1392            const ALLOWED: &[&str] = &["start-replication", "resume-processing", "reload-target"];
1393            if !t.is_empty() && !ALLOWED.contains(&t) {
1394                return Err(validation(
1395                    "StartReplicationTaskType must be one of [start-replication, resume-processing, reload-target].",
1396                ));
1397            }
1398        }
1399        self.set_task_status(ctx, b, "running")
1400    }
1401
1402    fn stop_replication_task(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1403        self.set_task_status(ctx, b, "stopped")
1404    }
1405
1406    fn start_replication_task_assessment(
1407        &self,
1408        ctx: &Ctx,
1409        b: &Value,
1410    ) -> Result<AwsResponse, AwsServiceError> {
1411        self.set_task_status(ctx, b, "testing")
1412    }
1413
1414    fn move_replication_task(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1415        let arn_id = req_str(b, "ReplicationTaskArn")?.to_string();
1416        let target_inst = req_str(b, "TargetReplicationInstanceArn")?.to_string();
1417        let mut guard = self.state.write();
1418        let data = guard.get_or_create(&ctx.account);
1419        if !data.replication_instances.contains_key(&target_inst) {
1420            return Err(not_found(&format!(
1421                "Target replication instance {target_inst} not found."
1422            )));
1423        }
1424        let Some(task) = data.replication_tasks.get_mut(&arn_id) else {
1425            return Err(not_found(&format!("Replication task {arn_id} not found.")));
1426        };
1427        task["ReplicationInstanceArn"] = json!(target_inst);
1428        task["Status"] = json!("moving");
1429        let task = task.clone();
1430        ok(json!({ "ReplicationTask": task }))
1431    }
1432
1433    fn start_replication_task_assessment_run(
1434        &self,
1435        ctx: &Ctx,
1436        b: &Value,
1437    ) -> Result<AwsResponse, AwsServiceError> {
1438        let task_arn = req_str(b, "ReplicationTaskArn")?.to_string();
1439        req_str(b, "ServiceAccessRoleArn")?;
1440        req_str(b, "ResultLocationBucket")?;
1441        let run_name = req_str(b, "AssessmentRunName")?.to_string();
1442        let mut guard = self.state.write();
1443        let data = guard.get_or_create(&ctx.account);
1444        if !data.replication_tasks.contains_key(&task_arn) {
1445            return Err(not_found(&format!(
1446                "Replication task {task_arn} not found."
1447            )));
1448        }
1449        if data
1450            .assessment_runs
1451            .values()
1452            .any(|r| r.get("AssessmentRunName").and_then(Value::as_str) == Some(&run_name))
1453        {
1454            return Err(already_exists(&format!(
1455                "Assessment run {run_name} already exists."
1456            )));
1457        }
1458        let run_arn = arn(ctx, "assessment-run", &res_id());
1459        let mut run = Map::new();
1460        run.insert("ReplicationTaskAssessmentRunArn".into(), json!(run_arn));
1461        run.insert("ReplicationTaskArn".into(), json!(task_arn));
1462        run.insert("Status".into(), json!("running"));
1463        run.insert(
1464            "ReplicationTaskAssessmentRunCreationDate".into(),
1465            json!(now_ts()),
1466        );
1467        run.insert("AssessmentRunName".into(), json!(run_name));
1468        run.insert("IsLatestTaskAssessmentRun".into(), json!(true));
1469        copy_fields(
1470            b,
1471            &mut run,
1472            &[
1473                "ServiceAccessRoleArn",
1474                "ResultLocationBucket",
1475                "ResultLocationFolder",
1476                "ResultEncryptionMode",
1477                "ResultKmsKeyArn",
1478            ],
1479        );
1480        let run = Value::Object(run);
1481        data.assessment_runs.insert(run_arn.clone(), run.clone());
1482        store_tags(data, &run_arn, b);
1483        ok(json!({ "ReplicationTaskAssessmentRun": run }))
1484    }
1485
1486    fn cancel_replication_task_assessment_run(
1487        &self,
1488        ctx: &Ctx,
1489        b: &Value,
1490    ) -> Result<AwsResponse, AwsServiceError> {
1491        let arn_id = req_str(b, "ReplicationTaskAssessmentRunArn")?.to_string();
1492        let mut guard = self.state.write();
1493        let data = guard.get_or_create(&ctx.account);
1494        let Some(run) = data.assessment_runs.get_mut(&arn_id) else {
1495            return Err(not_found(&format!("Assessment run {arn_id} not found.")));
1496        };
1497        run["Status"] = json!("cancelling");
1498        let run = run.clone();
1499        ok(json!({ "ReplicationTaskAssessmentRun": run }))
1500    }
1501
1502    fn delete_replication_task_assessment_run(
1503        &self,
1504        ctx: &Ctx,
1505        b: &Value,
1506    ) -> Result<AwsResponse, AwsServiceError> {
1507        let arn_id = req_str(b, "ReplicationTaskAssessmentRunArn")?.to_string();
1508        let mut guard = self.state.write();
1509        let data = guard.get_or_create(&ctx.account);
1510        let Some(run) = data.assessment_runs.remove(&arn_id) else {
1511            return Err(not_found(&format!("Assessment run {arn_id} not found.")));
1512        };
1513        ok(json!({ "ReplicationTaskAssessmentRun": run }))
1514    }
1515
1516    fn describe_replication_task_assessment_runs(
1517        &self,
1518        ctx: &Ctx,
1519        b: &Value,
1520    ) -> Result<AwsResponse, AwsServiceError> {
1521        let guard = self.state.read();
1522        let rows = guard
1523            .get(&ctx.account)
1524            .map(|d| d.assessment_runs.values().cloned().collect())
1525            .unwrap_or_default();
1526        list_marker(
1527            "ReplicationTaskAssessmentRuns",
1528            rows,
1529            b,
1530            &[("replication-task-arn", &["ReplicationTaskArn"])],
1531        )
1532    }
1533
1534    fn describe_replication_task_assessment_results(
1535        &self,
1536        ctx: &Ctx,
1537        b: &Value,
1538    ) -> Result<AwsResponse, AwsServiceError> {
1539        let _ = (ctx, b);
1540        ok(json!({ "BucketName": "", "ReplicationTaskAssessmentResults": [] }))
1541    }
1542
1543    fn describe_replication_task_individual_assessments(
1544        &self,
1545        b: &Value,
1546    ) -> Result<AwsResponse, AwsServiceError> {
1547        list_marker("ReplicationTaskIndividualAssessments", vec![], b, &[])
1548    }
1549
1550    fn describe_applicable_individual_assessments(
1551        &self,
1552        b: &Value,
1553    ) -> Result<AwsResponse, AwsServiceError> {
1554        if let Some(mt) = opt_str(b, "MigrationType") {
1555            check_migration_type(mt)?;
1556        }
1557        let rows: Vec<Value> = [
1558            "unsupported-data-types-in-source",
1559            "table-with-lob-but-no-primary-key",
1560        ]
1561        .iter()
1562        .map(|n| json!(n))
1563        .collect();
1564        list_marker("IndividualAssessmentNames", rows, b, &[])
1565    }
1566
1567    fn describe_table_statistics(
1568        &self,
1569        ctx: &Ctx,
1570        b: &Value,
1571    ) -> Result<AwsResponse, AwsServiceError> {
1572        let task_arn = req_str_allow_empty(b, "ReplicationTaskArn")?.to_string();
1573        let guard = self.state.read();
1574        if !guard
1575            .get(&ctx.account)
1576            .map(|d| d.replication_tasks.contains_key(&task_arn))
1577            .unwrap_or(false)
1578        {
1579            return Err(not_found(&format!(
1580                "Replication task {task_arn} not found."
1581            )));
1582        }
1583        let mut out = Map::new();
1584        out.insert("ReplicationTaskArn".into(), json!(task_arn));
1585        out.insert("TableStatistics".into(), json!([]));
1586        ok(Value::Object(out))
1587    }
1588
1589    fn reload_tables(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1590        let task_arn = req_str(b, "ReplicationTaskArn")?.to_string();
1591        if b.get("TablesToReload").and_then(Value::as_array).is_none() {
1592            return Err(validation("TablesToReload is required."));
1593        }
1594        let guard = self.state.read();
1595        if !guard
1596            .get(&ctx.account)
1597            .map(|d| d.replication_tasks.contains_key(&task_arn))
1598            .unwrap_or(false)
1599        {
1600            return Err(not_found(&format!(
1601                "Replication task {task_arn} not found."
1602            )));
1603        }
1604        ok(json!({ "ReplicationTaskArn": task_arn }))
1605    }
1606}
1607
1608fn check_migration_type(t: &str) -> Result<(), AwsServiceError> {
1609    const ALLOWED: &[&str] = &["full-load", "cdc", "full-load-and-cdc"];
1610    if !t.is_empty() && !ALLOWED.contains(&t) {
1611        return Err(validation(
1612            "MigrationType must be one of [full-load, cdc, full-load-and-cdc].",
1613        ));
1614    }
1615    Ok(())
1616}
1617
1618fn default_task_settings() -> String {
1619    json!({
1620        "TargetMetadata": { "TargetSchema": "", "SupportLobs": true, "FullLobMode": false, "LobChunkSize": 64 },
1621        "FullLoadSettings": { "TargetTablePrepMode": "DROP_AND_CREATE", "MaxFullLoadSubTasks": 8, "CommitRate": 10000 },
1622        "Logging": { "EnableLogging": false }
1623    })
1624    .to_string()
1625}
1626
1627// ===================== subnet groups =====================
1628
1629impl DmsService {
1630    fn create_subnet_group(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1631        let id = req_str_allow_empty(b, "ReplicationSubnetGroupIdentifier")?.to_string();
1632        let desc = req_str_allow_empty(b, "ReplicationSubnetGroupDescription")?.to_string();
1633        let subnet_ids: Vec<String> = b
1634            .get("SubnetIds")
1635            .and_then(Value::as_array)
1636            .map(|a| {
1637                a.iter()
1638                    .filter_map(|v| v.as_str().map(String::from))
1639                    .collect()
1640            })
1641            .ok_or_else(|| validation("SubnetIds is required."))?;
1642        let mut guard = self.state.write();
1643        let data = guard.get_or_create(&ctx.account);
1644        if !id.is_empty() && data.subnet_groups.contains_key(&id) {
1645            return Err(already_exists(&format!(
1646                "Subnet group {id} already exists."
1647            )));
1648        }
1649        let subnets: Vec<Value> = subnet_ids
1650            .iter()
1651            .enumerate()
1652            .map(|(i, sid)| {
1653                json!({
1654                    "SubnetIdentifier": sid,
1655                    "SubnetStatus": "Active",
1656                    "SubnetAvailabilityZone": { "Name": format!("{}{}", ctx.region, (b'a' + (i % 3) as u8) as char) }
1657                })
1658            })
1659            .collect();
1660        let group = json!({
1661            "ReplicationSubnetGroupIdentifier": id,
1662            "ReplicationSubnetGroupDescription": desc,
1663            "VpcId": "vpc-0a1b2c3d",
1664            "SubnetGroupStatus": "Complete",
1665            "SupportedNetworkTypes": ["IPV4"],
1666            "Subnets": subnets
1667        });
1668        data.subnet_groups.insert(id.clone(), group.clone());
1669        let group_arn = arn(ctx, "subgrp", &id);
1670        store_tags(data, &group_arn, b);
1671        ok(json!({ "ReplicationSubnetGroup": group }))
1672    }
1673
1674    fn describe_subnet_groups(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1675        let guard = self.state.read();
1676        let rows = guard
1677            .get(&ctx.account)
1678            .map(|d| d.subnet_groups.values().cloned().collect())
1679            .unwrap_or_default();
1680        list_marker(
1681            "ReplicationSubnetGroups",
1682            rows,
1683            b,
1684            &[(
1685                "replication-subnet-group-id",
1686                &["ReplicationSubnetGroupIdentifier"],
1687            )],
1688        )
1689    }
1690
1691    fn modify_subnet_group(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1692        let id = req_str_allow_empty(b, "ReplicationSubnetGroupIdentifier")?.to_string();
1693        if b.get("SubnetIds").and_then(Value::as_array).is_none() {
1694            return Err(validation("SubnetIds is required."));
1695        }
1696        let mut guard = self.state.write();
1697        let data = guard.get_or_create(&ctx.account);
1698        let Some(group) = data.subnet_groups.get_mut(&id) else {
1699            return Err(not_found(&format!("Subnet group {id} not found.")));
1700        };
1701        let obj = group.as_object_mut().unwrap();
1702        if let Some(desc) = opt_str(b, "ReplicationSubnetGroupDescription") {
1703            obj.insert("ReplicationSubnetGroupDescription".into(), json!(desc));
1704        }
1705        if let Some(ids) = b.get("SubnetIds").and_then(Value::as_array) {
1706            let subnets: Vec<Value> = ids
1707                .iter()
1708                .filter_map(|v| v.as_str())
1709                .enumerate()
1710                .map(|(i, sid)| {
1711                    json!({
1712                        "SubnetIdentifier": sid,
1713                        "SubnetStatus": "Active",
1714                        "SubnetAvailabilityZone": { "Name": format!("{}{}", ctx.region, (b'a' + (i % 3) as u8) as char) }
1715                    })
1716                })
1717                .collect();
1718            obj.insert("Subnets".into(), json!(subnets));
1719        }
1720        let group = group.clone();
1721        ok(json!({ "ReplicationSubnetGroup": group }))
1722    }
1723
1724    fn delete_subnet_group(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1725        let id = req_str(b, "ReplicationSubnetGroupIdentifier")?.to_string();
1726        let mut guard = self.state.write();
1727        let data = guard.get_or_create(&ctx.account);
1728        if data.subnet_groups.remove(&id).is_none() {
1729            return Err(not_found(&format!("Subnet group {id} not found.")));
1730        }
1731        data.tags.remove(&arn(ctx, "subgrp", &id));
1732        ok(json!({}))
1733    }
1734}
1735
1736// ===================== event subscriptions =====================
1737
1738impl DmsService {
1739    fn create_event_subscription(
1740        &self,
1741        ctx: &Ctx,
1742        b: &Value,
1743    ) -> Result<AwsResponse, AwsServiceError> {
1744        let name = req_str_allow_empty(b, "SubscriptionName")?.to_string();
1745        let topic = req_str_allow_empty(b, "SnsTopicArn")?.to_string();
1746        let mut guard = self.state.write();
1747        let data = guard.get_or_create(&ctx.account);
1748        if !name.is_empty() && data.event_subscriptions.contains_key(&name) {
1749            return Err(already_exists(&format!(
1750                "Event subscription {name} already exists."
1751            )));
1752        }
1753        let mut sub = Map::new();
1754        sub.insert("CustomerAwsId".into(), json!(ctx.account));
1755        sub.insert("CustSubscriptionId".into(), json!(name));
1756        sub.insert("SnsTopicArn".into(), json!(topic));
1757        sub.insert("Status".into(), json!("active"));
1758        sub.insert(
1759            "SubscriptionCreationTime".into(),
1760            json!(chrono::Utc::now().to_rfc3339()),
1761        );
1762        sub.insert(
1763            "Enabled".into(),
1764            b.get("Enabled").cloned().unwrap_or(json!(true)),
1765        );
1766        if let Some(st) = opt_str(b, "SourceType") {
1767            sub.insert("SourceType".into(), json!(st));
1768        }
1769        if let Some(cats) = b.get("EventCategories") {
1770            sub.insert("EventCategoriesList".into(), cats.clone());
1771        }
1772        if let Some(ids) = b.get("SourceIds") {
1773            sub.insert("SourceIdsList".into(), ids.clone());
1774        }
1775        let sub = Value::Object(sub);
1776        data.event_subscriptions.insert(name.clone(), sub.clone());
1777        store_tags(data, &arn(ctx, "es", &name), b);
1778        ok(json!({ "EventSubscription": sub }))
1779    }
1780
1781    fn describe_event_subscriptions(
1782        &self,
1783        ctx: &Ctx,
1784        b: &Value,
1785    ) -> Result<AwsResponse, AwsServiceError> {
1786        let name = opt_str(b, "SubscriptionName")
1787            .filter(|s| !s.is_empty())
1788            .map(String::from);
1789        let guard = self.state.read();
1790        let subs = guard.get(&ctx.account).map(|d| &d.event_subscriptions);
1791        // A named lookup for a non-existent subscription raises the
1792        // `ResourceNotFoundFault` the op declares, rather than a silent empty
1793        // list. (An empty/omitted name lists everything.)
1794        if let Some(n) = &name {
1795            if !subs.map(|s| s.contains_key(n)).unwrap_or(false) {
1796                return Err(not_found(&format!("Event subscription {n} not found.")));
1797            }
1798        }
1799        let rows: Vec<Value> = subs
1800            .map(|s| {
1801                s.iter()
1802                    .filter(|(k, _)| name.as_ref().map(|n| *k == n).unwrap_or(true))
1803                    .map(|(_, v)| v.clone())
1804                    .collect()
1805            })
1806            .unwrap_or_default();
1807        list_marker(
1808            "EventSubscriptionsList",
1809            rows,
1810            b,
1811            &[
1812                ("event-subscription-arn", &["EventSubscriptionArn"]),
1813                ("event-subscription-id", &["CustSubscriptionId"]),
1814            ],
1815        )
1816    }
1817
1818    fn modify_event_subscription(
1819        &self,
1820        ctx: &Ctx,
1821        b: &Value,
1822    ) -> Result<AwsResponse, AwsServiceError> {
1823        let name = req_str(b, "SubscriptionName")?.to_string();
1824        let mut guard = self.state.write();
1825        let data = guard.get_or_create(&ctx.account);
1826        let Some(sub) = data.event_subscriptions.get_mut(&name) else {
1827            return Err(not_found(&format!("Event subscription {name} not found.")));
1828        };
1829        let obj = sub.as_object_mut().unwrap();
1830        if let Some(v) = b.get("SnsTopicArn") {
1831            obj.insert("SnsTopicArn".into(), v.clone());
1832        }
1833        if let Some(v) = b.get("SourceType") {
1834            obj.insert("SourceType".into(), v.clone());
1835        }
1836        if let Some(v) = b.get("EventCategories") {
1837            obj.insert("EventCategoriesList".into(), v.clone());
1838        }
1839        if let Some(v) = b.get("Enabled") {
1840            obj.insert("Enabled".into(), v.clone());
1841        }
1842        let sub = sub.clone();
1843        ok(json!({ "EventSubscription": sub }))
1844    }
1845
1846    fn delete_event_subscription(
1847        &self,
1848        ctx: &Ctx,
1849        b: &Value,
1850    ) -> Result<AwsResponse, AwsServiceError> {
1851        let name = req_str(b, "SubscriptionName")?.to_string();
1852        let mut guard = self.state.write();
1853        let data = guard.get_or_create(&ctx.account);
1854        let Some(sub) = data.event_subscriptions.remove(&name) else {
1855            return Err(not_found(&format!("Event subscription {name} not found.")));
1856        };
1857        data.tags.remove(&arn(ctx, "es", &name));
1858        ok(json!({ "EventSubscription": sub }))
1859    }
1860
1861    fn describe_event_categories(&self, _b: &Value) -> Result<AwsResponse, AwsServiceError> {
1862        ok(json!({
1863            "EventCategoryGroupList": [
1864                {
1865                    "SourceType": "replication-instance",
1866                    "EventCategories": ["configuration change", "creation", "deletion", "failover", "failure", "maintenance"]
1867                },
1868                {
1869                    "SourceType": "replication-task",
1870                    "EventCategories": ["configuration change", "creation", "deletion", "failure", "state change"]
1871                }
1872            ]
1873        }))
1874    }
1875
1876    fn describe_events(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1877        if let Some(st) = opt_str(b, "SourceType") {
1878            if !st.is_empty() && st != "replication-instance" {
1879                return Err(validation("SourceType must be replication-instance."));
1880            }
1881        }
1882        list_marker("Events", vec![], b, &[])
1883    }
1884
1885    fn update_subscriptions_to_eventbridge(
1886        &self,
1887        _b: &Value,
1888    ) -> Result<AwsResponse, AwsServiceError> {
1889        ok(json!({ "Result": "Successfully migrated subscriptions to EventBridge." }))
1890    }
1891}
1892
1893// ===================== certificates =====================
1894
1895impl DmsService {
1896    fn import_certificate(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1897        let id = req_str_allow_empty(b, "CertificateIdentifier")?.to_string();
1898        let mut guard = self.state.write();
1899        let data = guard.get_or_create(&ctx.account);
1900        if !id.is_empty()
1901            && data
1902                .certificates
1903                .values()
1904                .any(|v| v.get("CertificateIdentifier").and_then(Value::as_str) == Some(&id))
1905        {
1906            return Err(already_exists(&format!("Certificate {id} already exists.")));
1907        }
1908        let cert_arn = arn(ctx, "cert", &res_id());
1909        let mut cert = Map::new();
1910        cert.insert("CertificateArn".into(), json!(cert_arn));
1911        cert.insert("CertificateIdentifier".into(), json!(id));
1912        cert.insert("CertificateCreationDate".into(), json!(now_ts()));
1913        cert.insert("CertificateOwner".into(), json!(ctx.account));
1914        cert.insert("SigningAlgorithm".into(), json!("SHA256withRSA"));
1915        cert.insert("KeyLength".into(), json!(2048));
1916        cert.insert("ValidFromDate".into(), json!(now_ts()));
1917        cert.insert(
1918            "ValidToDate".into(),
1919            json!(now_ts() + 365.0 * 24.0 * 3600.0),
1920        );
1921        if let Some(pem) = opt_str(b, "CertificatePem") {
1922            cert.insert("CertificatePem".into(), json!(pem));
1923        }
1924        if let Some(kms) = opt_str(b, "KmsKeyId") {
1925            cert.insert("KmsKeyId".into(), json!(kms));
1926        }
1927        let cert = Value::Object(cert);
1928        data.certificates.insert(cert_arn.clone(), cert.clone());
1929        store_tags(data, &cert_arn, b);
1930        ok(json!({ "Certificate": cert }))
1931    }
1932
1933    fn describe_certificates(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1934        let guard = self.state.read();
1935        let rows = guard
1936            .get(&ctx.account)
1937            .map(|d| d.certificates.values().cloned().collect())
1938            .unwrap_or_default();
1939        list_marker(
1940            "Certificates",
1941            rows,
1942            b,
1943            &[("certificate-id", &["CertificateIdentifier"])],
1944        )
1945    }
1946
1947    fn delete_certificate(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
1948        let arn_id = req_str(b, "CertificateArn")?.to_string();
1949        let mut guard = self.state.write();
1950        let data = guard.get_or_create(&ctx.account);
1951        let Some(cert) = data.certificates.remove(&arn_id) else {
1952            return Err(not_found(&format!("Certificate {arn_id} not found.")));
1953        };
1954        data.tags.remove(&arn_id);
1955        ok(json!({ "Certificate": cert }))
1956    }
1957}
1958
1959// ===================== serverless replication configs =====================
1960
1961impl DmsService {
1962    fn create_replication_config(
1963        &self,
1964        ctx: &Ctx,
1965        b: &Value,
1966    ) -> Result<AwsResponse, AwsServiceError> {
1967        let id = req_str_allow_empty(b, "ReplicationConfigIdentifier")?.to_string();
1968        let src = req_str_allow_empty(b, "SourceEndpointArn")?.to_string();
1969        let tgt = req_str_allow_empty(b, "TargetEndpointArn")?.to_string();
1970        let rep_type = req_str_allow_empty(b, "ReplicationType")?.to_string();
1971        check_replication_type(&rep_type)?;
1972        let table_mappings = req_str_allow_empty(b, "TableMappings")?.to_string();
1973        let compute = b
1974            .get("ComputeConfig")
1975            .cloned()
1976            .ok_or_else(|| validation("ComputeConfig is required."))?;
1977        let mut guard = self.state.write();
1978        let data = guard.get_or_create(&ctx.account);
1979        if !id.is_empty()
1980            && data
1981                .replication_configs
1982                .values()
1983                .any(|v| v.get("ReplicationConfigIdentifier").and_then(Value::as_str) == Some(&id))
1984        {
1985            return Err(already_exists(&format!(
1986                "Replication config {id} already exists."
1987            )));
1988        }
1989        let config_arn = arn(ctx, "replication-config", &res_id());
1990        let mut cfg = Map::new();
1991        cfg.insert("ReplicationConfigArn".into(), json!(config_arn));
1992        cfg.insert("ReplicationConfigIdentifier".into(), json!(id));
1993        cfg.insert("SourceEndpointArn".into(), json!(src));
1994        cfg.insert("TargetEndpointArn".into(), json!(tgt));
1995        cfg.insert("ReplicationType".into(), json!(rep_type));
1996        cfg.insert("ComputeConfig".into(), compute);
1997        cfg.insert("TableMappings".into(), json!(table_mappings));
1998        cfg.insert("ReplicationConfigCreateTime".into(), json!(now_ts()));
1999        cfg.insert("ReplicationConfigUpdateTime".into(), json!(now_ts()));
2000        copy_fields(
2001            b,
2002            &mut cfg,
2003            &["ReplicationSettings", "SupplementalSettings"],
2004        );
2005        let cfg = Value::Object(cfg);
2006        data.replication_configs
2007            .insert(config_arn.clone(), cfg.clone());
2008        store_tags(data, &config_arn, b);
2009        ok(json!({ "ReplicationConfig": cfg }))
2010    }
2011
2012    fn describe_replication_configs(
2013        &self,
2014        ctx: &Ctx,
2015        b: &Value,
2016    ) -> Result<AwsResponse, AwsServiceError> {
2017        let guard = self.state.read();
2018        let rows = guard
2019            .get(&ctx.account)
2020            .map(|d| d.replication_configs.values().cloned().collect())
2021            .unwrap_or_default();
2022        list_marker(
2023            "ReplicationConfigs",
2024            rows,
2025            b,
2026            &[
2027                ("replication-config-arn", &["ReplicationConfigArn"]),
2028                ("replication-config-id", &["ReplicationConfigIdentifier"]),
2029            ],
2030        )
2031    }
2032
2033    fn modify_replication_config(
2034        &self,
2035        ctx: &Ctx,
2036        b: &Value,
2037    ) -> Result<AwsResponse, AwsServiceError> {
2038        let arn_id = req_str(b, "ReplicationConfigArn")?.to_string();
2039        if let Some(rt) = opt_str(b, "ReplicationType") {
2040            check_replication_type(rt)?;
2041        }
2042        let mut guard = self.state.write();
2043        let data = guard.get_or_create(&ctx.account);
2044        let Some(cfg) = data.replication_configs.get_mut(&arn_id) else {
2045            return Err(not_found(&format!(
2046                "Replication config {arn_id} not found."
2047            )));
2048        };
2049        let obj = cfg.as_object_mut().unwrap();
2050        for f in [
2051            "ReplicationConfigIdentifier",
2052            "ReplicationType",
2053            "TableMappings",
2054            "ReplicationSettings",
2055            "SupplementalSettings",
2056            "ComputeConfig",
2057            "SourceEndpointArn",
2058            "TargetEndpointArn",
2059        ] {
2060            if let Some(v) = b.get(f) {
2061                obj.insert(f.to_string(), v.clone());
2062            }
2063        }
2064        obj.insert("ReplicationConfigUpdateTime".into(), json!(now_ts()));
2065        let cfg = cfg.clone();
2066        ok(json!({ "ReplicationConfig": cfg }))
2067    }
2068
2069    fn delete_replication_config(
2070        &self,
2071        ctx: &Ctx,
2072        b: &Value,
2073    ) -> Result<AwsResponse, AwsServiceError> {
2074        let arn_id = req_str(b, "ReplicationConfigArn")?.to_string();
2075        let mut guard = self.state.write();
2076        let data = guard.get_or_create(&ctx.account);
2077        let Some(cfg) = data.replication_configs.remove(&arn_id) else {
2078            return Err(not_found(&format!(
2079                "Replication config {arn_id} not found."
2080            )));
2081        };
2082        data.replications.remove(&arn_id);
2083        data.tags.remove(&arn_id);
2084        ok(json!({ "ReplicationConfig": cfg }))
2085    }
2086
2087    fn build_replication(&self, cfg: &Value, status: &str) -> Value {
2088        json!({
2089            "ReplicationConfigIdentifier": cfg.get("ReplicationConfigIdentifier").cloned().unwrap_or(json!("")),
2090            "ReplicationConfigArn": cfg.get("ReplicationConfigArn").cloned().unwrap_or(json!("")),
2091            "SourceEndpointArn": cfg.get("SourceEndpointArn").cloned().unwrap_or(json!("")),
2092            "TargetEndpointArn": cfg.get("TargetEndpointArn").cloned().unwrap_or(json!("")),
2093            "ReplicationType": cfg.get("ReplicationType").cloned().unwrap_or(json!("")),
2094            "Status": status,
2095            "ReplicationCreateTime": now_ts(),
2096            "ReplicationUpdateTime": now_ts(),
2097            "ProvisionData": { "ProvisionState": "provisioned", "ProvisionedCapacityUnits": 2 },
2098            "ReplicationStats": { "FullLoadProgressPercent": 0, "TablesLoaded": 0, "TablesLoading": 0, "TablesQueued": 0, "TablesErrored": 0 }
2099        })
2100    }
2101
2102    fn start_replication(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2103        let arn_id = req_str(b, "ReplicationConfigArn")?.to_string();
2104        req_str(b, "StartReplicationType")?;
2105        let mut guard = self.state.write();
2106        let data = guard.get_or_create(&ctx.account);
2107        let Some(cfg) = data.replication_configs.get(&arn_id).cloned() else {
2108            return Err(not_found(&format!(
2109                "Replication config {arn_id} not found."
2110            )));
2111        };
2112        if data
2113            .replications
2114            .get(&arn_id)
2115            .and_then(|r| r.get("Status"))
2116            .and_then(Value::as_str)
2117            == Some("running")
2118        {
2119            return Err(invalid_state(&format!(
2120                "Replication for {arn_id} is already running."
2121            )));
2122        }
2123        let mut rep = self.build_replication(&cfg, "running");
2124        rep["StartReplicationType"] = b
2125            .get("StartReplicationType")
2126            .cloned()
2127            .unwrap_or(json!("start-replication"));
2128        for f in ["CdcStartPosition", "CdcStopPosition"] {
2129            if let Some(v) = b.get(f) {
2130                rep[f] = v.clone();
2131            }
2132        }
2133        data.replications.insert(arn_id, rep.clone());
2134        ok(json!({ "Replication": rep }))
2135    }
2136
2137    fn stop_replication(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2138        let arn_id = req_str(b, "ReplicationConfigArn")?.to_string();
2139        let mut guard = self.state.write();
2140        let data = guard.get_or_create(&ctx.account);
2141        let Some(rep) = data.replications.get_mut(&arn_id) else {
2142            return Err(not_found(&format!("Replication for {arn_id} not found.")));
2143        };
2144        rep["Status"] = json!("stopped");
2145        rep["ReplicationLastStopTime"] = json!(now_ts());
2146        let rep = rep.clone();
2147        ok(json!({ "Replication": rep }))
2148    }
2149
2150    fn describe_replications(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2151        let guard = self.state.read();
2152        let rows = guard
2153            .get(&ctx.account)
2154            .map(|d| d.replications.values().cloned().collect())
2155            .unwrap_or_default();
2156        list_marker(
2157            "Replications",
2158            rows,
2159            b,
2160            &[("replication-config-arn", &["ReplicationConfigArn"])],
2161        )
2162    }
2163
2164    fn describe_replication_table_statistics(
2165        &self,
2166        ctx: &Ctx,
2167        b: &Value,
2168    ) -> Result<AwsResponse, AwsServiceError> {
2169        let arn_id = req_str(b, "ReplicationConfigArn")?.to_string();
2170        let guard = self.state.read();
2171        if !guard
2172            .get(&ctx.account)
2173            .map(|d| d.replication_configs.contains_key(&arn_id))
2174            .unwrap_or(false)
2175        {
2176            return Err(not_found(&format!(
2177                "Replication config {arn_id} not found."
2178            )));
2179        }
2180        ok(json!({
2181            "ReplicationConfigArn": arn_id,
2182            "ReplicationTableStatistics": []
2183        }))
2184    }
2185
2186    fn reload_replication_tables(
2187        &self,
2188        ctx: &Ctx,
2189        b: &Value,
2190    ) -> Result<AwsResponse, AwsServiceError> {
2191        let arn_id = req_str(b, "ReplicationConfigArn")?.to_string();
2192        if b.get("TablesToReload").and_then(Value::as_array).is_none() {
2193            return Err(validation("TablesToReload is required."));
2194        }
2195        let guard = self.state.read();
2196        if !guard
2197            .get(&ctx.account)
2198            .map(|d| d.replication_configs.contains_key(&arn_id))
2199            .unwrap_or(false)
2200        {
2201            return Err(not_found(&format!(
2202                "Replication config {arn_id} not found."
2203            )));
2204        }
2205        ok(json!({ "ReplicationConfigArn": arn_id }))
2206    }
2207}
2208
2209fn check_replication_type(t: &str) -> Result<(), AwsServiceError> {
2210    const ALLOWED: &[&str] = &["full-load", "cdc", "full-load-and-cdc"];
2211    if !t.is_empty() && !ALLOWED.contains(&t) {
2212        return Err(validation(
2213            "ReplicationType must be one of [full-load, cdc, full-load-and-cdc].",
2214        ));
2215    }
2216    Ok(())
2217}
2218
2219// ===================== data providers =====================
2220
2221impl DmsService {
2222    fn create_data_provider(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2223        let engine = req_str_allow_empty(b, "Engine")?.to_string();
2224        let settings = b
2225            .get("Settings")
2226            .cloned()
2227            .ok_or_else(|| validation("Settings is required."))?;
2228        let name = opt_str(b, "DataProviderName")
2229            .filter(|s| !s.is_empty())
2230            .map(String::from)
2231            .unwrap_or_else(|| format!("data-provider-{}", &res_id()[..8].to_lowercase()));
2232        let mut guard = self.state.write();
2233        let data = guard.get_or_create(&ctx.account);
2234        if data
2235            .data_providers
2236            .values()
2237            .any(|v| v.get("DataProviderName").and_then(Value::as_str) == Some(&name))
2238        {
2239            return Err(already_exists(&format!(
2240                "Data provider {name} already exists."
2241            )));
2242        }
2243        let provider_arn = arn(ctx, "data-provider", &name);
2244        let mut dp = Map::new();
2245        dp.insert("DataProviderArn".into(), json!(provider_arn));
2246        dp.insert("DataProviderName".into(), json!(name));
2247        dp.insert("Engine".into(), json!(engine));
2248        dp.insert("Settings".into(), settings);
2249        dp.insert("DataProviderCreationTime".into(), json!(now_ts()));
2250        if let Some(d) = opt_str(b, "Description") {
2251            dp.insert("Description".into(), json!(d));
2252        }
2253        if let Some(v) = b.get("Virtual") {
2254            dp.insert("Virtual".into(), v.clone());
2255        }
2256        let dp = Value::Object(dp);
2257        data.data_providers.insert(provider_arn.clone(), dp.clone());
2258        store_tags(data, &provider_arn, b);
2259        ok(json!({ "DataProvider": dp }))
2260    }
2261
2262    fn describe_data_providers(
2263        &self,
2264        ctx: &Ctx,
2265        b: &Value,
2266    ) -> Result<AwsResponse, AwsServiceError> {
2267        let guard = self.state.read();
2268        let rows = guard
2269            .get(&ctx.account)
2270            .map(|d| d.data_providers.values().cloned().collect())
2271            .unwrap_or_default();
2272        list_marker(
2273            "DataProviders",
2274            rows,
2275            b,
2276            &[(
2277                "data-provider-identifier",
2278                &["DataProviderArn", "DataProviderName"],
2279            )],
2280        )
2281    }
2282
2283    fn resolve_data_provider<'a>(data: &'a mut DmsData, ident: &str) -> Option<&'a mut Value> {
2284        if data.data_providers.contains_key(ident) {
2285            return data.data_providers.get_mut(ident);
2286        }
2287        let key = data
2288            .data_providers
2289            .iter()
2290            .find(|(_, v)| v.get("DataProviderName").and_then(Value::as_str) == Some(ident))
2291            .map(|(k, _)| k.clone())?;
2292        data.data_providers.get_mut(&key)
2293    }
2294
2295    fn modify_data_provider(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2296        let ident = req_str(b, "DataProviderIdentifier")?.to_string();
2297        let mut guard = self.state.write();
2298        let data = guard.get_or_create(&ctx.account);
2299        let Some(dp) = Self::resolve_data_provider(data, &ident) else {
2300            return Err(not_found(&format!("Data provider {ident} not found.")));
2301        };
2302        let obj = dp.as_object_mut().unwrap();
2303        for (in_f, out_f) in [
2304            ("DataProviderName", "DataProviderName"),
2305            ("Description", "Description"),
2306            ("Engine", "Engine"),
2307            ("Virtual", "Virtual"),
2308            ("Settings", "Settings"),
2309        ] {
2310            if let Some(v) = b.get(in_f) {
2311                obj.insert(out_f.to_string(), v.clone());
2312            }
2313        }
2314        let dp = dp.clone();
2315        ok(json!({ "DataProvider": dp }))
2316    }
2317
2318    fn delete_data_provider(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2319        let ident = req_str(b, "DataProviderIdentifier")?.to_string();
2320        let mut guard = self.state.write();
2321        let data = guard.get_or_create(&ctx.account);
2322        let key = if data.data_providers.contains_key(&ident) {
2323            Some(ident.clone())
2324        } else {
2325            data.data_providers
2326                .iter()
2327                .find(|(_, v)| {
2328                    v.get("DataProviderName").and_then(Value::as_str) == Some(ident.as_str())
2329                })
2330                .map(|(k, _)| k.clone())
2331        };
2332        let Some(key) = key else {
2333            return Err(not_found(&format!("Data provider {ident} not found.")));
2334        };
2335        let dp = data.data_providers.remove(&key).unwrap();
2336        data.tags.remove(&key);
2337        ok(json!({ "DataProvider": dp }))
2338    }
2339}
2340
2341// ===================== instance profiles =====================
2342
2343impl DmsService {
2344    fn build_instance_profile(&self, ctx: &Ctx, b: &Value, profile_arn: &str, name: &str) -> Value {
2345        let mut ip = Map::new();
2346        ip.insert("InstanceProfileArn".into(), json!(profile_arn));
2347        ip.insert("InstanceProfileName".into(), json!(name));
2348        ip.insert("InstanceProfileCreationTime".into(), json!(now_ts()));
2349        ip.insert(
2350            "VpcSecurityGroups".into(),
2351            b.get("VpcSecurityGroups").cloned().unwrap_or(json!([])),
2352        );
2353        copy_fields(
2354            b,
2355            &mut ip,
2356            &[
2357                "AvailabilityZone",
2358                "KmsKeyArn",
2359                "PubliclyAccessible",
2360                "NetworkType",
2361                "Description",
2362                "SubnetGroupIdentifier",
2363            ],
2364        );
2365        let _ = ctx;
2366        Value::Object(ip)
2367    }
2368
2369    fn create_instance_profile(
2370        &self,
2371        ctx: &Ctx,
2372        b: &Value,
2373    ) -> Result<AwsResponse, AwsServiceError> {
2374        let name = opt_str(b, "InstanceProfileName")
2375            .filter(|s| !s.is_empty())
2376            .map(String::from)
2377            .unwrap_or_else(|| format!("instance-profile-{}", &res_id()[..8].to_lowercase()));
2378        let mut guard = self.state.write();
2379        let data = guard.get_or_create(&ctx.account);
2380        if data
2381            .instance_profiles
2382            .values()
2383            .any(|v| v.get("InstanceProfileName").and_then(Value::as_str) == Some(&name))
2384        {
2385            return Err(already_exists(&format!(
2386                "Instance profile {name} already exists."
2387            )));
2388        }
2389        let profile_arn = arn(ctx, "instance-profile", &name);
2390        let ip = self.build_instance_profile(ctx, b, &profile_arn, &name);
2391        data.instance_profiles
2392            .insert(profile_arn.clone(), ip.clone());
2393        store_tags(data, &profile_arn, b);
2394        ok(json!({ "InstanceProfile": ip }))
2395    }
2396
2397    fn describe_instance_profiles(
2398        &self,
2399        ctx: &Ctx,
2400        b: &Value,
2401    ) -> Result<AwsResponse, AwsServiceError> {
2402        let guard = self.state.read();
2403        let rows = guard
2404            .get(&ctx.account)
2405            .map(|d| d.instance_profiles.values().cloned().collect())
2406            .unwrap_or_default();
2407        list_marker(
2408            "InstanceProfiles",
2409            rows,
2410            b,
2411            &[(
2412                "instance-profile-identifier",
2413                &["InstanceProfileArn", "InstanceProfileName"],
2414            )],
2415        )
2416    }
2417
2418    fn resolve_instance_profile_key(data: &DmsData, ident: &str) -> Option<String> {
2419        if data.instance_profiles.contains_key(ident) {
2420            return Some(ident.to_string());
2421        }
2422        data.instance_profiles
2423            .iter()
2424            .find(|(_, v)| v.get("InstanceProfileName").and_then(Value::as_str) == Some(ident))
2425            .map(|(k, _)| k.clone())
2426    }
2427
2428    fn modify_instance_profile(
2429        &self,
2430        ctx: &Ctx,
2431        b: &Value,
2432    ) -> Result<AwsResponse, AwsServiceError> {
2433        let ident = req_str_allow_empty(b, "InstanceProfileIdentifier")?.to_string();
2434        let mut guard = self.state.write();
2435        let data = guard.get_or_create(&ctx.account);
2436        let Some(key) = Self::resolve_instance_profile_key(data, &ident) else {
2437            return Err(not_found(&format!("Instance profile {ident} not found.")));
2438        };
2439        let ip = data.instance_profiles.get_mut(&key).unwrap();
2440        let obj = ip.as_object_mut().unwrap();
2441        for f in [
2442            "AvailabilityZone",
2443            "KmsKeyArn",
2444            "PubliclyAccessible",
2445            "NetworkType",
2446            "InstanceProfileName",
2447            "Description",
2448            "SubnetGroupIdentifier",
2449            "VpcSecurityGroups",
2450        ] {
2451            if let Some(v) = b.get(f) {
2452                obj.insert(f.to_string(), v.clone());
2453            }
2454        }
2455        let ip = ip.clone();
2456        ok(json!({ "InstanceProfile": ip }))
2457    }
2458
2459    fn delete_instance_profile(
2460        &self,
2461        ctx: &Ctx,
2462        b: &Value,
2463    ) -> Result<AwsResponse, AwsServiceError> {
2464        let ident = req_str(b, "InstanceProfileIdentifier")?.to_string();
2465        let mut guard = self.state.write();
2466        let data = guard.get_or_create(&ctx.account);
2467        let Some(key) = Self::resolve_instance_profile_key(data, &ident) else {
2468            return Err(not_found(&format!("Instance profile {ident} not found.")));
2469        };
2470        let ip = data.instance_profiles.remove(&key).unwrap();
2471        data.tags.remove(&key);
2472        ok(json!({ "InstanceProfile": ip }))
2473    }
2474}
2475
2476// ===================== migration projects =====================
2477
2478fn build_provider_descriptors(input: Option<&Value>) -> Vec<Value> {
2479    input
2480        .and_then(Value::as_array)
2481        .map(|arr| {
2482            arr.iter()
2483                .map(|d| {
2484                    let ident = d
2485                        .get("DataProviderIdentifier")
2486                        .and_then(Value::as_str)
2487                        .unwrap_or("");
2488                    let name = ident.rsplit(':').next().unwrap_or(ident);
2489                    let mut m = Map::new();
2490                    m.insert("DataProviderArn".into(), json!(ident));
2491                    m.insert("DataProviderName".into(), json!(name));
2492                    if let Some(v) = d.get("SecretsManagerSecretId") {
2493                        m.insert("SecretsManagerSecretId".into(), v.clone());
2494                    }
2495                    if let Some(v) = d.get("SecretsManagerAccessRoleArn") {
2496                        m.insert("SecretsManagerAccessRoleArn".into(), v.clone());
2497                    }
2498                    Value::Object(m)
2499                })
2500                .collect()
2501        })
2502        .unwrap_or_default()
2503}
2504
2505impl DmsService {
2506    fn create_migration_project(
2507        &self,
2508        ctx: &Ctx,
2509        b: &Value,
2510    ) -> Result<AwsResponse, AwsServiceError> {
2511        if b.get("SourceDataProviderDescriptors")
2512            .and_then(Value::as_array)
2513            .is_none()
2514        {
2515            return Err(validation("SourceDataProviderDescriptors is required."));
2516        }
2517        if b.get("TargetDataProviderDescriptors")
2518            .and_then(Value::as_array)
2519            .is_none()
2520        {
2521            return Err(validation("TargetDataProviderDescriptors is required."));
2522        }
2523        let profile_ident = req_str_allow_empty(b, "InstanceProfileIdentifier")?.to_string();
2524        let name = opt_str(b, "MigrationProjectName")
2525            .filter(|s| !s.is_empty())
2526            .map(String::from)
2527            .unwrap_or_else(|| format!("migration-project-{}", &res_id()[..8].to_lowercase()));
2528        let mut guard = self.state.write();
2529        let data = guard.get_or_create(&ctx.account);
2530        if data
2531            .migration_projects
2532            .values()
2533            .any(|v| v.get("MigrationProjectName").and_then(Value::as_str) == Some(&name))
2534        {
2535            return Err(already_exists(&format!(
2536                "Migration project {name} already exists."
2537            )));
2538        }
2539        let profile_name = profile_ident
2540            .rsplit(':')
2541            .next()
2542            .unwrap_or(&profile_ident)
2543            .to_string();
2544        let profile_arn = if profile_ident.starts_with("arn:") {
2545            profile_ident.clone()
2546        } else {
2547            arn(ctx, "instance-profile", &profile_ident)
2548        };
2549        let project_arn = arn(ctx, "migration-project", &name);
2550        let mut mp = Map::new();
2551        mp.insert("MigrationProjectArn".into(), json!(project_arn));
2552        mp.insert("MigrationProjectName".into(), json!(name));
2553        mp.insert("MigrationProjectCreationTime".into(), json!(now_ts()));
2554        mp.insert("InstanceProfileArn".into(), json!(profile_arn));
2555        mp.insert("InstanceProfileName".into(), json!(profile_name));
2556        mp.insert(
2557            "SourceDataProviderDescriptors".into(),
2558            json!(build_provider_descriptors(
2559                b.get("SourceDataProviderDescriptors")
2560            )),
2561        );
2562        mp.insert(
2563            "TargetDataProviderDescriptors".into(),
2564            json!(build_provider_descriptors(
2565                b.get("TargetDataProviderDescriptors")
2566            )),
2567        );
2568        copy_fields(
2569            b,
2570            &mut mp,
2571            &[
2572                "TransformationRules",
2573                "Description",
2574                "SchemaConversionApplicationAttributes",
2575            ],
2576        );
2577        let mp = Value::Object(mp);
2578        data.migration_projects
2579            .insert(project_arn.clone(), mp.clone());
2580        store_tags(data, &project_arn, b);
2581        ok(json!({ "MigrationProject": mp }))
2582    }
2583
2584    fn describe_migration_projects(
2585        &self,
2586        ctx: &Ctx,
2587        b: &Value,
2588    ) -> Result<AwsResponse, AwsServiceError> {
2589        let guard = self.state.read();
2590        let rows = guard
2591            .get(&ctx.account)
2592            .map(|d| d.migration_projects.values().cloned().collect())
2593            .unwrap_or_default();
2594        list_marker(
2595            "MigrationProjects",
2596            rows,
2597            b,
2598            &[(
2599                "migration-project-identifier",
2600                &["MigrationProjectArn", "MigrationProjectName"],
2601            )],
2602        )
2603    }
2604
2605    fn resolve_migration_project_key(data: &DmsData, ident: &str) -> Option<String> {
2606        if data.migration_projects.contains_key(ident) {
2607            return Some(ident.to_string());
2608        }
2609        data.migration_projects
2610            .iter()
2611            .find(|(_, v)| v.get("MigrationProjectName").and_then(Value::as_str) == Some(ident))
2612            .map(|(k, _)| k.clone())
2613    }
2614
2615    fn modify_migration_project(
2616        &self,
2617        ctx: &Ctx,
2618        b: &Value,
2619    ) -> Result<AwsResponse, AwsServiceError> {
2620        let ident = req_str(b, "MigrationProjectIdentifier")?.to_string();
2621        let mut guard = self.state.write();
2622        let data = guard.get_or_create(&ctx.account);
2623        let Some(key) = Self::resolve_migration_project_key(data, &ident) else {
2624            return Err(not_found(&format!("Migration project {ident} not found.")));
2625        };
2626        let mp = data.migration_projects.get_mut(&key).unwrap();
2627        let obj = mp.as_object_mut().unwrap();
2628        for f in [
2629            "MigrationProjectName",
2630            "TransformationRules",
2631            "Description",
2632            "SchemaConversionApplicationAttributes",
2633        ] {
2634            if let Some(v) = b.get(f) {
2635                obj.insert(f.to_string(), v.clone());
2636            }
2637        }
2638        if let Some(v) = b.get("SourceDataProviderDescriptors") {
2639            obj.insert(
2640                "SourceDataProviderDescriptors".into(),
2641                json!(build_provider_descriptors(Some(v))),
2642            );
2643        }
2644        if let Some(v) = b.get("TargetDataProviderDescriptors") {
2645            obj.insert(
2646                "TargetDataProviderDescriptors".into(),
2647                json!(build_provider_descriptors(Some(v))),
2648            );
2649        }
2650        if let Some(pi) = opt_str(b, "InstanceProfileIdentifier") {
2651            let pname = pi.rsplit(':').next().unwrap_or(pi).to_string();
2652            let parn = if pi.starts_with("arn:") {
2653                pi.to_string()
2654            } else {
2655                arn(ctx, "instance-profile", pi)
2656            };
2657            obj.insert("InstanceProfileArn".into(), json!(parn));
2658            obj.insert("InstanceProfileName".into(), json!(pname));
2659        }
2660        let mp = mp.clone();
2661        ok(json!({ "MigrationProject": mp }))
2662    }
2663
2664    fn delete_migration_project(
2665        &self,
2666        ctx: &Ctx,
2667        b: &Value,
2668    ) -> Result<AwsResponse, AwsServiceError> {
2669        let ident = req_str(b, "MigrationProjectIdentifier")?.to_string();
2670        let mut guard = self.state.write();
2671        let data = guard.get_or_create(&ctx.account);
2672        let Some(key) = Self::resolve_migration_project_key(data, &ident) else {
2673            return Err(not_found(&format!("Migration project {ident} not found.")));
2674        };
2675        let mp = data.migration_projects.remove(&key).unwrap();
2676        data.conversion_configs.remove(&key);
2677        data.tags.remove(&key);
2678        ok(json!({ "MigrationProject": mp }))
2679    }
2680}
2681
2682// ============ conversion config + metadata model (schema conversion) ============
2683
2684impl DmsService {
2685    fn require_project(data: &DmsData, b: &Value) -> Result<String, AwsServiceError> {
2686        // `MigrationProjectIdentifier` is `@length(min: 0)`, so an empty string
2687        // is a valid input; it simply resolves to no project. Reaching for a
2688        // non-existent (or empty) project yields the declared
2689        // `ResourceNotFoundFault` rather than a validation error.
2690        let ident = req_str_allow_empty(b, "MigrationProjectIdentifier")?;
2691        Self::resolve_migration_project_key(data, ident)
2692            .ok_or_else(|| not_found(&format!("Migration project {ident} not found.")))
2693    }
2694
2695    fn describe_conversion_configuration(
2696        &self,
2697        ctx: &Ctx,
2698        b: &Value,
2699    ) -> Result<AwsResponse, AwsServiceError> {
2700        let guard = self.state.read();
2701        let data = guard
2702            .get(&ctx.account)
2703            .ok_or_else(|| not_found("Migration project not found."))?;
2704        let key = Self::require_project(data, b)?;
2705        let cfg = data
2706            .conversion_configs
2707            .get(&key)
2708            .cloned()
2709            .unwrap_or_else(|| "{}".to_string());
2710        ok(json!({ "MigrationProjectIdentifier": key, "ConversionConfiguration": cfg }))
2711    }
2712
2713    fn modify_conversion_configuration(
2714        &self,
2715        ctx: &Ctx,
2716        b: &Value,
2717    ) -> Result<AwsResponse, AwsServiceError> {
2718        let cfg = req_str(b, "ConversionConfiguration")?.to_string();
2719        let mut guard = self.state.write();
2720        let data = guard.get_or_create(&ctx.account);
2721        let key = Self::require_project(data, b)?;
2722        data.conversion_configs.insert(key.clone(), cfg);
2723        ok(json!({ "MigrationProjectIdentifier": key }))
2724    }
2725
2726    fn describe_metadata_model(
2727        &self,
2728        ctx: &Ctx,
2729        b: &Value,
2730    ) -> Result<AwsResponse, AwsServiceError> {
2731        req_str(b, "SelectionRules")?;
2732        if let Some(o) = opt_str(b, "Origin") {
2733            check_origin(o)?;
2734        }
2735        let guard = self.state.read();
2736        let data = guard
2737            .get(&ctx.account)
2738            .ok_or_else(|| not_found("Migration project not found."))?;
2739        Self::require_project(data, b)?;
2740        ok(json!({
2741            "MetadataModelName": "root",
2742            "MetadataModelType": "SchemaObject",
2743            "TargetMetadataModels": [],
2744            "Definition": "{}"
2745        }))
2746    }
2747
2748    fn describe_metadata_model_children(
2749        &self,
2750        ctx: &Ctx,
2751        b: &Value,
2752    ) -> Result<AwsResponse, AwsServiceError> {
2753        req_str(b, "SelectionRules")?;
2754        if let Some(o) = opt_str(b, "Origin") {
2755            check_origin(o)?;
2756        }
2757        let guard = self.state.read();
2758        let data = guard
2759            .get(&ctx.account)
2760            .ok_or_else(|| not_found("Migration project not found."))?;
2761        Self::require_project(data, b)?;
2762        list_marker("MetadataModelChildren", vec![], b, &[])
2763    }
2764
2765    /// Shared handler for the family of `Describe*` schema-conversion-request
2766    /// list ops (assessments, conversions, imports, exports, extension packs).
2767    fn describe_scr(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2768        // `MigrationProjectIdentifier` is `@required @length(0, 255)`: enforce
2769        // presence and the length bound (so omit / too-long negative variants
2770        // are rejected), but not existence — several of these list ops
2771        // (`DescribeExtensionPackAssociations`, ...) declare no errors at all,
2772        // so a present-but-unknown project must still answer 200 with an empty
2773        // page.
2774        let ident = req_str_allow_empty(b, "MigrationProjectIdentifier")?;
2775        if ident.chars().count() > 255 {
2776            return Err(validation(
2777                "MigrationProjectIdentifier exceeds 255 characters.",
2778            ));
2779        }
2780        let guard = self.state.read();
2781        let key = guard.get(&ctx.account).and_then(|data| {
2782            opt_str(b, "MigrationProjectIdentifier")
2783                .and_then(|ident| Self::resolve_migration_project_key(data, ident))
2784        });
2785        let rows: Vec<Value> = match (guard.get(&ctx.account), &key) {
2786            (Some(data), Some(key)) => data
2787                .schema_conversion_requests
2788                .values()
2789                .filter(|r| {
2790                    r.get("MigrationProjectArn").and_then(Value::as_str) == Some(key.as_str())
2791                })
2792                .cloned()
2793                .collect(),
2794            _ => Vec::new(),
2795        };
2796        list_marker(
2797            "Requests",
2798            rows,
2799            b,
2800            &[("request-id", &["RequestIdentifier"])],
2801        )
2802    }
2803
2804    /// Shared handler for the family of `Start*` schema-conversion ops. Persists
2805    /// a `SchemaConversionRequest` under the project and returns its id.
2806    fn start_scr(&self, ctx: &Ctx, b: &Value, _kind: &str) -> Result<AwsResponse, AwsServiceError> {
2807        if let Some(o) = opt_str(b, "Origin") {
2808            check_origin(o)?;
2809        }
2810        let mut guard = self.state.write();
2811        let data = guard.get_or_create(&ctx.account);
2812        let key = Self::require_project(data, b)?;
2813        let request_id = Uuid::new_v4().to_string();
2814        let req = json!({
2815            "Status": "SUCCESS",
2816            "RequestIdentifier": request_id,
2817            "MigrationProjectArn": key
2818        });
2819        data.schema_conversion_requests
2820            .insert(request_id.clone(), req);
2821        ok(json!({ "RequestIdentifier": request_id }))
2822    }
2823
2824    fn cancel_scr(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2825        let request_id = req_str(b, "RequestIdentifier")?.to_string();
2826        let mut guard = self.state.write();
2827        let data = guard.get_or_create(&ctx.account);
2828        Self::require_project(data, b)?;
2829        let req = data
2830            .schema_conversion_requests
2831            .get(&request_id)
2832            .cloned()
2833            .unwrap_or_else(|| json!({ "Status": "CANCELLED", "RequestIdentifier": request_id }));
2834        if let Some(r) = data.schema_conversion_requests.get_mut(&request_id) {
2835            r["Status"] = json!("CANCELLED");
2836        }
2837        ok(json!({ "Request": req }))
2838    }
2839
2840    fn export_metadata_model_assessment(
2841        &self,
2842        ctx: &Ctx,
2843        b: &Value,
2844    ) -> Result<AwsResponse, AwsServiceError> {
2845        req_str(b, "SelectionRules")?;
2846        let guard = self.state.read();
2847        let data = guard
2848            .get(&ctx.account)
2849            .ok_or_else(|| not_found("Migration project not found."))?;
2850        Self::require_project(data, b)?;
2851        let entry = |suffix: &str| {
2852            json!({
2853                "S3ObjectKey": format!("assessment-report.{suffix}"),
2854                "ObjectURL": format!("https://s3.amazonaws.com/dms-assessments/assessment-report.{suffix}")
2855            })
2856        };
2857        ok(json!({ "PdfReport": entry("pdf"), "CsvReport": entry("csv") }))
2858    }
2859
2860    fn get_target_selection_rules(
2861        &self,
2862        ctx: &Ctx,
2863        b: &Value,
2864    ) -> Result<AwsResponse, AwsServiceError> {
2865        let rules = req_str(b, "SelectionRules")?.to_string();
2866        let guard = self.state.read();
2867        let data = guard
2868            .get(&ctx.account)
2869            .ok_or_else(|| not_found("Migration project not found."))?;
2870        Self::require_project(data, b)?;
2871        ok(json!({ "TargetSelectionRules": rules }))
2872    }
2873}
2874
2875fn check_origin(o: &str) -> Result<(), AwsServiceError> {
2876    if !o.is_empty() && o != "SOURCE" && o != "TARGET" {
2877        return Err(validation("Origin must be one of [SOURCE, TARGET]."));
2878    }
2879    Ok(())
2880}
2881
2882// ===================== data migrations =====================
2883
2884impl DmsService {
2885    fn create_data_migration(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2886        let project_ident = req_str_allow_empty(b, "MigrationProjectIdentifier")?.to_string();
2887        let mig_type = req_str_allow_empty(b, "DataMigrationType")?.to_string();
2888        check_data_migration_type(&mig_type)?;
2889        req_str_allow_empty(b, "ServiceAccessRoleArn")?;
2890        let name = opt_str(b, "DataMigrationName")
2891            .filter(|s| !s.is_empty())
2892            .map(String::from)
2893            .unwrap_or_else(|| format!("data-migration-{}", &res_id()[..8].to_lowercase()));
2894        let mut guard = self.state.write();
2895        let data = guard.get_or_create(&ctx.account);
2896        if data
2897            .data_migrations
2898            .values()
2899            .any(|v| v.get("DataMigrationName").and_then(Value::as_str) == Some(&name))
2900        {
2901            return Err(already_exists(&format!(
2902                "Data migration {name} already exists."
2903            )));
2904        }
2905        let project_arn = if project_ident.starts_with("arn:") {
2906            project_ident.clone()
2907        } else {
2908            arn(ctx, "migration-project", &project_ident)
2909        };
2910        let dm_arn = arn(ctx, "data-migration", &name);
2911        let mut dm = Map::new();
2912        dm.insert("DataMigrationArn".into(), json!(dm_arn));
2913        dm.insert("DataMigrationName".into(), json!(name));
2914        dm.insert("MigrationProjectArn".into(), json!(project_arn));
2915        dm.insert("DataMigrationType".into(), json!(mig_type));
2916        dm.insert("DataMigrationCreateTime".into(), json!(now_ts()));
2917        dm.insert("DataMigrationStatus".into(), json!("READY"));
2918        copy_fields(
2919            b,
2920            &mut dm,
2921            &[
2922                "ServiceAccessRoleArn",
2923                "SourceDataSettings",
2924                "TargetDataSettings",
2925            ],
2926        );
2927        let dm = Value::Object(dm);
2928        data.data_migrations.insert(dm_arn.clone(), dm.clone());
2929        store_tags(data, &dm_arn, b);
2930        ok(json!({ "DataMigration": dm }))
2931    }
2932
2933    fn describe_data_migrations(
2934        &self,
2935        ctx: &Ctx,
2936        b: &Value,
2937    ) -> Result<AwsResponse, AwsServiceError> {
2938        if let Some(m) = opt_str(b, "Marker") {
2939            if m.chars().count() > 1024 {
2940                return Err(validation("Marker exceeds 1024 characters."));
2941            }
2942        }
2943        let guard = self.state.read();
2944        let rows = guard
2945            .get(&ctx.account)
2946            .map(|d| d.data_migrations.values().cloned().collect())
2947            .unwrap_or_default();
2948        list_marker(
2949            "DataMigrations",
2950            rows,
2951            b,
2952            &[("data-migration-name", &["DataMigrationName"])],
2953        )
2954    }
2955
2956    fn resolve_data_migration_key(data: &DmsData, ident: &str) -> Option<String> {
2957        if data.data_migrations.contains_key(ident) {
2958            return Some(ident.to_string());
2959        }
2960        data.data_migrations
2961            .iter()
2962            .find(|(_, v)| v.get("DataMigrationName").and_then(Value::as_str) == Some(ident))
2963            .map(|(k, _)| k.clone())
2964    }
2965
2966    fn modify_data_migration(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2967        let ident = req_str(b, "DataMigrationIdentifier")?.to_string();
2968        if let Some(mt) = opt_str(b, "DataMigrationType") {
2969            check_data_migration_type(mt)?;
2970        }
2971        let mut guard = self.state.write();
2972        let data = guard.get_or_create(&ctx.account);
2973        let Some(key) = Self::resolve_data_migration_key(data, &ident) else {
2974            return Err(not_found(&format!("Data migration {ident} not found.")));
2975        };
2976        let dm = data.data_migrations.get_mut(&key).unwrap();
2977        let obj = dm.as_object_mut().unwrap();
2978        for f in [
2979            "DataMigrationName",
2980            "ServiceAccessRoleArn",
2981            "DataMigrationType",
2982            "SourceDataSettings",
2983            "TargetDataSettings",
2984        ] {
2985            if let Some(v) = b.get(f) {
2986                obj.insert(f.to_string(), v.clone());
2987            }
2988        }
2989        let dm = dm.clone();
2990        ok(json!({ "DataMigration": dm }))
2991    }
2992
2993    fn delete_data_migration(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
2994        let ident = req_str(b, "DataMigrationIdentifier")?.to_string();
2995        let mut guard = self.state.write();
2996        let data = guard.get_or_create(&ctx.account);
2997        let Some(key) = Self::resolve_data_migration_key(data, &ident) else {
2998            return Err(not_found(&format!("Data migration {ident} not found.")));
2999        };
3000        let dm = data.data_migrations.remove(&key).unwrap();
3001        data.tags.remove(&key);
3002        ok(json!({ "DataMigration": dm }))
3003    }
3004
3005    fn set_data_migration_status(
3006        &self,
3007        ctx: &Ctx,
3008        b: &Value,
3009        status: &str,
3010    ) -> Result<AwsResponse, AwsServiceError> {
3011        let ident = req_str(b, "DataMigrationIdentifier")?.to_string();
3012        let mut guard = self.state.write();
3013        let data = guard.get_or_create(&ctx.account);
3014        let Some(key) = Self::resolve_data_migration_key(data, &ident) else {
3015            return Err(not_found(&format!("Data migration {ident} not found.")));
3016        };
3017        let dm = data.data_migrations.get_mut(&key).unwrap();
3018        dm["DataMigrationStatus"] = json!(status);
3019        if status == "RUNNING" {
3020            dm["DataMigrationStartTime"] = json!(now_ts());
3021        }
3022        let dm = dm.clone();
3023        ok(json!({ "DataMigration": dm }))
3024    }
3025
3026    fn start_data_migration(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3027        if let Some(t) = opt_str(b, "StartType") {
3028            const ALLOWED: &[&str] = &["reload-target", "resume-processing", "start-replication"];
3029            if !t.is_empty() && !ALLOWED.contains(&t) {
3030                return Err(validation(
3031                    "StartType must be one of [reload-target, resume-processing, start-replication].",
3032                ));
3033            }
3034        }
3035        self.set_data_migration_status(ctx, b, "RUNNING")
3036    }
3037
3038    fn stop_data_migration(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3039        self.set_data_migration_status(ctx, b, "STOPPED")
3040    }
3041}
3042
3043fn check_data_migration_type(t: &str) -> Result<(), AwsServiceError> {
3044    const ALLOWED: &[&str] = &["full-load", "cdc", "full-load-and-cdc"];
3045    if !t.is_empty() && !ALLOWED.contains(&t) {
3046        return Err(validation(
3047            "DataMigrationType must be one of [full-load, cdc, full-load-and-cdc].",
3048        ));
3049    }
3050    Ok(())
3051}
3052
3053// ===================== fleet advisor =====================
3054
3055impl DmsService {
3056    fn create_fleet_advisor_collector(
3057        &self,
3058        ctx: &Ctx,
3059        b: &Value,
3060    ) -> Result<AwsResponse, AwsServiceError> {
3061        let name = req_str_allow_empty(b, "CollectorName")?.to_string();
3062        let role = req_str_allow_empty(b, "ServiceAccessRoleArn")?.to_string();
3063        let bucket = req_str_allow_empty(b, "S3BucketName")?.to_string();
3064        let collector_id = Uuid::new_v4().to_string();
3065        let mut guard = self.state.write();
3066        let data = guard.get_or_create(&ctx.account);
3067        let mut collector = Map::new();
3068        collector.insert("CollectorReferencedId".into(), json!(collector_id));
3069        collector.insert("CollectorName".into(), json!(name));
3070        collector.insert("ServiceAccessRoleArn".into(), json!(role));
3071        collector.insert("S3BucketName".into(), json!(bucket));
3072        if let Some(d) = opt_str(b, "Description") {
3073            collector.insert("Description".into(), json!(d));
3074        }
3075        data.fleet_advisor_collectors
3076            .insert(collector_id.clone(), Value::Object(collector.clone()));
3077        // The create response shape is flat (not wrapped in a Collector member).
3078        ok(Value::Object(collector))
3079    }
3080
3081    fn delete_fleet_advisor_collector(
3082        &self,
3083        ctx: &Ctx,
3084        b: &Value,
3085    ) -> Result<AwsResponse, AwsServiceError> {
3086        let id = req_str(b, "CollectorReferencedId")?.to_string();
3087        let mut guard = self.state.write();
3088        let data = guard.get_or_create(&ctx.account);
3089        if data.fleet_advisor_collectors.remove(&id).is_none() {
3090            return Err(AwsServiceError::aws_error(
3091                StatusCode::NOT_FOUND,
3092                "CollectorNotFoundFault",
3093                format!("Collector {id} not found."),
3094            ));
3095        }
3096        ok(json!({}))
3097    }
3098
3099    fn describe_fleet_advisor_collectors(
3100        &self,
3101        ctx: &Ctx,
3102        b: &Value,
3103    ) -> Result<AwsResponse, AwsServiceError> {
3104        let guard = self.state.read();
3105        let rows = guard
3106            .get(&ctx.account)
3107            .map(|d| d.fleet_advisor_collectors.values().cloned().collect())
3108            .unwrap_or_default();
3109        list_next_token(
3110            "Collectors",
3111            rows,
3112            b,
3113            &[
3114                ("collector-referenced-id", &["CollectorReferencedId"]),
3115                ("collector-name", &["CollectorName"]),
3116            ],
3117        )
3118    }
3119
3120    fn describe_fleet_advisor_databases(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3121        list_next_token("Databases", vec![], b, &[])
3122    }
3123
3124    fn delete_fleet_advisor_databases(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3125        let ids = b
3126            .get("DatabaseIds")
3127            .cloned()
3128            .ok_or_else(|| validation("DatabaseIds is required."))?;
3129        ok(json!({ "DatabaseIds": ids }))
3130    }
3131
3132    fn describe_fleet_advisor_lsa_analysis(
3133        &self,
3134        b: &Value,
3135    ) -> Result<AwsResponse, AwsServiceError> {
3136        list_next_token("Analysis", vec![], b, &[])
3137    }
3138
3139    fn describe_fleet_advisor_schema_object_summary(
3140        &self,
3141        b: &Value,
3142    ) -> Result<AwsResponse, AwsServiceError> {
3143        list_next_token("FleetAdvisorSchemaObjects", vec![], b, &[])
3144    }
3145
3146    fn describe_fleet_advisor_schemas(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3147        list_next_token("FleetAdvisorSchemas", vec![], b, &[])
3148    }
3149
3150    fn run_fleet_advisor_lsa_analysis(&self) -> Result<AwsResponse, AwsServiceError> {
3151        ok(json!({ "LsaAnalysisId": Uuid::new_v4().to_string(), "Status": "RUNNING" }))
3152    }
3153}
3154
3155// ===================== recommendations =====================
3156
3157impl DmsService {
3158    fn start_recommendations(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3159        let db_id = req_str(b, "DatabaseId")?.to_string();
3160        if b.get("Settings").is_none() {
3161            return Err(validation("Settings is required."));
3162        }
3163        let mut guard = self.state.write();
3164        let data = guard.get_or_create(&ctx.account);
3165        data.recommendations.insert(
3166            db_id.clone(),
3167            json!({
3168                "DatabaseId": db_id,
3169                "Status": "in-progress",
3170                "Settings": b.get("Settings").cloned().unwrap_or(json!({}))
3171            }),
3172        );
3173        ok(json!({}))
3174    }
3175
3176    fn batch_start_recommendations(
3177        &self,
3178        ctx: &Ctx,
3179        b: &Value,
3180    ) -> Result<AwsResponse, AwsServiceError> {
3181        let mut guard = self.state.write();
3182        let data = guard.get_or_create(&ctx.account);
3183        if let Some(entries) = b.get("Data").and_then(Value::as_array) {
3184            for e in entries {
3185                if let Some(db_id) = e.get("DatabaseId").and_then(Value::as_str) {
3186                    data.recommendations.insert(
3187                        db_id.to_string(),
3188                        json!({ "DatabaseId": db_id, "Status": "in-progress" }),
3189                    );
3190                }
3191            }
3192        }
3193        ok(json!({ "ErrorEntries": [] }))
3194    }
3195
3196    fn describe_recommendations(
3197        &self,
3198        ctx: &Ctx,
3199        b: &Value,
3200    ) -> Result<AwsResponse, AwsServiceError> {
3201        let guard = self.state.read();
3202        let rows = guard
3203            .get(&ctx.account)
3204            .map(|d| d.recommendations.values().cloned().collect())
3205            .unwrap_or_default();
3206        list_next_token(
3207            "Recommendations",
3208            rows,
3209            b,
3210            &[("database-id", &["DatabaseId"]), ("status", &["Status"])],
3211        )
3212    }
3213
3214    fn describe_recommendation_limitations(
3215        &self,
3216        b: &Value,
3217    ) -> Result<AwsResponse, AwsServiceError> {
3218        list_next_token("Limitations", vec![], b, &[])
3219    }
3220}
3221
3222// ===================== account / catalogue =====================
3223
3224impl DmsService {
3225    fn describe_account_attributes(&self, ctx: &Ctx) -> Result<AwsResponse, AwsServiceError> {
3226        let guard = self.state.read();
3227        let data = guard.get(&ctx.account);
3228        let count = |f: fn(&DmsData) -> usize| data.map(f).unwrap_or(0) as i64;
3229        let quotas = json!([
3230            { "AccountQuotaName": "ReplicationInstances", "Used": count(|d| d.replication_instances.len()), "Max": 20 },
3231            { "AccountQuotaName": "ReplicationSubnetGroups", "Used": count(|d| d.subnet_groups.len()), "Max": 20 },
3232            { "AccountQuotaName": "Endpoints", "Used": count(|d| d.endpoints.len()), "Max": 1000 },
3233            { "AccountQuotaName": "ReplicationTasks", "Used": count(|d| d.replication_tasks.len()), "Max": 200 },
3234            { "AccountQuotaName": "AllocatedStorage", "Used": 0, "Max": 10000 },
3235            { "AccountQuotaName": "EventSubscriptions", "Used": count(|d| d.event_subscriptions.len()), "Max": 20 }
3236        ]);
3237        ok(json!({
3238            "AccountQuotas": quotas,
3239            "UniqueAccountIdentifier": format!("dms-{}", ctx.account)
3240        }))
3241    }
3242
3243    fn describe_engine_versions(&self, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3244        let rows: Vec<Value> = ["3.4.7", "3.5.1", "3.5.2", "3.5.3", "3.5.4"]
3245            .iter()
3246            .map(|v| {
3247                json!({
3248                    "Version": v,
3249                    "Lifecycle": "available",
3250                    "ReleaseStatus": "prod",
3251                    "AvailableUpgrades": []
3252                })
3253            })
3254            .collect();
3255        list_marker("EngineVersions", rows, b, &[])
3256    }
3257}
3258
3259// ===================== tagging =====================
3260
3261impl DmsService {
3262    fn add_tags_to_resource(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3263        let resource_arn = req_str(b, "ResourceArn")?.to_string();
3264        if b.get("Tags").and_then(Value::as_array).is_none() {
3265            return Err(validation("Tags is required."));
3266        }
3267        let mut guard = self.state.write();
3268        let data = guard.get_or_create(&ctx.account);
3269        store_tags(data, &resource_arn, b);
3270        ok(json!({}))
3271    }
3272
3273    fn remove_tags_from_resource(
3274        &self,
3275        ctx: &Ctx,
3276        b: &Value,
3277    ) -> Result<AwsResponse, AwsServiceError> {
3278        let resource_arn = req_str(b, "ResourceArn")?.to_string();
3279        let keys: Vec<String> = b
3280            .get("TagKeys")
3281            .and_then(Value::as_array)
3282            .map(|a| {
3283                a.iter()
3284                    .filter_map(|v| v.as_str().map(String::from))
3285                    .collect()
3286            })
3287            .ok_or_else(|| validation("TagKeys is required."))?;
3288        let mut guard = self.state.write();
3289        let data = guard.get_or_create(&ctx.account);
3290        if let Some(entry) = data.tags.get_mut(&resource_arn) {
3291            for k in &keys {
3292                entry.remove(k);
3293            }
3294        }
3295        ok(json!({}))
3296    }
3297
3298    fn list_tags_for_resource(&self, ctx: &Ctx, b: &Value) -> Result<AwsResponse, AwsServiceError> {
3299        let mut arns: Vec<String> = Vec::new();
3300        if let Some(a) = opt_str(b, "ResourceArn") {
3301            arns.push(a.to_string());
3302        }
3303        if let Some(list) = b.get("ResourceArnList").and_then(Value::as_array) {
3304            arns.extend(list.iter().filter_map(|v| v.as_str().map(String::from)));
3305        }
3306        let guard = self.state.read();
3307        let data = guard.get(&ctx.account);
3308        let mut tag_list = Vec::new();
3309        for a in &arns {
3310            if let Some(entry) = data.and_then(|d| d.tags.get(a)) {
3311                for (k, v) in entry {
3312                    tag_list.push(json!({ "Key": k, "Value": v, "ResourceArn": a }));
3313                }
3314            }
3315        }
3316        ok(json!({ "TagList": tag_list }))
3317    }
3318}