1use std::sync::Arc;
16
17use async_trait::async_trait;
18use http::StatusCode;
19use serde_json::{json, Map, Value};
20use tokio::sync::Mutex as AsyncMutex;
21
22use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
23use fakecloud_persistence::SnapshotStore;
24
25use crate::shared::{
26 data_location, decode_blob, job_id, now_epoch, parse_file, parse_resource_arn, resource_arn,
27};
28use crate::state::{SharedTranslateState, TranslateData};
29
30pub const TRANSLATE_ACTIONS: &[&str] = &[
32 "CreateParallelData",
33 "DeleteParallelData",
34 "DeleteTerminology",
35 "DescribeTextTranslationJob",
36 "GetParallelData",
37 "GetTerminology",
38 "ImportTerminology",
39 "ListLanguages",
40 "ListParallelData",
41 "ListTagsForResource",
42 "ListTerminologies",
43 "ListTextTranslationJobs",
44 "StartTextTranslationJob",
45 "StopTextTranslationJob",
46 "TagResource",
47 "TranslateDocument",
48 "TranslateText",
49 "UntagResource",
50 "UpdateParallelData",
51];
52
53fn is_mutating_action(action: &str) -> bool {
57 const READ_PREFIXES: &[&str] = &["Get", "List", "Describe"];
58 if action == "TranslateText" || action == "TranslateDocument" {
59 return false;
60 }
61 !READ_PREFIXES.iter().any(|p| action.starts_with(p))
62}
63
64pub struct TranslateService {
65 state: SharedTranslateState,
66 snapshot_store: Option<Arc<dyn SnapshotStore>>,
67 snapshot_lock: Arc<AsyncMutex<()>>,
68}
69
70impl TranslateService {
71 pub fn new(state: SharedTranslateState) -> Self {
72 Self {
73 state,
74 snapshot_store: None,
75 snapshot_lock: Arc::new(AsyncMutex::new(())),
76 }
77 }
78
79 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
80 self.snapshot_store = Some(store);
81 self
82 }
83
84 async fn save(&self) {
85 crate::persistence::save_snapshot(
86 &self.state,
87 self.snapshot_store.clone(),
88 &self.snapshot_lock,
89 )
90 .await;
91 }
92
93 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
95 let store = self.snapshot_store.clone()?;
96 let state = self.state.clone();
97 let lock = self.snapshot_lock.clone();
98 Some(Arc::new(move || {
99 let state = state.clone();
100 let store = store.clone();
101 let lock = lock.clone();
102 Box::pin(async move {
103 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
104 })
105 }))
106 }
107
108 fn with_account_mut<R>(&self, req: &AwsRequest, f: impl FnOnce(&mut TranslateData) -> R) -> R {
110 let mut guard = self.state.write();
111 let acct = guard.get_or_create(&req.account_id);
112 f(acct)
113 }
114}
115
116#[async_trait]
117impl AwsService for TranslateService {
118 fn service_name(&self) -> &str {
119 "translate"
120 }
121
122 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
123 let action = req.action.clone();
124 if let Err(msg) = crate::validate::validate_input(&action, &req.json_body()) {
125 let code = crate::validate::validation_error_code(&action);
126 return Err(AwsServiceError::aws_error(
127 StatusCode::BAD_REQUEST,
128 code,
129 msg,
130 ));
131 }
132 let result = self.dispatch(&action, &req);
133 if is_mutating_action(&action)
134 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
135 {
136 self.save().await;
137 }
138 result
139 }
140
141 fn supported_actions(&self) -> &[&str] {
142 TRANSLATE_ACTIONS
143 }
144}
145
146impl TranslateService {
147 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
148 let body = req.json_body();
149 match action {
150 "TranslateText" => self.translate_text(req, &body),
152 "TranslateDocument" => self.translate_document(req, &body),
153 "StartTextTranslationJob" => self.start_text_translation_job(req, &body),
155 "DescribeTextTranslationJob" => self.describe_text_translation_job(req, &body),
156 "ListTextTranslationJobs" => self.list_text_translation_jobs(req, &body),
157 "StopTextTranslationJob" => self.stop_text_translation_job(req, &body),
158 "CreateParallelData" => self.create_parallel_data(req, &body),
160 "GetParallelData" => self.get_parallel_data(req, &body),
161 "UpdateParallelData" => self.update_parallel_data(req, &body),
162 "ListParallelData" => self.list_parallel_data(req, &body),
163 "DeleteParallelData" => self.delete_parallel_data(req, &body),
164 "ImportTerminology" => self.import_terminology(req, &body),
166 "GetTerminology" => self.get_terminology(req, &body),
167 "ListTerminologies" => self.list_terminologies(req, &body),
168 "DeleteTerminology" => self.delete_terminology(req, &body),
169 "ListLanguages" => self.list_languages(req, &body),
171 "TagResource" => self.tag_resource(req, &body),
173 "UntagResource" => self.untag_resource(req, &body),
174 "ListTagsForResource" => self.list_tags_for_resource(req, &body),
175 _ => Err(AwsServiceError::action_not_implemented(
176 self.service_name(),
177 action,
178 )),
179 }
180 }
181}
182
183fn invalid_parameter(msg: impl Into<String>) -> AwsServiceError {
186 AwsServiceError::aws_error(
187 StatusCode::BAD_REQUEST,
188 "InvalidParameterValueException",
189 msg.into(),
190 )
191}
192
193fn conflict(msg: impl Into<String>) -> AwsServiceError {
194 AwsServiceError::aws_error(StatusCode::CONFLICT, "ConflictException", msg.into())
195}
196
197fn not_found(msg: impl Into<String>) -> AwsServiceError {
198 AwsServiceError::aws_error(
199 StatusCode::NOT_FOUND,
200 "ResourceNotFoundException",
201 msg.into(),
202 )
203}
204
205fn ok(value: Value) -> Result<AwsResponse, AwsServiceError> {
206 Ok(AwsResponse::ok_json(value))
207}
208
209fn str_member<'a>(body: &'a Value, name: &str) -> Option<&'a str> {
212 body.get(name).and_then(Value::as_str)
213}
214
215fn copy_members(body: &Value, obj: &mut Map<String, Value>, names: &[&str]) {
217 for name in names {
218 if let Some(v) = body.get(*name).filter(|v| !v.is_null()) {
219 obj.insert((*name).to_string(), v.clone());
220 }
221 }
222}
223
224fn taglist_to_map(tags: &Value) -> std::collections::BTreeMap<String, String> {
226 let mut out = std::collections::BTreeMap::new();
227 if let Some(arr) = tags.as_array() {
228 for t in arr {
229 if let (Some(k), Some(v)) = (str_member(t, "Key"), str_member(t, "Value")) {
230 out.insert(k.to_string(), v.to_string());
231 }
232 }
233 }
234 out
235}
236
237fn map_to_taglist(map: &std::collections::BTreeMap<String, String>) -> Value {
239 Value::Array(
240 map.iter()
241 .map(|(k, v)| json!({ "Key": k, "Value": v }))
242 .collect(),
243 )
244}
245
246impl TranslateData {
247 fn store_tags(&mut self, arn: &str, body: &Value) {
249 if let Some(tags) = body.get("Tags").filter(|v| !v.is_null()) {
250 let map = taglist_to_map(tags);
251 if !map.is_empty() {
252 self.tags.insert(arn.to_string(), map);
253 }
254 }
255 }
256
257 fn resource_exists(&self, rtype: &str, name: &str) -> bool {
259 match rtype {
260 "terminology" => self.terminologies.contains_key(name),
261 "parallel-data" => self.parallel_data.contains_key(name),
262 _ => false,
263 }
264 }
265}
266
267impl TranslateService {
270 fn translate_text(
271 &self,
272 req: &AwsRequest,
273 body: &Value,
274 ) -> Result<AwsResponse, AwsServiceError> {
275 let text = str_member(body, "Text").unwrap_or_default().to_string();
276 let source = str_member(body, "SourceLanguageCode")
277 .unwrap_or_default()
278 .to_string();
279 let target = str_member(body, "TargetLanguageCode")
280 .unwrap_or_default()
281 .to_string();
282 let applied = self.applied_terminologies(req, body)?;
283 let mut out = Map::new();
284 out.insert("TranslatedText".into(), json!(text));
287 out.insert("SourceLanguageCode".into(), json!(source));
288 out.insert("TargetLanguageCode".into(), json!(target));
289 if let Some(a) = applied {
290 out.insert("AppliedTerminologies".into(), a);
291 }
292 if let Some(s) = body.get("Settings").filter(|v| !v.is_null()) {
293 out.insert("AppliedSettings".into(), s.clone());
294 }
295 ok(Value::Object(out))
296 }
297
298 fn translate_document(
299 &self,
300 req: &AwsRequest,
301 body: &Value,
302 ) -> Result<AwsResponse, AwsServiceError> {
303 let source = str_member(body, "SourceLanguageCode")
304 .unwrap_or_default()
305 .to_string();
306 let target = str_member(body, "TargetLanguageCode")
307 .unwrap_or_default()
308 .to_string();
309 let content = body
311 .get("Document")
312 .and_then(|d| d.get("Content"))
313 .cloned()
314 .unwrap_or(json!(""));
315 let applied = self.applied_terminologies(req, body)?;
316 let mut out = Map::new();
317 out.insert("TranslatedDocument".into(), json!({ "Content": content }));
318 out.insert("SourceLanguageCode".into(), json!(source));
319 out.insert("TargetLanguageCode".into(), json!(target));
320 if let Some(a) = applied {
321 out.insert("AppliedTerminologies".into(), a);
322 }
323 if let Some(s) = body.get("Settings").filter(|v| !v.is_null()) {
324 out.insert("AppliedSettings".into(), s.clone());
325 }
326 ok(Value::Object(out))
327 }
328
329 fn applied_terminologies(
333 &self,
334 req: &AwsRequest,
335 body: &Value,
336 ) -> Result<Option<Value>, AwsServiceError> {
337 let Some(names) = body.get("TerminologyNames").and_then(Value::as_array) else {
338 return Ok(None);
339 };
340 if names.is_empty() {
341 return Ok(None);
342 }
343 self.with_account_mut(req, |d| {
344 let mut applied = Vec::new();
345 for n in names.iter().filter_map(Value::as_str) {
346 if !d.terminologies.contains_key(n) {
347 return Err(not_found(format!(
348 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {n}"
349 )));
350 }
351 applied.push(json!({ "Name": n, "Terms": [] }));
354 }
355 Ok(Some(Value::Array(applied)))
356 })
357 }
358}
359
360impl TranslateService {
363 fn start_text_translation_job(
364 &self,
365 req: &AwsRequest,
366 body: &Value,
367 ) -> Result<AwsResponse, AwsServiceError> {
368 self.with_account_mut(req, |d| {
369 for n in body
371 .get("TerminologyNames")
372 .and_then(Value::as_array)
373 .into_iter()
374 .flatten()
375 .filter_map(Value::as_str)
376 {
377 if !d.terminologies.contains_key(n) {
378 return Err(not_found(format!(
379 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {n}"
380 )));
381 }
382 }
383 for n in body
384 .get("ParallelDataNames")
385 .and_then(Value::as_array)
386 .into_iter()
387 .flatten()
388 .filter_map(Value::as_str)
389 {
390 if !d.parallel_data.contains_key(n) {
391 return Err(not_found(format!(
392 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {n}"
393 )));
394 }
395 }
396 let id = job_id();
397 let now = now_epoch();
398 let mut obj = Map::new();
399 obj.insert("JobId".into(), json!(id));
400 obj.insert("JobStatus".into(), json!("SUBMITTED"));
401 obj.insert("SubmittedTime".into(), json!(now));
402 copy_members(
403 body,
404 &mut obj,
405 &[
406 "JobName",
407 "InputDataConfig",
408 "OutputDataConfig",
409 "DataAccessRoleArn",
410 "SourceLanguageCode",
411 "TargetLanguageCodes",
412 "TerminologyNames",
413 "ParallelDataNames",
414 "Settings",
415 ],
416 );
417 d.text_translation_jobs
418 .insert(id.clone(), Value::Object(obj));
419 ok(json!({ "JobId": id, "JobStatus": "SUBMITTED" }))
420 })
421 }
422
423 fn describe_text_translation_job(
424 &self,
425 req: &AwsRequest,
426 body: &Value,
427 ) -> Result<AwsResponse, AwsServiceError> {
428 let id = str_member(body, "JobId").unwrap_or_default().to_string();
429 self.with_account_mut(req, |d| {
430 d.reconcile();
431 let Some(job) = d.text_translation_jobs.get(&id).cloned() else {
432 return Err(not_found(format!(
433 "Cannot find the resource you are looking for. Review the resource and try again: {id}"
434 )));
435 };
436 ok(json!({ "TextTranslationJobProperties": job }))
437 })
438 }
439
440 fn list_text_translation_jobs(
441 &self,
442 req: &AwsRequest,
443 body: &Value,
444 ) -> Result<AwsResponse, AwsServiceError> {
445 self.with_account_mut(req, |d| {
446 d.reconcile();
447 let filter = body.get("Filter");
448 let name_eq = filter.and_then(|f| str_member(f, "JobName"));
449 let status_eq = filter.and_then(|f| str_member(f, "JobStatus"));
450 let jobs: Vec<Value> = d
451 .text_translation_jobs
452 .values()
453 .filter(|j| {
454 name_eq
455 .map(|n| j.get("JobName").and_then(Value::as_str) == Some(n))
456 .unwrap_or(true)
457 && status_eq
458 .map(|s| j.get("JobStatus").and_then(Value::as_str) == Some(s))
459 .unwrap_or(true)
460 })
461 .cloned()
462 .collect();
463 ok(json!({ "TextTranslationJobPropertiesList": jobs }))
464 })
465 }
466
467 fn stop_text_translation_job(
468 &self,
469 req: &AwsRequest,
470 body: &Value,
471 ) -> Result<AwsResponse, AwsServiceError> {
472 let id = str_member(body, "JobId").unwrap_or_default().to_string();
473 self.with_account_mut(req, |d| {
474 let Some(job) = d.text_translation_jobs.get_mut(&id) else {
475 return Err(not_found(format!(
476 "Cannot find the resource you are looking for. Review the resource and try again: {id}"
477 )));
478 };
479 let status = job
480 .get("JobStatus")
481 .and_then(Value::as_str)
482 .unwrap_or("")
483 .to_string();
484 let new_status = if matches!(status.as_str(), "SUBMITTED" | "IN_PROGRESS") {
485 if let Some(obj) = job.as_object_mut() {
486 obj.insert("JobStatus".into(), json!("STOP_REQUESTED"));
487 }
488 "STOP_REQUESTED".to_string()
489 } else {
490 status
491 };
492 ok(json!({ "JobId": id, "JobStatus": new_status }))
493 })
494 }
495}
496
497impl TranslateService {
500 fn create_parallel_data(
501 &self,
502 req: &AwsRequest,
503 body: &Value,
504 ) -> Result<AwsResponse, AwsServiceError> {
505 let name = str_member(body, "Name").unwrap_or_default().to_string();
506 let region = req.region.clone();
507 let account = req.account_id.clone();
508 self.with_account_mut(req, |d| {
509 if d.parallel_data.contains_key(&name) {
510 return Err(conflict(format!(
511 "Parallel data with the name {name} already exists. Use a different name and try again."
512 )));
513 }
514 let now = now_epoch();
515 let arn = resource_arn(®ion, &account, "parallel-data", &name);
516 let mut obj = Map::new();
517 obj.insert("Name".into(), json!(name));
518 obj.insert("Arn".into(), json!(arn));
519 obj.insert("Status".into(), json!("CREATING"));
520 obj.insert("ImportedDataSize".into(), json!(0));
521 obj.insert("ImportedRecordCount".into(), json!(0));
522 obj.insert("FailedRecordCount".into(), json!(0));
523 obj.insert("SkippedRecordCount".into(), json!(0));
524 obj.insert("CreatedAt".into(), json!(now));
525 obj.insert("LastUpdatedAt".into(), json!(now));
526 copy_members(
527 body,
528 &mut obj,
529 &["Description", "ParallelDataConfig", "EncryptionKey"],
530 );
531 d.parallel_data
532 .insert(name.clone(), Value::Object(obj));
533 d.store_tags(&arn, body);
534 ok(json!({ "Name": name, "Status": "CREATING" }))
535 })
536 }
537
538 fn get_parallel_data(
539 &self,
540 req: &AwsRequest,
541 body: &Value,
542 ) -> Result<AwsResponse, AwsServiceError> {
543 let name = str_member(body, "Name").unwrap_or_default().to_string();
544 let region = req.region.clone();
545 let account = req.account_id.clone();
546 self.with_account_mut(req, |d| {
547 d.reconcile();
548 let Some(pd) = d.parallel_data.get(&name).cloned() else {
549 return Err(not_found(format!(
550 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {name}"
551 )));
552 };
553 let loc = json!({
554 "RepositoryType": "S3",
555 "Location": data_location(®ion, &account, "parallel-data", &name, "input.tsv"),
556 });
557 ok(json!({
558 "ParallelDataProperties": pd,
559 "DataLocation": loc,
560 }))
561 })
562 }
563
564 fn update_parallel_data(
565 &self,
566 req: &AwsRequest,
567 body: &Value,
568 ) -> Result<AwsResponse, AwsServiceError> {
569 let name = str_member(body, "Name").unwrap_or_default().to_string();
570 self.with_account_mut(req, |d| {
571 d.reconcile();
572 let Some(pd) = d.parallel_data.get_mut(&name) else {
573 return Err(not_found(format!(
574 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {name}"
575 )));
576 };
577 let now = now_epoch();
578 if let Some(obj) = pd.as_object_mut() {
579 obj.insert("LatestUpdateAttemptStatus".into(), json!("UPDATING"));
580 obj.insert("LatestUpdateAttemptAt".into(), json!(now));
581 obj.insert("LastUpdatedAt".into(), json!(now));
582 if let Some(cfg) = body.get("ParallelDataConfig").filter(|v| !v.is_null()) {
583 obj.insert("ParallelDataConfig".into(), cfg.clone());
584 }
585 if let Some(desc) = body.get("Description").filter(|v| !v.is_null()) {
586 obj.insert("Description".into(), desc.clone());
587 }
588 }
589 let status = pd
590 .get("Status")
591 .cloned()
592 .unwrap_or(json!("ACTIVE"));
593 ok(json!({
594 "Name": name,
595 "Status": status,
596 "LatestUpdateAttemptStatus": "UPDATING",
597 "LatestUpdateAttemptAt": now,
598 }))
599 })
600 }
601
602 fn list_parallel_data(
603 &self,
604 req: &AwsRequest,
605 _body: &Value,
606 ) -> Result<AwsResponse, AwsServiceError> {
607 self.with_account_mut(req, |d| {
608 d.reconcile();
609 let list: Vec<Value> = d.parallel_data.values().cloned().collect();
610 ok(json!({ "ParallelDataPropertiesList": list }))
611 })
612 }
613
614 fn delete_parallel_data(
615 &self,
616 req: &AwsRequest,
617 body: &Value,
618 ) -> Result<AwsResponse, AwsServiceError> {
619 let name = str_member(body, "Name").unwrap_or_default().to_string();
620 let region = req.region.clone();
621 let account = req.account_id.clone();
622 self.with_account_mut(req, |d| {
623 if d.parallel_data.remove(&name).is_none() {
624 return Err(not_found(format!(
625 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {name}"
626 )));
627 }
628 d.tags
629 .remove(&resource_arn(®ion, &account, "parallel-data", &name));
630 ok(json!({ "Name": name, "Status": "DELETING" }))
631 })
632 }
633}
634
635impl TranslateService {
638 fn import_terminology(
639 &self,
640 req: &AwsRequest,
641 body: &Value,
642 ) -> Result<AwsResponse, AwsServiceError> {
643 let name = str_member(body, "Name").unwrap_or_default().to_string();
644 let region = req.region.clone();
645 let account = req.account_id.clone();
646 let td = body.get("TerminologyData");
647 let format = td
648 .and_then(|t| str_member(t, "Format"))
649 .unwrap_or("CSV")
650 .to_string();
651 let directionality = td
652 .and_then(|t| str_member(t, "Directionality"))
653 .unwrap_or("UNI")
654 .to_string();
655 let facts = td
656 .and_then(|t| str_member(t, "File"))
657 .map(|f| parse_file(&decode_blob(f), &format))
658 .unwrap_or_default();
659 self.with_account_mut(req, |d| {
660 let now = now_epoch();
661 let created_at = d
664 .terminologies
665 .get(&name)
666 .and_then(|t| t.get("CreatedAt"))
667 .cloned()
668 .unwrap_or(json!(now));
669 let arn = resource_arn(®ion, &account, "terminology", &name);
670 let mut obj = Map::new();
671 obj.insert("Name".into(), json!(name));
672 obj.insert("Arn".into(), json!(arn));
673 obj.insert("Format".into(), json!(format));
674 obj.insert("Directionality".into(), json!(directionality));
675 obj.insert("SizeBytes".into(), json!(facts.size_bytes));
676 obj.insert("TermCount".into(), json!(facts.record_count));
677 obj.insert("SkippedTermCount".into(), json!(0));
678 obj.insert("CreatedAt".into(), created_at);
679 obj.insert("LastUpdatedAt".into(), json!(now));
680 if let Some(src) = &facts.source_language {
681 obj.insert("SourceLanguageCode".into(), json!(src));
682 }
683 if !facts.target_languages.is_empty() {
684 obj.insert("TargetLanguageCodes".into(), json!(facts.target_languages));
685 }
686 copy_members(body, &mut obj, &["Description", "EncryptionKey"]);
687 d.terminologies
688 .insert(name.clone(), Value::Object(obj.clone()));
689 d.store_tags(&arn, body);
690 ok(json!({ "TerminologyProperties": Value::Object(obj) }))
691 })
692 }
693
694 fn get_terminology(
695 &self,
696 req: &AwsRequest,
697 body: &Value,
698 ) -> Result<AwsResponse, AwsServiceError> {
699 let name = str_member(body, "Name").unwrap_or_default().to_string();
700 let region = req.region.clone();
701 let account = req.account_id.clone();
702 self.with_account_mut(req, |d| {
703 let Some(term) = d.terminologies.get(&name).cloned() else {
704 return Err(not_found(format!(
705 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {name}"
706 )));
707 };
708 let format = term
709 .get("Format")
710 .and_then(Value::as_str)
711 .unwrap_or("CSV")
712 .to_lowercase();
713 let loc = json!({
714 "RepositoryType": "S3",
715 "Location": data_location(®ion, &account, "terminology", &name, &format!("terminology.{format}")),
716 });
717 ok(json!({
718 "TerminologyProperties": term,
719 "TerminologyDataLocation": loc,
720 }))
721 })
722 }
723
724 fn list_terminologies(
725 &self,
726 req: &AwsRequest,
727 _body: &Value,
728 ) -> Result<AwsResponse, AwsServiceError> {
729 self.with_account_mut(req, |d| {
730 let list: Vec<Value> = d.terminologies.values().cloned().collect();
731 ok(json!({ "TerminologyPropertiesList": list }))
732 })
733 }
734
735 fn delete_terminology(
736 &self,
737 req: &AwsRequest,
738 body: &Value,
739 ) -> Result<AwsResponse, AwsServiceError> {
740 let name = str_member(body, "Name").unwrap_or_default().to_string();
741 let region = req.region.clone();
742 let account = req.account_id.clone();
743 self.with_account_mut(req, |d| {
744 if d.terminologies.remove(&name).is_none() {
745 return Err(not_found(format!(
746 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {name}"
747 )));
748 }
749 d.tags
750 .remove(&resource_arn(®ion, &account, "terminology", &name));
751 ok(json!({}))
752 })
753 }
754}
755
756const SUPPORTED_LANGUAGES: &[(&str, &str)] = &[
763 ("af", "Afrikaans"),
764 ("sq", "Albanian"),
765 ("am", "Amharic"),
766 ("ar", "Arabic"),
767 ("hy", "Armenian"),
768 ("az", "Azerbaijani"),
769 ("bn", "Bengali"),
770 ("bs", "Bosnian"),
771 ("bg", "Bulgarian"),
772 ("ca", "Catalan"),
773 ("zh", "Chinese (Simplified)"),
774 ("zh-TW", "Chinese (Traditional)"),
775 ("hr", "Croatian"),
776 ("cs", "Czech"),
777 ("da", "Danish"),
778 ("fa-AF", "Dari"),
779 ("nl", "Dutch"),
780 ("en", "English"),
781 ("et", "Estonian"),
782 ("fa", "Farsi (Persian)"),
783 ("tl", "Filipino, Tagalog"),
784 ("fi", "Finnish"),
785 ("fr", "French"),
786 ("fr-CA", "French (Canada)"),
787 ("ka", "Georgian"),
788 ("de", "German"),
789 ("el", "Greek"),
790 ("gu", "Gujarati"),
791 ("ht", "Haitian Creole"),
792 ("ha", "Hausa"),
793 ("he", "Hebrew"),
794 ("hi", "Hindi"),
795 ("hu", "Hungarian"),
796 ("is", "Icelandic"),
797 ("id", "Indonesian"),
798 ("ga", "Irish"),
799 ("it", "Italian"),
800 ("ja", "Japanese"),
801 ("kn", "Kannada"),
802 ("kk", "Kazakh"),
803 ("ko", "Korean"),
804 ("lv", "Latvian"),
805 ("lt", "Lithuanian"),
806 ("mk", "Macedonian"),
807 ("ms", "Malay"),
808 ("ml", "Malayalam"),
809 ("mt", "Maltese"),
810 ("mr", "Marathi"),
811 ("mn", "Mongolian"),
812 ("no", "Norwegian (Bokmal)"),
813 ("ps", "Pashto"),
814 ("pl", "Polish"),
815 ("pt", "Portuguese (Brazil)"),
816 ("pt-PT", "Portuguese (Portugal)"),
817 ("pa", "Punjabi"),
818 ("ro", "Romanian"),
819 ("ru", "Russian"),
820 ("sr", "Serbian"),
821 ("si", "Sinhala"),
822 ("sk", "Slovak"),
823 ("sl", "Slovenian"),
824 ("so", "Somali"),
825 ("es", "Spanish"),
826 ("es-MX", "Spanish (Mexico)"),
827 ("sw", "Swahili"),
828 ("sv", "Swedish"),
829 ("ta", "Tamil"),
830 ("te", "Telugu"),
831 ("th", "Thai"),
832 ("tr", "Turkish"),
833 ("uk", "Ukrainian"),
834 ("ur", "Urdu"),
835 ("uz", "Uzbek"),
836 ("vi", "Vietnamese"),
837 ("cy", "Welsh"),
838];
839
840impl TranslateService {
841 fn list_languages(
842 &self,
843 _req: &AwsRequest,
844 body: &Value,
845 ) -> Result<AwsResponse, AwsServiceError> {
846 let display = str_member(body, "DisplayLanguageCode").unwrap_or("en");
847 let languages: Vec<Value> = SUPPORTED_LANGUAGES
848 .iter()
849 .map(|(code, name)| json!({ "LanguageCode": code, "LanguageName": name }))
850 .collect();
851 ok(json!({
852 "Languages": languages,
853 "DisplayLanguageCode": display,
854 }))
855 }
856}
857
858impl TranslateService {
861 fn tag_resource(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
862 let arn = str_member(body, "ResourceArn")
863 .unwrap_or_default()
864 .to_string();
865 let Some((rtype, name)) = parse_resource_arn(&arn) else {
866 return Err(invalid_parameter(format!("Invalid resource ARN: {arn}")));
867 };
868 self.with_account_mut(req, |d| {
869 if !d.resource_exists(&rtype, &name) {
870 return Err(not_found(format!(
871 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {arn}"
872 )));
873 }
874 let entry = d.tags.entry(arn.clone()).or_default();
875 for (k, v) in taglist_to_map(body.get("Tags").unwrap_or(&Value::Null)) {
876 entry.insert(k, v);
877 }
878 ok(json!({}))
879 })
880 }
881
882 fn untag_resource(
883 &self,
884 req: &AwsRequest,
885 body: &Value,
886 ) -> Result<AwsResponse, AwsServiceError> {
887 let arn = str_member(body, "ResourceArn")
888 .unwrap_or_default()
889 .to_string();
890 let Some((rtype, name)) = parse_resource_arn(&arn) else {
891 return Err(invalid_parameter(format!("Invalid resource ARN: {arn}")));
892 };
893 self.with_account_mut(req, |d| {
894 if !d.resource_exists(&rtype, &name) {
895 return Err(not_found(format!(
896 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {arn}"
897 )));
898 }
899 if let Some(entry) = d.tags.get_mut(&arn) {
900 if let Some(keys) = body.get("TagKeys").and_then(Value::as_array) {
901 for k in keys.iter().filter_map(Value::as_str) {
902 entry.remove(k);
903 }
904 }
905 }
906 ok(json!({}))
907 })
908 }
909
910 fn list_tags_for_resource(
911 &self,
912 req: &AwsRequest,
913 body: &Value,
914 ) -> Result<AwsResponse, AwsServiceError> {
915 let arn = str_member(body, "ResourceArn")
916 .unwrap_or_default()
917 .to_string();
918 let Some((rtype, name)) = parse_resource_arn(&arn) else {
919 return Err(invalid_parameter(format!("Invalid resource ARN: {arn}")));
920 };
921 self.with_account_mut(req, |d| {
922 if !d.resource_exists(&rtype, &name) {
923 return Err(not_found(format!(
924 "The resource you are looking for has not been found. Review the resource you're looking for and try again: {arn}"
925 )));
926 }
927 let tags = d.tags.get(&arn).map(map_to_taglist).unwrap_or(json!([]));
928 ok(json!({ "Tags": tags }))
929 })
930 }
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936 use base64::Engine;
937 use fakecloud_core::multi_account::MultiAccountState;
938 use parking_lot::RwLock;
939
940 fn service() -> TranslateService {
941 TranslateService::new(Arc::new(RwLock::new(MultiAccountState::new(
942 "000000000000",
943 "us-east-1",
944 "",
945 ))))
946 }
947
948 fn req(action: &str, body: Value) -> AwsRequest {
949 AwsRequest {
950 service: "translate".to_string(),
951 action: action.to_string(),
952 region: "us-east-1".to_string(),
953 account_id: "000000000000".to_string(),
954 request_id: "test".to_string(),
955 headers: http::HeaderMap::new(),
956 query_params: std::collections::HashMap::new(),
957 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
958 body_stream: parking_lot::Mutex::new(None),
959 path_segments: Vec::new(),
960 raw_path: String::new(),
961 raw_query: String::new(),
962 method: http::Method::POST,
963 is_query_protocol: false,
964 access_key_id: None,
965 principal: None,
966 }
967 }
968
969 fn body_of(resp: AwsResponse) -> Value {
970 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
971 }
972
973 #[tokio::test]
974 async fn translate_text_echoes_passthrough() {
975 let svc = service();
976 let resp = svc
977 .handle(req(
978 "TranslateText",
979 json!({ "Text": "hello", "SourceLanguageCode": "en", "TargetLanguageCode": "fr" }),
980 ))
981 .await
982 .unwrap();
983 let v = body_of(resp);
984 assert_eq!(v["TranslatedText"], "hello");
985 assert_eq!(v["SourceLanguageCode"], "en");
986 assert_eq!(v["TargetLanguageCode"], "fr");
987 }
988
989 #[tokio::test]
990 async fn terminology_round_trip_and_tagging() {
991 let svc = service();
992 let file = base64::engine::general_purpose::STANDARD
993 .encode("en,fr\nhello,bonjour\ndog,chien\n".as_bytes());
994 svc.handle(req(
995 "ImportTerminology",
996 json!({
997 "Name": "my-term",
998 "MergeStrategy": "OVERWRITE",
999 "TerminologyData": { "File": file, "Format": "CSV" }
1000 }),
1001 ))
1002 .await
1003 .unwrap();
1004
1005 let got = body_of(
1006 svc.handle(req("GetTerminology", json!({ "Name": "my-term" })))
1007 .await
1008 .unwrap(),
1009 );
1010 let props = &got["TerminologyProperties"];
1011 assert_eq!(props["Name"], "my-term");
1012 assert_eq!(props["SourceLanguageCode"], "en");
1013 assert_eq!(props["TargetLanguageCodes"][0], "fr");
1014 assert_eq!(props["TermCount"], 2);
1015 assert!(got["TerminologyDataLocation"]["Location"].is_string());
1016
1017 let arn = "arn:aws:translate:us-east-1:000000000000:terminology/my-term";
1019 svc.handle(req(
1020 "TagResource",
1021 json!({ "ResourceArn": arn, "Tags": [{ "Key": "team", "Value": "loc" }] }),
1022 ))
1023 .await
1024 .unwrap();
1025 let tags = body_of(
1026 svc.handle(req("ListTagsForResource", json!({ "ResourceArn": arn })))
1027 .await
1028 .unwrap(),
1029 );
1030 assert_eq!(tags["Tags"][0]["Key"], "team");
1031
1032 let bad = svc
1034 .handle(req(
1035 "ListTagsForResource",
1036 json!({ "ResourceArn": "arn:aws:translate:us-east-1:000000000000:terminology/nope" }),
1037 ))
1038 .await;
1039 assert_eq!(bad.err().unwrap().code(), "ResourceNotFoundException");
1040
1041 svc.handle(req("DeleteTerminology", json!({ "Name": "my-term" })))
1043 .await
1044 .unwrap();
1045 let after = svc
1046 .handle(req("GetTerminology", json!({ "Name": "my-term" })))
1047 .await;
1048 assert_eq!(after.err().unwrap().code(), "ResourceNotFoundException");
1049 }
1050
1051 #[tokio::test]
1052 async fn batch_job_settles_to_completed() {
1053 let svc = service();
1054 let started = body_of(
1055 svc.handle(req(
1056 "StartTextTranslationJob",
1057 json!({
1058 "InputDataConfig": { "S3Uri": "s3://in/", "ContentType": "text/plain" },
1059 "OutputDataConfig": { "S3Uri": "s3://out/" },
1060 "DataAccessRoleArn": "arn:aws:iam::000000000000:role/translate",
1061 "SourceLanguageCode": "en",
1062 "TargetLanguageCodes": ["fr"],
1063 "ClientToken": "tok-1"
1064 }),
1065 ))
1066 .await
1067 .unwrap(),
1068 );
1069 let id = started["JobId"].as_str().unwrap().to_string();
1070 assert_eq!(started["JobStatus"], "SUBMITTED");
1071 assert_eq!(id.len(), 32);
1072
1073 let described = body_of(
1074 svc.handle(req("DescribeTextTranslationJob", json!({ "JobId": id })))
1075 .await
1076 .unwrap(),
1077 );
1078 assert_eq!(
1079 described["TextTranslationJobProperties"]["JobStatus"],
1080 "COMPLETED"
1081 );
1082
1083 let missing = svc
1085 .handle(req(
1086 "DescribeTextTranslationJob",
1087 json!({ "JobId": "doesnotexist" }),
1088 ))
1089 .await;
1090 assert_eq!(missing.err().unwrap().code(), "ResourceNotFoundException");
1091 }
1092
1093 #[tokio::test]
1094 async fn parallel_data_lifecycle() {
1095 let svc = service();
1096 svc.handle(req(
1097 "CreateParallelData",
1098 json!({
1099 "Name": "pd1",
1100 "ParallelDataConfig": { "S3Uri": "s3://b/pd.tsv", "Format": "TSV" },
1101 "ClientToken": "tok-2"
1102 }),
1103 ))
1104 .await
1105 .unwrap();
1106 let got = body_of(
1107 svc.handle(req("GetParallelData", json!({ "Name": "pd1" })))
1108 .await
1109 .unwrap(),
1110 );
1111 assert_eq!(got["ParallelDataProperties"]["Status"], "ACTIVE");
1112
1113 let dup = svc
1115 .handle(req(
1116 "CreateParallelData",
1117 json!({ "Name": "pd1", "ParallelDataConfig": {}, "ClientToken": "t" }),
1118 ))
1119 .await;
1120 assert_eq!(dup.err().unwrap().code(), "ConflictException");
1121
1122 svc.handle(req("DeleteParallelData", json!({ "Name": "pd1" })))
1123 .await
1124 .unwrap();
1125 let after = svc
1126 .handle(req("GetParallelData", json!({ "Name": "pd1" })))
1127 .await;
1128 assert_eq!(after.err().unwrap().code(), "ResourceNotFoundException");
1129 }
1130
1131 #[tokio::test]
1132 async fn list_languages_returns_catalogue() {
1133 let svc = service();
1134 let v = body_of(svc.handle(req("ListLanguages", json!({}))).await.unwrap());
1135 let langs = v["Languages"].as_array().unwrap();
1136 assert!(langs.iter().any(|l| l["LanguageCode"] == "en"));
1137 assert!(langs.iter().any(|l| l["LanguageCode"] == "fr"));
1138 }
1139
1140 #[tokio::test]
1141 async fn missing_required_is_validation_error() {
1142 let svc = service();
1143 let err = svc
1146 .handle(req(
1147 "TranslateText",
1148 json!({ "SourceLanguageCode": "en", "TargetLanguageCode": "fr" }),
1149 ))
1150 .await
1151 .err()
1152 .unwrap();
1153 assert_eq!(err.code(), "InvalidRequestException");
1154 }
1155}