1use std::sync::Arc;
18
19use async_trait::async_trait;
20use http::StatusCode;
21use serde_json::{json, Map, Value};
22use tokio::sync::Mutex as AsyncMutex;
23
24use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
25use fakecloud_persistence::SnapshotStore;
26
27use crate::shared::{hex32, now_epoch, parse_resource_arn, resource_arn};
28use crate::state::{ComprehendData, SharedComprehendState};
29
30pub const COMPREHEND_ACTIONS: &[&str] = &[
32 "BatchDetectDominantLanguage",
33 "BatchDetectEntities",
34 "BatchDetectKeyPhrases",
35 "BatchDetectSentiment",
36 "BatchDetectSyntax",
37 "BatchDetectTargetedSentiment",
38 "ClassifyDocument",
39 "ContainsPiiEntities",
40 "CreateDataset",
41 "CreateDocumentClassifier",
42 "CreateEndpoint",
43 "CreateEntityRecognizer",
44 "CreateFlywheel",
45 "DeleteDocumentClassifier",
46 "DeleteEndpoint",
47 "DeleteEntityRecognizer",
48 "DeleteFlywheel",
49 "DeleteResourcePolicy",
50 "DescribeDataset",
51 "DescribeDocumentClassificationJob",
52 "DescribeDocumentClassifier",
53 "DescribeDominantLanguageDetectionJob",
54 "DescribeEndpoint",
55 "DescribeEntitiesDetectionJob",
56 "DescribeEntityRecognizer",
57 "DescribeEventsDetectionJob",
58 "DescribeFlywheel",
59 "DescribeFlywheelIteration",
60 "DescribeKeyPhrasesDetectionJob",
61 "DescribePiiEntitiesDetectionJob",
62 "DescribeResourcePolicy",
63 "DescribeSentimentDetectionJob",
64 "DescribeTargetedSentimentDetectionJob",
65 "DescribeTopicsDetectionJob",
66 "DetectDominantLanguage",
67 "DetectEntities",
68 "DetectKeyPhrases",
69 "DetectPiiEntities",
70 "DetectSentiment",
71 "DetectSyntax",
72 "DetectTargetedSentiment",
73 "DetectToxicContent",
74 "ImportModel",
75 "ListDatasets",
76 "ListDocumentClassificationJobs",
77 "ListDocumentClassifierSummaries",
78 "ListDocumentClassifiers",
79 "ListDominantLanguageDetectionJobs",
80 "ListEndpoints",
81 "ListEntitiesDetectionJobs",
82 "ListEntityRecognizerSummaries",
83 "ListEntityRecognizers",
84 "ListEventsDetectionJobs",
85 "ListFlywheelIterationHistory",
86 "ListFlywheels",
87 "ListKeyPhrasesDetectionJobs",
88 "ListPiiEntitiesDetectionJobs",
89 "ListSentimentDetectionJobs",
90 "ListTagsForResource",
91 "ListTargetedSentimentDetectionJobs",
92 "ListTopicsDetectionJobs",
93 "PutResourcePolicy",
94 "StartDocumentClassificationJob",
95 "StartDominantLanguageDetectionJob",
96 "StartEntitiesDetectionJob",
97 "StartEventsDetectionJob",
98 "StartFlywheelIteration",
99 "StartKeyPhrasesDetectionJob",
100 "StartPiiEntitiesDetectionJob",
101 "StartSentimentDetectionJob",
102 "StartTargetedSentimentDetectionJob",
103 "StartTopicsDetectionJob",
104 "StopDominantLanguageDetectionJob",
105 "StopEntitiesDetectionJob",
106 "StopEventsDetectionJob",
107 "StopKeyPhrasesDetectionJob",
108 "StopPiiEntitiesDetectionJob",
109 "StopSentimentDetectionJob",
110 "StopTargetedSentimentDetectionJob",
111 "StopTrainingDocumentClassifier",
112 "StopTrainingEntityRecognizer",
113 "TagResource",
114 "UntagResource",
115 "UpdateEndpoint",
116 "UpdateFlywheel",
117];
118
119fn is_mutating_action(action: &str) -> bool {
123 const READ_PREFIXES: &[&str] = &[
124 "Detect", "Batch", "Describe", "List", "Contains", "Classify",
125 ];
126 !READ_PREFIXES.iter().any(|p| action.starts_with(p))
127}
128
129struct JobFamily {
133 key: &'static str,
135 arn_type: &'static str,
137 props: &'static str,
139 props_list: &'static str,
141 copy: &'static [&'static str],
145}
146
147const JOB_FAMILIES: &[JobFamily] = &[
148 JobFamily {
149 key: "sentiment",
150 arn_type: "sentiment-detection-job",
151 props: "SentimentDetectionJobProperties",
152 props_list: "SentimentDetectionJobPropertiesList",
153 copy: &[
154 "JobName",
155 "InputDataConfig",
156 "OutputDataConfig",
157 "LanguageCode",
158 "DataAccessRoleArn",
159 "VolumeKmsKeyId",
160 "VpcConfig",
161 ],
162 },
163 JobFamily {
164 key: "targeted-sentiment",
165 arn_type: "targeted-sentiment-detection-job",
166 props: "TargetedSentimentDetectionJobProperties",
167 props_list: "TargetedSentimentDetectionJobPropertiesList",
168 copy: &[
169 "JobName",
170 "InputDataConfig",
171 "OutputDataConfig",
172 "LanguageCode",
173 "DataAccessRoleArn",
174 "VolumeKmsKeyId",
175 "VpcConfig",
176 ],
177 },
178 JobFamily {
179 key: "key-phrases",
180 arn_type: "key-phrases-detection-job",
181 props: "KeyPhrasesDetectionJobProperties",
182 props_list: "KeyPhrasesDetectionJobPropertiesList",
183 copy: &[
184 "JobName",
185 "InputDataConfig",
186 "OutputDataConfig",
187 "LanguageCode",
188 "DataAccessRoleArn",
189 "VolumeKmsKeyId",
190 "VpcConfig",
191 ],
192 },
193 JobFamily {
194 key: "entities",
195 arn_type: "entities-detection-job",
196 props: "EntitiesDetectionJobProperties",
197 props_list: "EntitiesDetectionJobPropertiesList",
198 copy: &[
199 "JobName",
200 "EntityRecognizerArn",
201 "InputDataConfig",
202 "OutputDataConfig",
203 "LanguageCode",
204 "DataAccessRoleArn",
205 "VolumeKmsKeyId",
206 "VpcConfig",
207 "FlywheelArn",
208 ],
209 },
210 JobFamily {
211 key: "dominant-language",
212 arn_type: "dominant-language-detection-job",
213 props: "DominantLanguageDetectionJobProperties",
214 props_list: "DominantLanguageDetectionJobPropertiesList",
215 copy: &[
216 "JobName",
217 "InputDataConfig",
218 "OutputDataConfig",
219 "DataAccessRoleArn",
220 "VolumeKmsKeyId",
221 "VpcConfig",
222 ],
223 },
224 JobFamily {
225 key: "pii",
226 arn_type: "pii-entities-detection-job",
227 props: "PiiEntitiesDetectionJobProperties",
228 props_list: "PiiEntitiesDetectionJobPropertiesList",
229 copy: &[
230 "JobName",
231 "InputDataConfig",
232 "OutputDataConfig",
233 "RedactionConfig",
234 "LanguageCode",
235 "DataAccessRoleArn",
236 "Mode",
237 ],
238 },
239 JobFamily {
240 key: "events",
241 arn_type: "events-detection-job",
242 props: "EventsDetectionJobProperties",
243 props_list: "EventsDetectionJobPropertiesList",
244 copy: &[
245 "JobName",
246 "InputDataConfig",
247 "OutputDataConfig",
248 "LanguageCode",
249 "DataAccessRoleArn",
250 "TargetEventTypes",
251 ],
252 },
253 JobFamily {
254 key: "topics",
255 arn_type: "topics-detection-job",
256 props: "TopicsDetectionJobProperties",
257 props_list: "TopicsDetectionJobPropertiesList",
258 copy: &[
259 "JobName",
260 "InputDataConfig",
261 "OutputDataConfig",
262 "NumberOfTopics",
263 "DataAccessRoleArn",
264 "VolumeKmsKeyId",
265 "VpcConfig",
266 ],
267 },
268 JobFamily {
269 key: "document-classification",
270 arn_type: "document-classification-job",
271 props: "DocumentClassificationJobProperties",
272 props_list: "DocumentClassificationJobPropertiesList",
273 copy: &[
274 "JobName",
275 "DocumentClassifierArn",
276 "InputDataConfig",
277 "OutputDataConfig",
278 "DataAccessRoleArn",
279 "VolumeKmsKeyId",
280 "VpcConfig",
281 "FlywheelArn",
282 ],
283 },
284];
285
286fn family(key: &str) -> &'static JobFamily {
287 JOB_FAMILIES
288 .iter()
289 .find(|f| f.key == key)
290 .expect("known job family")
291}
292
293pub struct ComprehendService {
294 state: SharedComprehendState,
295 snapshot_store: Option<Arc<dyn SnapshotStore>>,
296 snapshot_lock: Arc<AsyncMutex<()>>,
297}
298
299impl ComprehendService {
300 pub fn new(state: SharedComprehendState) -> Self {
301 Self {
302 state,
303 snapshot_store: None,
304 snapshot_lock: Arc::new(AsyncMutex::new(())),
305 }
306 }
307
308 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
309 self.snapshot_store = Some(store);
310 self
311 }
312
313 async fn save(&self) {
314 crate::persistence::save_snapshot(
315 &self.state,
316 self.snapshot_store.clone(),
317 &self.snapshot_lock,
318 )
319 .await;
320 }
321
322 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
324 let store = self.snapshot_store.clone()?;
325 let state = self.state.clone();
326 let lock = self.snapshot_lock.clone();
327 Some(Arc::new(move || {
328 let state = state.clone();
329 let store = store.clone();
330 let lock = lock.clone();
331 Box::pin(async move {
332 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
333 })
334 }))
335 }
336
337 fn with_account_mut<R>(&self, req: &AwsRequest, f: impl FnOnce(&mut ComprehendData) -> R) -> R {
339 let mut guard = self.state.write();
340 let acct = guard.get_or_create(&req.account_id);
341 f(acct)
342 }
343}
344
345#[async_trait]
346impl AwsService for ComprehendService {
347 fn service_name(&self) -> &str {
348 "comprehend"
349 }
350
351 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
352 let action = req.action.clone();
353 if let Err(msg) = crate::validate::validate_input(&action, &req.json_body()) {
354 return Err(invalid_request(msg));
355 }
356 let result = self.dispatch(&action, &req);
357 if is_mutating_action(&action)
358 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
359 {
360 self.save().await;
361 }
362 result
363 }
364
365 fn supported_actions(&self) -> &[&str] {
366 COMPREHEND_ACTIONS
367 }
368}
369
370impl ComprehendService {
371 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
372 let body = req.json_body();
373 match action {
374 "DetectDominantLanguage" => self.detect_dominant_language(&body),
376 "DetectEntities" => self.detect_entities(&body),
377 "DetectKeyPhrases" => self.detect_key_phrases(&body),
378 "DetectSyntax" => self.detect_syntax(&body),
379 "DetectSentiment" => self.detect_sentiment(&body),
380 "DetectTargetedSentiment" => self.detect_targeted_sentiment(&body),
381 "DetectPiiEntities" => self.detect_pii_entities(&body),
382 "ContainsPiiEntities" => self.contains_pii_entities(&body),
383 "DetectToxicContent" => self.detect_toxic_content(&body),
384 "ClassifyDocument" => self.classify_document(&body),
385 "BatchDetectDominantLanguage" => self.batch_detect(&body, "Languages"),
387 "BatchDetectEntities" => self.batch_detect(&body, "Entities"),
388 "BatchDetectKeyPhrases" => self.batch_detect(&body, "KeyPhrases"),
389 "BatchDetectSyntax" => self.batch_detect(&body, "SyntaxTokens"),
390 "BatchDetectSentiment" => self.batch_detect(&body, "Sentiment"),
391 "BatchDetectTargetedSentiment" => self.batch_detect(&body, "Entities"),
392 "StartDominantLanguageDetectionJob" => self.start_job(req, &body, "dominant-language"),
394 "StartEntitiesDetectionJob" => self.start_job(req, &body, "entities"),
395 "StartKeyPhrasesDetectionJob" => self.start_job(req, &body, "key-phrases"),
396 "StartSentimentDetectionJob" => self.start_job(req, &body, "sentiment"),
397 "StartTargetedSentimentDetectionJob" => {
398 self.start_job(req, &body, "targeted-sentiment")
399 }
400 "StartPiiEntitiesDetectionJob" => self.start_job(req, &body, "pii"),
401 "StartEventsDetectionJob" => self.start_job(req, &body, "events"),
402 "StartTopicsDetectionJob" => self.start_job(req, &body, "topics"),
403 "StartDocumentClassificationJob" => {
404 self.start_job(req, &body, "document-classification")
405 }
406 "DescribeDominantLanguageDetectionJob" => {
407 self.describe_job(req, &body, "dominant-language")
408 }
409 "DescribeEntitiesDetectionJob" => self.describe_job(req, &body, "entities"),
410 "DescribeKeyPhrasesDetectionJob" => self.describe_job(req, &body, "key-phrases"),
411 "DescribeSentimentDetectionJob" => self.describe_job(req, &body, "sentiment"),
412 "DescribeTargetedSentimentDetectionJob" => {
413 self.describe_job(req, &body, "targeted-sentiment")
414 }
415 "DescribePiiEntitiesDetectionJob" => self.describe_job(req, &body, "pii"),
416 "DescribeEventsDetectionJob" => self.describe_job(req, &body, "events"),
417 "DescribeTopicsDetectionJob" => self.describe_job(req, &body, "topics"),
418 "DescribeDocumentClassificationJob" => {
419 self.describe_job(req, &body, "document-classification")
420 }
421 "ListDominantLanguageDetectionJobs" => self.list_jobs(req, &body, "dominant-language"),
422 "ListEntitiesDetectionJobs" => self.list_jobs(req, &body, "entities"),
423 "ListKeyPhrasesDetectionJobs" => self.list_jobs(req, &body, "key-phrases"),
424 "ListSentimentDetectionJobs" => self.list_jobs(req, &body, "sentiment"),
425 "ListTargetedSentimentDetectionJobs" => {
426 self.list_jobs(req, &body, "targeted-sentiment")
427 }
428 "ListPiiEntitiesDetectionJobs" => self.list_jobs(req, &body, "pii"),
429 "ListEventsDetectionJobs" => self.list_jobs(req, &body, "events"),
430 "ListTopicsDetectionJobs" => self.list_jobs(req, &body, "topics"),
431 "ListDocumentClassificationJobs" => {
432 self.list_jobs(req, &body, "document-classification")
433 }
434 "StopDominantLanguageDetectionJob" => self.stop_job(req, &body, "dominant-language"),
435 "StopEntitiesDetectionJob" => self.stop_job(req, &body, "entities"),
436 "StopKeyPhrasesDetectionJob" => self.stop_job(req, &body, "key-phrases"),
437 "StopSentimentDetectionJob" => self.stop_job(req, &body, "sentiment"),
438 "StopTargetedSentimentDetectionJob" => self.stop_job(req, &body, "targeted-sentiment"),
439 "StopPiiEntitiesDetectionJob" => self.stop_job(req, &body, "pii"),
440 "StopEventsDetectionJob" => self.stop_job(req, &body, "events"),
441 "CreateDocumentClassifier" => self.create_document_classifier(req, &body),
443 "DescribeDocumentClassifier" => self.describe_document_classifier(req, &body),
444 "DeleteDocumentClassifier" => self.delete_document_classifier(req, &body),
445 "StopTrainingDocumentClassifier" => self.stop_training_document_classifier(req, &body),
446 "ListDocumentClassifiers" => self.list_document_classifiers(req, &body),
447 "ListDocumentClassifierSummaries" => {
448 self.list_document_classifier_summaries(req, &body)
449 }
450 "CreateEntityRecognizer" => self.create_entity_recognizer(req, &body),
452 "DescribeEntityRecognizer" => self.describe_entity_recognizer(req, &body),
453 "DeleteEntityRecognizer" => self.delete_entity_recognizer(req, &body),
454 "StopTrainingEntityRecognizer" => self.stop_training_entity_recognizer(req, &body),
455 "ListEntityRecognizers" => self.list_entity_recognizers(req, &body),
456 "ListEntityRecognizerSummaries" => self.list_entity_recognizer_summaries(req, &body),
457 "CreateEndpoint" => self.create_endpoint(req, &body),
459 "DescribeEndpoint" => self.describe_endpoint(req, &body),
460 "UpdateEndpoint" => self.update_endpoint(req, &body),
461 "DeleteEndpoint" => self.delete_endpoint(req, &body),
462 "ListEndpoints" => self.list_endpoints(req, &body),
463 "CreateFlywheel" => self.create_flywheel(req, &body),
465 "DescribeFlywheel" => self.describe_flywheel(req, &body),
466 "UpdateFlywheel" => self.update_flywheel(req, &body),
467 "DeleteFlywheel" => self.delete_flywheel(req, &body),
468 "ListFlywheels" => self.list_flywheels(req, &body),
469 "StartFlywheelIteration" => self.start_flywheel_iteration(req, &body),
470 "DescribeFlywheelIteration" => self.describe_flywheel_iteration(req, &body),
471 "ListFlywheelIterationHistory" => self.list_flywheel_iteration_history(req, &body),
472 "CreateDataset" => self.create_dataset(req, &body),
474 "DescribeDataset" => self.describe_dataset(req, &body),
475 "ListDatasets" => self.list_datasets(req, &body),
476 "ImportModel" => self.import_model(req, &body),
478 "PutResourcePolicy" => self.put_resource_policy(req, &body),
479 "DescribeResourcePolicy" => self.describe_resource_policy(req, &body),
480 "DeleteResourcePolicy" => self.delete_resource_policy(req, &body),
481 "TagResource" => self.tag_resource(req, &body),
483 "UntagResource" => self.untag_resource(req, &body),
484 "ListTagsForResource" => self.list_tags_for_resource(req, &body),
485 _ => Err(AwsServiceError::action_not_implemented(
486 self.service_name(),
487 action,
488 )),
489 }
490 }
491}
492
493fn invalid_request(msg: impl Into<String>) -> AwsServiceError {
496 AwsServiceError::aws_error(
497 StatusCode::BAD_REQUEST,
498 "InvalidRequestException",
499 msg.into(),
500 )
501}
502
503fn resource_not_found(msg: impl Into<String>) -> AwsServiceError {
504 AwsServiceError::aws_error(
505 StatusCode::BAD_REQUEST,
506 "ResourceNotFoundException",
507 msg.into(),
508 )
509}
510
511fn job_not_found(msg: impl Into<String>) -> AwsServiceError {
512 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "JobNotFoundException", msg.into())
513}
514
515fn resource_in_use(msg: impl Into<String>) -> AwsServiceError {
516 AwsServiceError::aws_error(
517 StatusCode::BAD_REQUEST,
518 "ResourceInUseException",
519 msg.into(),
520 )
521}
522
523fn ok(value: Value) -> Result<AwsResponse, AwsServiceError> {
524 Ok(AwsResponse::ok_json(value))
525}
526
527fn copy_members(body: &Value, obj: &mut Map<String, Value>, names: &[&str]) {
531 for name in names {
532 if let Some(v) = body.get(*name).filter(|v| !v.is_null()) {
533 obj.insert((*name).to_string(), v.clone());
534 }
535 }
536}
537
538fn str_member<'a>(body: &'a Value, name: &str) -> Option<&'a str> {
539 body.get(name).and_then(Value::as_str)
540}
541
542fn neutral_sentiment_score() -> Value {
545 json!({ "Positive": 0.0, "Negative": 0.0, "Neutral": 1.0, "Mixed": 0.0 })
546}
547
548fn taglist_to_map(tags: &Value) -> std::collections::BTreeMap<String, String> {
550 let mut out = std::collections::BTreeMap::new();
551 if let Some(arr) = tags.as_array() {
552 for t in arr {
553 if let (Some(k), Some(v)) = (str_member(t, "Key"), str_member(t, "Value")) {
554 out.insert(k.to_string(), v.to_string());
555 }
556 }
557 }
558 out
559}
560
561fn map_to_taglist(map: &std::collections::BTreeMap<String, String>) -> Value {
563 Value::Array(
564 map.iter()
565 .map(|(k, v)| json!({ "Key": k, "Value": v }))
566 .collect(),
567 )
568}
569
570impl ComprehendData {
571 fn store_tags(&mut self, arn: &str, body: &Value) {
573 if let Some(tags) = body.get("Tags").filter(|v| !v.is_null()) {
574 let map = taglist_to_map(tags);
575 if !map.is_empty() {
576 self.tags.insert(arn.to_string(), map);
577 }
578 }
579 }
580
581 fn resource_exists(&self, arn: &str) -> bool {
583 self.document_classifiers.contains_key(arn)
584 || self.entity_recognizers.contains_key(arn)
585 || self.endpoints.contains_key(arn)
586 || self.flywheels.contains_key(arn)
587 || self.datasets.contains_key(arn)
588 }
589}
590
591impl ComprehendService {
594 fn detect_dominant_language(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
595 ok(json!({ "Languages": [] }))
597 }
598
599 fn detect_entities(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
600 ok(json!({ "Entities": [] }))
601 }
602
603 fn detect_key_phrases(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
604 ok(json!({ "KeyPhrases": [] }))
605 }
606
607 fn detect_syntax(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
608 ok(json!({ "SyntaxTokens": [] }))
609 }
610
611 fn detect_sentiment(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
612 ok(json!({
613 "Sentiment": "NEUTRAL",
614 "SentimentScore": neutral_sentiment_score(),
615 }))
616 }
617
618 fn detect_targeted_sentiment(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
619 ok(json!({ "Entities": [] }))
620 }
621
622 fn detect_pii_entities(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
623 ok(json!({ "Entities": [] }))
624 }
625
626 fn contains_pii_entities(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
627 ok(json!({ "Labels": [] }))
628 }
629
630 fn detect_toxic_content(&self, body: &Value) -> Result<AwsResponse, AwsServiceError> {
631 let n = body
633 .get("TextSegments")
634 .and_then(Value::as_array)
635 .map(|a| a.len())
636 .unwrap_or(0);
637 let results: Vec<Value> = (0..n)
638 .map(|_| json!({ "Labels": [], "Toxicity": 0.0 }))
639 .collect();
640 ok(json!({ "ResultList": results }))
641 }
642
643 fn classify_document(&self, _body: &Value) -> Result<AwsResponse, AwsServiceError> {
644 ok(json!({ "Classes": [], "Labels": [] }))
646 }
647
648 fn batch_detect(&self, body: &Value, field: &str) -> Result<AwsResponse, AwsServiceError> {
651 let n = body
652 .get("TextList")
653 .and_then(Value::as_array)
654 .map(|a| a.len())
655 .unwrap_or(0);
656 let results: Vec<Value> = (0..n)
657 .map(|i| {
658 let mut obj = Map::new();
659 obj.insert("Index".into(), json!(i as i64));
660 if field == "Sentiment" {
661 obj.insert("Sentiment".into(), json!("NEUTRAL"));
662 obj.insert("SentimentScore".into(), neutral_sentiment_score());
663 } else {
664 obj.insert(field.to_string(), json!([]));
665 }
666 Value::Object(obj)
667 })
668 .collect();
669 ok(json!({ "ResultList": results, "ErrorList": [] }))
670 }
671}
672
673impl ComprehendService {
676 fn start_job(
677 &self,
678 req: &AwsRequest,
679 body: &Value,
680 family_key: &str,
681 ) -> Result<AwsResponse, AwsServiceError> {
682 let fam = family(family_key);
683 let region = req.region.clone();
684 let account = req.account_id.clone();
685 let seed = str_member(body, "ClientRequestToken")
686 .map(|t| format!("{account}/{family_key}/{t}"))
687 .unwrap_or_default();
688 let job_id = hex32(&seed);
689 let job_arn = resource_arn(®ion, &account, fam.arn_type, &job_id);
690 let job_name = str_member(body, "JobName")
691 .map(String::from)
692 .unwrap_or_else(|| job_id.clone());
693 self.with_account_mut(req, |d| {
694 let now = now_epoch();
695 let mut obj = Map::new();
696 obj.insert("JobId".into(), json!(job_id));
697 obj.insert("JobArn".into(), json!(job_arn));
698 obj.insert("JobName".into(), json!(job_name));
699 obj.insert("JobStatus".into(), json!("SUBMITTED"));
700 obj.insert("SubmitTime".into(), json!(now));
701 copy_members(body, &mut obj, fam.copy);
702 d.jobs.insert(job_id.clone(), Value::Object(obj));
703 d.job_families.insert(job_id.clone(), fam.key.to_string());
704 d.store_tags(&job_arn, body);
705 ok(json!({
706 "JobId": job_id,
707 "JobArn": job_arn,
708 "JobStatus": "SUBMITTED",
709 }))
710 })
711 }
712
713 fn describe_job(
714 &self,
715 req: &AwsRequest,
716 body: &Value,
717 family_key: &str,
718 ) -> Result<AwsResponse, AwsServiceError> {
719 let fam = family(family_key);
720 let job_id = str_member(body, "JobId").unwrap_or_default().to_string();
721 self.with_account_mut(req, |d| {
722 d.reconcile();
723 let belongs = d.job_families.get(&job_id).map(String::as_str) == Some(fam.key);
724 let Some(job) = d.jobs.get(&job_id).filter(|_| belongs).cloned() else {
725 return Err(job_not_found(
726 "The specified job was not found. Check the job ID and try again.",
727 ));
728 };
729 ok(json!({ fam.props: job }))
730 })
731 }
732
733 fn list_jobs(
734 &self,
735 req: &AwsRequest,
736 body: &Value,
737 family_key: &str,
738 ) -> Result<AwsResponse, AwsServiceError> {
739 let fam = family(family_key);
740 let filter = body.get("Filter");
741 let name_eq = filter.and_then(|f| str_member(f, "JobName"));
742 let status_eq = filter.and_then(|f| str_member(f, "JobStatus"));
743 self.with_account_mut(req, |d| {
744 d.reconcile();
745 let list: Vec<Value> = d
746 .jobs
747 .iter()
748 .filter(|(id, _)| d.job_families.get(*id).map(String::as_str) == Some(fam.key))
749 .map(|(_, v)| v)
750 .filter(|v| {
751 name_eq
752 .map(|n| v.get("JobName").and_then(Value::as_str) == Some(n))
753 .unwrap_or(true)
754 && status_eq
755 .map(|s| v.get("JobStatus").and_then(Value::as_str) == Some(s))
756 .unwrap_or(true)
757 })
758 .cloned()
759 .collect();
760 ok(json!({ fam.props_list: list }))
761 })
762 }
763
764 fn stop_job(
765 &self,
766 req: &AwsRequest,
767 body: &Value,
768 family_key: &str,
769 ) -> Result<AwsResponse, AwsServiceError> {
770 let fam = family(family_key);
771 let job_id = str_member(body, "JobId").unwrap_or_default().to_string();
772 self.with_account_mut(req, |d| {
773 let belongs = d.job_families.get(&job_id).map(String::as_str) == Some(fam.key);
774 let Some(job) = d.jobs.get_mut(&job_id).filter(|_| belongs) else {
775 return Err(job_not_found(
776 "The specified job was not found. Check the job ID and try again.",
777 ));
778 };
779 let obj = job.as_object_mut().expect("job is an object");
780 let status = obj
781 .get("JobStatus")
782 .and_then(Value::as_str)
783 .unwrap_or("")
784 .to_string();
785 let new_status = if matches!(status.as_str(), "SUBMITTED" | "IN_PROGRESS") {
786 obj.insert("JobStatus".into(), json!("STOP_REQUESTED"));
787 "STOP_REQUESTED"
788 } else {
789 status.as_str()
790 };
791 ok(json!({ "JobId": job_id, "JobStatus": new_status }))
792 })
793 }
794}
795
796impl ComprehendService {
799 fn create_document_classifier(
800 &self,
801 req: &AwsRequest,
802 body: &Value,
803 ) -> Result<AwsResponse, AwsServiceError> {
804 let name = str_member(body, "DocumentClassifierName")
805 .unwrap_or_default()
806 .to_string();
807 let version = str_member(body, "VersionName").map(String::from);
808 let region = req.region.clone();
809 let account = req.account_id.clone();
810 let tail = match &version {
811 Some(v) => format!("{name}/version/{v}"),
812 None => name.clone(),
813 };
814 let arn = resource_arn(®ion, &account, "document-classifier", &tail);
815 self.with_account_mut(req, |d| {
816 if d.document_classifiers.contains_key(&arn) {
817 return Err(resource_in_use(
818 "Concurrent modification of resources. A resource with the same name already exists.",
819 ));
820 }
821 let now = now_epoch();
822 let mut obj = Map::new();
823 obj.insert("DocumentClassifierArn".into(), json!(arn));
824 obj.insert("Status".into(), json!("SUBMITTED"));
825 obj.insert("SubmitTime".into(), json!(now));
826 copy_members(
827 body,
828 &mut obj,
829 &[
830 "LanguageCode",
831 "InputDataConfig",
832 "OutputDataConfig",
833 "DataAccessRoleArn",
834 "VolumeKmsKeyId",
835 "VpcConfig",
836 "Mode",
837 "ModelKmsKeyId",
838 "VersionName",
839 ],
840 );
841 d.document_classifiers.insert(arn.clone(), Value::Object(obj));
842 d.store_tags(&arn, body);
843 ok(json!({ "DocumentClassifierArn": arn }))
844 })
845 }
846
847 fn describe_document_classifier(
848 &self,
849 req: &AwsRequest,
850 body: &Value,
851 ) -> Result<AwsResponse, AwsServiceError> {
852 let arn = str_member(body, "DocumentClassifierArn")
853 .unwrap_or_default()
854 .to_string();
855 self.with_account_mut(req, |d| {
856 d.reconcile();
857 let Some(v) = d.document_classifiers.get(&arn).cloned() else {
858 return Err(resource_not_found(
859 "The specified resource ARN was not found. Check the ARN and try your request again.",
860 ));
861 };
862 ok(json!({ "DocumentClassifierProperties": v }))
863 })
864 }
865
866 fn delete_document_classifier(
867 &self,
868 req: &AwsRequest,
869 body: &Value,
870 ) -> Result<AwsResponse, AwsServiceError> {
871 let arn = str_member(body, "DocumentClassifierArn")
872 .unwrap_or_default()
873 .to_string();
874 self.with_account_mut(req, |d| {
875 if d.document_classifiers.remove(&arn).is_none() {
876 return Err(resource_not_found(
877 "The specified resource ARN was not found. Check the ARN and try your request again.",
878 ));
879 }
880 d.tags.remove(&arn);
881 ok(json!({}))
882 })
883 }
884
885 fn stop_training_document_classifier(
886 &self,
887 req: &AwsRequest,
888 body: &Value,
889 ) -> Result<AwsResponse, AwsServiceError> {
890 let arn = str_member(body, "DocumentClassifierArn")
891 .unwrap_or_default()
892 .to_string();
893 self.with_account_mut(req, |d| {
894 let Some(v) = d.document_classifiers.get_mut(&arn) else {
895 return Err(resource_not_found(
896 "The specified resource ARN was not found. Check the ARN and try your request again.",
897 ));
898 };
899 stop_training(v);
900 ok(json!({}))
901 })
902 }
903
904 fn list_document_classifiers(
905 &self,
906 req: &AwsRequest,
907 body: &Value,
908 ) -> Result<AwsResponse, AwsServiceError> {
909 let filter = body.get("Filter");
910 let status_eq = filter.and_then(|f| str_member(f, "Status"));
911 let name_eq = filter.and_then(|f| str_member(f, "DocumentClassifierName"));
912 self.with_account_mut(req, |d| {
913 d.reconcile();
914 let list: Vec<Value> = d
915 .document_classifiers
916 .values()
917 .filter(|v| {
918 status_eq
919 .map(|s| v.get("Status").and_then(Value::as_str) == Some(s))
920 .unwrap_or(true)
921 && name_eq
922 .map(|n| classifier_name(v).as_deref() == Some(n))
923 .unwrap_or(true)
924 })
925 .cloned()
926 .collect();
927 ok(json!({ "DocumentClassifierPropertiesList": list }))
928 })
929 }
930
931 fn list_document_classifier_summaries(
932 &self,
933 req: &AwsRequest,
934 _body: &Value,
935 ) -> Result<AwsResponse, AwsServiceError> {
936 self.with_account_mut(req, |d| {
937 d.reconcile();
938 let summaries = summarize_versions(
939 d.document_classifiers.values(),
940 classifier_name,
941 "DocumentClassifierName",
942 );
943 ok(json!({ "DocumentClassifierSummariesList": summaries }))
944 })
945 }
946}
947
948fn classifier_name(v: &Value) -> Option<String> {
951 let arn = v.get("DocumentClassifierArn").and_then(Value::as_str)?;
952 let (_, tail) = parse_resource_arn(arn)?;
953 Some(tail.split("/version/").next().unwrap_or(&tail).to_string())
954}
955
956fn recognizer_name(v: &Value) -> Option<String> {
958 let arn = v.get("EntityRecognizerArn").and_then(Value::as_str)?;
959 let (_, tail) = parse_resource_arn(arn)?;
960 Some(tail.split("/version/").next().unwrap_or(&tail).to_string())
961}
962
963fn stop_training(v: &mut Value) {
966 if let Some(obj) = v.as_object_mut() {
967 let status = obj.get("Status").and_then(Value::as_str).unwrap_or("");
968 if matches!(status, "SUBMITTED" | "TRAINING") {
969 obj.insert("Status".into(), json!("STOP_REQUESTED"));
970 }
971 }
972}
973
974fn summarize_versions<'a>(
977 values: impl Iterator<Item = &'a Value>,
978 name_of: fn(&Value) -> Option<String>,
979 name_field: &str,
980) -> Vec<Value> {
981 use std::collections::BTreeMap;
982 let mut groups: BTreeMap<String, (i64, f64, Option<String>, Option<String>)> = BTreeMap::new();
984 for v in values {
985 let Some(name) = name_of(v) else { continue };
986 let submit = v.get("SubmitTime").and_then(Value::as_f64).unwrap_or(0.0);
987 let version = v
988 .get("VersionName")
989 .and_then(Value::as_str)
990 .map(String::from);
991 let status = v.get("Status").and_then(Value::as_str).map(String::from);
992 let entry = groups.entry(name).or_insert((0, f64::MIN, None, None));
993 entry.0 += 1;
994 if submit >= entry.1 {
995 entry.1 = submit;
996 entry.2 = version;
997 entry.3 = status;
998 }
999 }
1000 groups
1001 .into_iter()
1002 .map(|(name, (count, submit, version, status))| {
1003 let mut obj = Map::new();
1004 obj.insert(name_field.to_string(), json!(name));
1005 obj.insert("NumberOfVersions".into(), json!(count));
1006 if submit > f64::MIN {
1007 obj.insert("LatestVersionCreatedAt".into(), json!(submit));
1008 }
1009 if let Some(v) = version {
1010 obj.insert("LatestVersionName".into(), json!(v));
1011 }
1012 if let Some(s) = status {
1013 obj.insert("LatestVersionStatus".into(), json!(s));
1014 }
1015 Value::Object(obj)
1016 })
1017 .collect()
1018}
1019
1020impl ComprehendService {
1023 fn create_entity_recognizer(
1024 &self,
1025 req: &AwsRequest,
1026 body: &Value,
1027 ) -> Result<AwsResponse, AwsServiceError> {
1028 let name = str_member(body, "RecognizerName")
1029 .unwrap_or_default()
1030 .to_string();
1031 let version = str_member(body, "VersionName").map(String::from);
1032 let region = req.region.clone();
1033 let account = req.account_id.clone();
1034 let tail = match &version {
1035 Some(v) => format!("{name}/version/{v}"),
1036 None => name.clone(),
1037 };
1038 let arn = resource_arn(®ion, &account, "entity-recognizer", &tail);
1039 self.with_account_mut(req, |d| {
1040 if d.entity_recognizers.contains_key(&arn) {
1041 return Err(resource_in_use(
1042 "Concurrent modification of resources. A resource with the same name already exists.",
1043 ));
1044 }
1045 let now = now_epoch();
1046 let mut obj = Map::new();
1047 obj.insert("EntityRecognizerArn".into(), json!(arn));
1048 obj.insert("Status".into(), json!("SUBMITTED"));
1049 obj.insert("SubmitTime".into(), json!(now));
1050 copy_members(
1051 body,
1052 &mut obj,
1053 &[
1054 "LanguageCode",
1055 "InputDataConfig",
1056 "DataAccessRoleArn",
1057 "VolumeKmsKeyId",
1058 "VpcConfig",
1059 "ModelKmsKeyId",
1060 "VersionName",
1061 ],
1062 );
1063 d.entity_recognizers.insert(arn.clone(), Value::Object(obj));
1064 d.store_tags(&arn, body);
1065 ok(json!({ "EntityRecognizerArn": arn }))
1066 })
1067 }
1068
1069 fn describe_entity_recognizer(
1070 &self,
1071 req: &AwsRequest,
1072 body: &Value,
1073 ) -> Result<AwsResponse, AwsServiceError> {
1074 let arn = str_member(body, "EntityRecognizerArn")
1075 .unwrap_or_default()
1076 .to_string();
1077 self.with_account_mut(req, |d| {
1078 d.reconcile();
1079 let Some(v) = d.entity_recognizers.get(&arn).cloned() else {
1080 return Err(resource_not_found(
1081 "The specified resource ARN was not found. Check the ARN and try your request again.",
1082 ));
1083 };
1084 ok(json!({ "EntityRecognizerProperties": v }))
1085 })
1086 }
1087
1088 fn delete_entity_recognizer(
1089 &self,
1090 req: &AwsRequest,
1091 body: &Value,
1092 ) -> Result<AwsResponse, AwsServiceError> {
1093 let arn = str_member(body, "EntityRecognizerArn")
1094 .unwrap_or_default()
1095 .to_string();
1096 self.with_account_mut(req, |d| {
1097 if d.entity_recognizers.remove(&arn).is_none() {
1098 return Err(resource_not_found(
1099 "The specified resource ARN was not found. Check the ARN and try your request again.",
1100 ));
1101 }
1102 d.tags.remove(&arn);
1103 ok(json!({}))
1104 })
1105 }
1106
1107 fn stop_training_entity_recognizer(
1108 &self,
1109 req: &AwsRequest,
1110 body: &Value,
1111 ) -> Result<AwsResponse, AwsServiceError> {
1112 let arn = str_member(body, "EntityRecognizerArn")
1113 .unwrap_or_default()
1114 .to_string();
1115 self.with_account_mut(req, |d| {
1116 let Some(v) = d.entity_recognizers.get_mut(&arn) else {
1117 return Err(resource_not_found(
1118 "The specified resource ARN was not found. Check the ARN and try your request again.",
1119 ));
1120 };
1121 stop_training(v);
1122 ok(json!({}))
1123 })
1124 }
1125
1126 fn list_entity_recognizers(
1127 &self,
1128 req: &AwsRequest,
1129 body: &Value,
1130 ) -> Result<AwsResponse, AwsServiceError> {
1131 let filter = body.get("Filter");
1132 let status_eq = filter.and_then(|f| str_member(f, "Status"));
1133 let name_eq = filter.and_then(|f| str_member(f, "RecognizerName"));
1134 self.with_account_mut(req, |d| {
1135 d.reconcile();
1136 let list: Vec<Value> = d
1137 .entity_recognizers
1138 .values()
1139 .filter(|v| {
1140 status_eq
1141 .map(|s| v.get("Status").and_then(Value::as_str) == Some(s))
1142 .unwrap_or(true)
1143 && name_eq
1144 .map(|n| recognizer_name(v).as_deref() == Some(n))
1145 .unwrap_or(true)
1146 })
1147 .cloned()
1148 .collect();
1149 ok(json!({ "EntityRecognizerPropertiesList": list }))
1150 })
1151 }
1152
1153 fn list_entity_recognizer_summaries(
1154 &self,
1155 req: &AwsRequest,
1156 _body: &Value,
1157 ) -> Result<AwsResponse, AwsServiceError> {
1158 self.with_account_mut(req, |d| {
1159 d.reconcile();
1160 let summaries = summarize_versions(
1161 d.entity_recognizers.values(),
1162 recognizer_name,
1163 "RecognizerName",
1164 );
1165 ok(json!({ "EntityRecognizerSummariesList": summaries }))
1166 })
1167 }
1168}
1169
1170impl ComprehendService {
1173 fn create_endpoint(
1174 &self,
1175 req: &AwsRequest,
1176 body: &Value,
1177 ) -> Result<AwsResponse, AwsServiceError> {
1178 let name = str_member(body, "EndpointName")
1179 .unwrap_or_default()
1180 .to_string();
1181 let model_arn = str_member(body, "ModelArn").unwrap_or_default().to_string();
1182 let region = req.region.clone();
1183 let account = req.account_id.clone();
1184 let ep_type = if model_arn.contains("entity-recognizer") {
1186 "entity-recognizer-endpoint"
1187 } else {
1188 "document-classifier-endpoint"
1189 };
1190 let arn = resource_arn(®ion, &account, ep_type, &name);
1191 self.with_account_mut(req, |d| {
1192 if d.endpoints.contains_key(&arn) {
1193 return Err(resource_in_use(
1194 "Concurrent modification of resources. An endpoint with the same name already exists.",
1195 ));
1196 }
1197 let now = now_epoch();
1198 let mut obj = Map::new();
1199 obj.insert("EndpointArn".into(), json!(arn));
1200 obj.insert("Status".into(), json!("CREATING"));
1201 obj.insert("CurrentInferenceUnits".into(), json!(0));
1202 obj.insert("CreationTime".into(), json!(now));
1203 obj.insert("LastModifiedTime".into(), json!(now));
1204 copy_members(
1205 body,
1206 &mut obj,
1207 &[
1208 "ModelArn",
1209 "DesiredInferenceUnits",
1210 "DataAccessRoleArn",
1211 "FlywheelArn",
1212 ],
1213 );
1214 d.endpoints.insert(arn.clone(), Value::Object(obj));
1215 d.store_tags(&arn, body);
1216 ok(json!({ "EndpointArn": arn, "ModelArn": model_arn }))
1217 })
1218 }
1219
1220 fn describe_endpoint(
1221 &self,
1222 req: &AwsRequest,
1223 body: &Value,
1224 ) -> Result<AwsResponse, AwsServiceError> {
1225 let arn = str_member(body, "EndpointArn")
1226 .unwrap_or_default()
1227 .to_string();
1228 self.with_account_mut(req, |d| {
1229 d.reconcile();
1230 let Some(v) = d.endpoints.get(&arn).cloned() else {
1231 return Err(resource_not_found(
1232 "The specified resource ARN was not found. Check the ARN and try your request again.",
1233 ));
1234 };
1235 ok(json!({ "EndpointProperties": v }))
1236 })
1237 }
1238
1239 fn update_endpoint(
1240 &self,
1241 req: &AwsRequest,
1242 body: &Value,
1243 ) -> Result<AwsResponse, AwsServiceError> {
1244 let arn = str_member(body, "EndpointArn")
1245 .unwrap_or_default()
1246 .to_string();
1247 self.with_account_mut(req, |d| {
1248 let Some(v) = d.endpoints.get_mut(&arn) else {
1249 return Err(resource_not_found(
1250 "The specified resource ARN was not found. Check the ARN and try your request again.",
1251 ));
1252 };
1253 let obj = v.as_object_mut().expect("endpoint is an object");
1254 let now = now_epoch();
1255 if let Some(m) = body.get("DesiredModelArn").filter(|v| !v.is_null()) {
1256 obj.insert("DesiredModelArn".into(), m.clone());
1257 }
1258 if let Some(u) = body.get("DesiredInferenceUnits").filter(|v| !v.is_null()) {
1259 obj.insert("DesiredInferenceUnits".into(), u.clone());
1260 }
1261 if let Some(r) = body.get("DesiredDataAccessRoleArn").filter(|v| !v.is_null()) {
1262 obj.insert("DesiredDataAccessRoleArn".into(), r.clone());
1263 }
1264 if let Some(f) = body.get("FlywheelArn").filter(|v| !v.is_null()) {
1265 obj.insert("FlywheelArn".into(), f.clone());
1266 }
1267 obj.insert("Status".into(), json!("UPDATING"));
1268 obj.insert("LastModifiedTime".into(), json!(now));
1269 let desired = obj.get("DesiredModelArn").cloned().unwrap_or(Value::Null);
1270 ok(json!({ "DesiredModelArn": desired }))
1271 })
1272 }
1273
1274 fn delete_endpoint(
1275 &self,
1276 req: &AwsRequest,
1277 body: &Value,
1278 ) -> Result<AwsResponse, AwsServiceError> {
1279 let arn = str_member(body, "EndpointArn")
1280 .unwrap_or_default()
1281 .to_string();
1282 self.with_account_mut(req, |d| {
1283 if d.endpoints.remove(&arn).is_none() {
1284 return Err(resource_not_found(
1285 "The specified resource ARN was not found. Check the ARN and try your request again.",
1286 ));
1287 }
1288 d.tags.remove(&arn);
1289 ok(json!({}))
1290 })
1291 }
1292
1293 fn list_endpoints(
1294 &self,
1295 req: &AwsRequest,
1296 body: &Value,
1297 ) -> Result<AwsResponse, AwsServiceError> {
1298 let filter = body.get("Filter");
1299 let status_eq = filter.and_then(|f| str_member(f, "Status"));
1300 let model_eq = filter.and_then(|f| str_member(f, "ModelArn"));
1301 self.with_account_mut(req, |d| {
1302 d.reconcile();
1303 let list: Vec<Value> = d
1304 .endpoints
1305 .values()
1306 .filter(|v| {
1307 status_eq
1308 .map(|s| v.get("Status").and_then(Value::as_str) == Some(s))
1309 .unwrap_or(true)
1310 && model_eq
1311 .map(|m| v.get("ModelArn").and_then(Value::as_str) == Some(m))
1312 .unwrap_or(true)
1313 })
1314 .cloned()
1315 .collect();
1316 ok(json!({ "EndpointPropertiesList": list }))
1317 })
1318 }
1319}
1320
1321impl ComprehendService {
1324 fn create_flywheel(
1325 &self,
1326 req: &AwsRequest,
1327 body: &Value,
1328 ) -> Result<AwsResponse, AwsServiceError> {
1329 let name = str_member(body, "FlywheelName")
1330 .unwrap_or_default()
1331 .to_string();
1332 let region = req.region.clone();
1333 let account = req.account_id.clone();
1334 let arn = resource_arn(®ion, &account, "flywheel", &name);
1335 let active_model = str_member(body, "ActiveModelArn").map(String::from);
1336 self.with_account_mut(req, |d| {
1337 if d.flywheels.contains_key(&arn) {
1338 return Err(resource_in_use(
1339 "Concurrent modification of resources. A flywheel with the same name already exists.",
1340 ));
1341 }
1342 let now = now_epoch();
1343 let mut obj = Map::new();
1344 obj.insert("FlywheelArn".into(), json!(arn));
1345 obj.insert("Status".into(), json!("CREATING"));
1346 obj.insert("CreationTime".into(), json!(now));
1347 obj.insert("LastModifiedTime".into(), json!(now));
1348 copy_members(
1349 body,
1350 &mut obj,
1351 &[
1352 "ActiveModelArn",
1353 "DataAccessRoleArn",
1354 "TaskConfig",
1355 "ModelType",
1356 "DataLakeS3Uri",
1357 "DataSecurityConfig",
1358 ],
1359 );
1360 d.flywheels.insert(arn.clone(), Value::Object(obj));
1361 d.store_tags(&arn, body);
1362 ok(json!({ "FlywheelArn": arn, "ActiveModelArn": active_model }))
1363 })
1364 }
1365
1366 fn describe_flywheel(
1367 &self,
1368 req: &AwsRequest,
1369 body: &Value,
1370 ) -> Result<AwsResponse, AwsServiceError> {
1371 let arn = str_member(body, "FlywheelArn")
1372 .unwrap_or_default()
1373 .to_string();
1374 self.with_account_mut(req, |d| {
1375 d.reconcile();
1376 let Some(v) = d.flywheels.get(&arn).cloned() else {
1377 return Err(resource_not_found(
1378 "The specified resource ARN was not found. Check the ARN and try your request again.",
1379 ));
1380 };
1381 ok(json!({ "FlywheelProperties": v }))
1382 })
1383 }
1384
1385 fn update_flywheel(
1386 &self,
1387 req: &AwsRequest,
1388 body: &Value,
1389 ) -> Result<AwsResponse, AwsServiceError> {
1390 let arn = str_member(body, "FlywheelArn")
1391 .unwrap_or_default()
1392 .to_string();
1393 self.with_account_mut(req, |d| {
1394 let Some(v) = d.flywheels.get_mut(&arn) else {
1395 return Err(resource_not_found(
1396 "The specified resource ARN was not found. Check the ARN and try your request again.",
1397 ));
1398 };
1399 let obj = v.as_object_mut().expect("flywheel is an object");
1400 for m in ["ActiveModelArn", "DataAccessRoleArn", "DataSecurityConfig"] {
1401 if let Some(val) = body.get(m).filter(|v| !v.is_null()) {
1402 obj.insert(m.to_string(), val.clone());
1403 }
1404 }
1405 obj.insert("LastModifiedTime".into(), json!(now_epoch()));
1406 ok(json!({ "FlywheelProperties": Value::Object(obj.clone()) }))
1407 })
1408 }
1409
1410 fn delete_flywheel(
1411 &self,
1412 req: &AwsRequest,
1413 body: &Value,
1414 ) -> Result<AwsResponse, AwsServiceError> {
1415 let arn = str_member(body, "FlywheelArn")
1416 .unwrap_or_default()
1417 .to_string();
1418 self.with_account_mut(req, |d| {
1419 if d.flywheels.remove(&arn).is_none() {
1420 return Err(resource_not_found(
1421 "The specified resource ARN was not found. Check the ARN and try your request again.",
1422 ));
1423 }
1424 d.tags.remove(&arn);
1425 d.flywheel_iterations
1427 .retain(|k, _| !k.starts_with(&format!("{arn}#")));
1428 d.datasets
1429 .retain(|_, v| v.get("DatasetArn").and_then(Value::as_str)
1430 .map(|a| !a.starts_with(&format!("{arn}/dataset/")))
1431 .unwrap_or(true));
1432 ok(json!({}))
1433 })
1434 }
1435
1436 fn list_flywheels(
1437 &self,
1438 req: &AwsRequest,
1439 body: &Value,
1440 ) -> Result<AwsResponse, AwsServiceError> {
1441 let filter = body.get("Filter");
1442 let status_eq = filter.and_then(|f| str_member(f, "Status"));
1443 self.with_account_mut(req, |d| {
1444 d.reconcile();
1445 let list: Vec<Value> = d
1446 .flywheels
1447 .values()
1448 .filter(|v| {
1449 status_eq
1450 .map(|s| v.get("Status").and_then(Value::as_str) == Some(s))
1451 .unwrap_or(true)
1452 })
1453 .map(|v| {
1454 let mut s = Map::new();
1456 if let Some(obj) = v.as_object() {
1457 for f in [
1458 "FlywheelArn",
1459 "ActiveModelArn",
1460 "DataLakeS3Uri",
1461 "Status",
1462 "ModelType",
1463 "Message",
1464 "CreationTime",
1465 "LastModifiedTime",
1466 "LatestFlywheelIteration",
1467 ] {
1468 if let Some(val) = obj.get(f) {
1469 s.insert(f.to_string(), val.clone());
1470 }
1471 }
1472 }
1473 Value::Object(s)
1474 })
1475 .collect();
1476 ok(json!({ "FlywheelSummaryList": list }))
1477 })
1478 }
1479
1480 fn start_flywheel_iteration(
1481 &self,
1482 req: &AwsRequest,
1483 body: &Value,
1484 ) -> Result<AwsResponse, AwsServiceError> {
1485 let arn = str_member(body, "FlywheelArn")
1486 .unwrap_or_default()
1487 .to_string();
1488 let iter_id = hex32(&format!(
1489 "{}/{}",
1490 arn,
1491 str_member(body, "ClientRequestToken").unwrap_or_default()
1492 ));
1493 self.with_account_mut(req, |d| {
1494 if !d.flywheels.contains_key(&arn) {
1495 return Err(resource_not_found(
1496 "The specified resource ARN was not found. Check the ARN and try your request again.",
1497 ));
1498 }
1499 let now = now_epoch();
1500 let obj = json!({
1501 "FlywheelArn": arn,
1502 "FlywheelIterationId": iter_id,
1503 "CreationTime": now,
1504 "Status": "TRAINING",
1505 });
1506 d.flywheel_iterations
1507 .insert(format!("{arn}#{iter_id}"), obj);
1508 if let Some(fw) = d.flywheels.get_mut(&arn).and_then(Value::as_object_mut) {
1510 fw.insert("LatestFlywheelIteration".into(), json!(iter_id));
1511 }
1512 ok(json!({ "FlywheelArn": arn, "FlywheelIterationId": iter_id }))
1513 })
1514 }
1515
1516 fn describe_flywheel_iteration(
1517 &self,
1518 req: &AwsRequest,
1519 body: &Value,
1520 ) -> Result<AwsResponse, AwsServiceError> {
1521 let arn = str_member(body, "FlywheelArn")
1522 .unwrap_or_default()
1523 .to_string();
1524 let iter_id = str_member(body, "FlywheelIterationId")
1525 .unwrap_or_default()
1526 .to_string();
1527 self.with_account_mut(req, |d| {
1528 d.reconcile();
1529 let Some(v) = d.flywheel_iterations.get(&format!("{arn}#{iter_id}")).cloned() else {
1530 return Err(resource_not_found(
1531 "The specified resource ARN was not found. Check the ARN and try your request again.",
1532 ));
1533 };
1534 ok(json!({ "FlywheelIterationProperties": v }))
1535 })
1536 }
1537
1538 fn list_flywheel_iteration_history(
1539 &self,
1540 req: &AwsRequest,
1541 body: &Value,
1542 ) -> Result<AwsResponse, AwsServiceError> {
1543 let arn = str_member(body, "FlywheelArn")
1544 .unwrap_or_default()
1545 .to_string();
1546 self.with_account_mut(req, |d| {
1547 d.reconcile();
1548 if !d.flywheels.contains_key(&arn) {
1549 return Err(resource_not_found(
1550 "The specified resource ARN was not found. Check the ARN and try your request again.",
1551 ));
1552 }
1553 let prefix = format!("{arn}#");
1554 let list: Vec<Value> = d
1555 .flywheel_iterations
1556 .iter()
1557 .filter(|(k, _)| k.starts_with(&prefix))
1558 .map(|(_, v)| v.clone())
1559 .collect();
1560 ok(json!({ "FlywheelIterationPropertiesList": list }))
1561 })
1562 }
1563}
1564
1565impl ComprehendService {
1568 fn create_dataset(
1569 &self,
1570 req: &AwsRequest,
1571 body: &Value,
1572 ) -> Result<AwsResponse, AwsServiceError> {
1573 let fw_arn = str_member(body, "FlywheelArn")
1574 .unwrap_or_default()
1575 .to_string();
1576 let name = str_member(body, "DatasetName")
1577 .unwrap_or_default()
1578 .to_string();
1579 let arn = format!("{fw_arn}/dataset/{name}");
1580 self.with_account_mut(req, |d| {
1581 if !d.flywheels.contains_key(&fw_arn) {
1582 return Err(resource_not_found(
1583 "The specified resource ARN was not found. Check the ARN and try your request again.",
1584 ));
1585 }
1586 if d.datasets.contains_key(&arn) {
1587 return Err(resource_in_use(
1588 "Concurrent modification of resources. A dataset with the same name already exists.",
1589 ));
1590 }
1591 let now = now_epoch();
1592 let mut obj = Map::new();
1593 obj.insert("DatasetArn".into(), json!(arn));
1594 obj.insert("DatasetName".into(), json!(name));
1595 obj.insert("Status".into(), json!("CREATING"));
1596 obj.insert("NumberOfDocuments".into(), json!(0));
1597 obj.insert("CreationTime".into(), json!(now));
1598 copy_members(body, &mut obj, &["DatasetType", "Description"]);
1599 d.datasets.insert(arn.clone(), Value::Object(obj));
1600 d.store_tags(&arn, body);
1601 ok(json!({ "DatasetArn": arn }))
1602 })
1603 }
1604
1605 fn describe_dataset(
1606 &self,
1607 req: &AwsRequest,
1608 body: &Value,
1609 ) -> Result<AwsResponse, AwsServiceError> {
1610 let arn = str_member(body, "DatasetArn")
1611 .unwrap_or_default()
1612 .to_string();
1613 self.with_account_mut(req, |d| {
1614 d.reconcile();
1615 let Some(v) = d.datasets.get(&arn).cloned() else {
1616 return Err(resource_not_found(
1617 "The specified resource ARN was not found. Check the ARN and try your request again.",
1618 ));
1619 };
1620 ok(json!({ "DatasetProperties": v }))
1621 })
1622 }
1623
1624 fn list_datasets(
1625 &self,
1626 req: &AwsRequest,
1627 body: &Value,
1628 ) -> Result<AwsResponse, AwsServiceError> {
1629 let fw_arn = str_member(body, "FlywheelArn").map(String::from);
1630 let filter = body.get("Filter");
1631 let status_eq = filter.and_then(|f| str_member(f, "Status"));
1632 self.with_account_mut(req, |d| {
1633 d.reconcile();
1634 let list: Vec<Value> = d
1635 .datasets
1636 .iter()
1637 .filter(|(arn, _)| {
1638 fw_arn
1639 .as_deref()
1640 .map(|fw| arn.starts_with(&format!("{fw}/dataset/")))
1641 .unwrap_or(true)
1642 })
1643 .map(|(_, v)| v)
1644 .filter(|v| {
1645 status_eq
1646 .map(|s| v.get("Status").and_then(Value::as_str) == Some(s))
1647 .unwrap_or(true)
1648 })
1649 .cloned()
1650 .collect();
1651 ok(json!({ "DatasetPropertiesList": list }))
1652 })
1653 }
1654}
1655
1656impl ComprehendService {
1659 fn import_model(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1660 let source = str_member(body, "SourceModelArn")
1661 .unwrap_or_default()
1662 .to_string();
1663 let region = req.region.clone();
1664 let account = req.account_id.clone();
1665 let is_recognizer = source.contains("entity-recognizer");
1668 let rtype = if is_recognizer {
1669 "entity-recognizer"
1670 } else {
1671 "document-classifier"
1672 };
1673 let name = str_member(body, "ModelName")
1674 .map(String::from)
1675 .or_else(|| {
1676 parse_resource_arn(&source)
1677 .map(|(_, tail)| tail.split("/version/").next().unwrap_or(&tail).to_string())
1678 })
1679 .unwrap_or_else(|| "imported-model".to_string());
1680 let version = str_member(body, "VersionName").map(String::from);
1681 let tail = match &version {
1682 Some(v) => format!("{name}/version/{v}"),
1683 None => name.clone(),
1684 };
1685 let arn = resource_arn(®ion, &account, rtype, &tail);
1686 self.with_account_mut(req, |d| {
1687 let now = now_epoch();
1688 let mut obj = Map::new();
1689 obj.insert("Status".into(), json!("TRAINED"));
1690 obj.insert("SubmitTime".into(), json!(now));
1691 obj.insert("TrainingStartTime".into(), json!(now));
1692 obj.insert("TrainingEndTime".into(), json!(now));
1693 obj.insert("EndTime".into(), json!(now));
1694 obj.insert("SourceModelArn".into(), json!(source));
1695 if let Some(v) = &version {
1696 obj.insert("VersionName".into(), json!(v));
1697 }
1698 copy_members(body, &mut obj, &["ModelKmsKeyId", "DataAccessRoleArn"]);
1699 if is_recognizer {
1700 obj.insert("EntityRecognizerArn".into(), json!(arn));
1701 d.entity_recognizers.insert(arn.clone(), Value::Object(obj));
1702 } else {
1703 obj.insert("DocumentClassifierArn".into(), json!(arn));
1704 d.document_classifiers
1705 .insert(arn.clone(), Value::Object(obj));
1706 }
1707 d.store_tags(&arn, body);
1708 ok(json!({ "ModelArn": arn }))
1709 })
1710 }
1711
1712 fn put_resource_policy(
1713 &self,
1714 req: &AwsRequest,
1715 body: &Value,
1716 ) -> Result<AwsResponse, AwsServiceError> {
1717 let arn = str_member(body, "ResourceArn")
1718 .unwrap_or_default()
1719 .to_string();
1720 let policy = str_member(body, "ResourcePolicy")
1721 .unwrap_or_default()
1722 .to_string();
1723 self.with_account_mut(req, |d| {
1724 let now = now_epoch();
1725 let revision = hex32(&format!("{arn}/{}", now));
1726 let created = d
1727 .resource_policies
1728 .get(&arn)
1729 .and_then(|p| p.get("CreationTime").cloned())
1730 .unwrap_or(json!(now));
1731 d.resource_policies.insert(
1732 arn.clone(),
1733 json!({
1734 "ResourcePolicy": policy,
1735 "PolicyRevisionId": revision,
1736 "CreationTime": created,
1737 "LastModifiedTime": now,
1738 }),
1739 );
1740 ok(json!({ "PolicyRevisionId": revision }))
1741 })
1742 }
1743
1744 fn describe_resource_policy(
1745 &self,
1746 req: &AwsRequest,
1747 body: &Value,
1748 ) -> Result<AwsResponse, AwsServiceError> {
1749 let arn = str_member(body, "ResourceArn")
1750 .unwrap_or_default()
1751 .to_string();
1752 self.with_account_mut(req, |d| {
1753 let Some(p) = d.resource_policies.get(&arn).cloned() else {
1754 return Err(resource_not_found(
1755 "The specified resource ARN was not found. Check the ARN and try your request again.",
1756 ));
1757 };
1758 ok(p)
1759 })
1760 }
1761
1762 fn delete_resource_policy(
1763 &self,
1764 req: &AwsRequest,
1765 body: &Value,
1766 ) -> Result<AwsResponse, AwsServiceError> {
1767 let arn = str_member(body, "ResourceArn")
1768 .unwrap_or_default()
1769 .to_string();
1770 self.with_account_mut(req, |d| {
1771 d.resource_policies.remove(&arn);
1772 ok(json!({}))
1773 })
1774 }
1775}
1776
1777impl ComprehendService {
1780 fn tag_resource(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1781 let arn = str_member(body, "ResourceArn")
1782 .unwrap_or_default()
1783 .to_string();
1784 self.with_account_mut(req, |d| {
1785 if !d.resource_exists(&arn) {
1786 return Err(resource_not_found(format!(
1787 "The specified resource ARN was not found: {arn}"
1788 )));
1789 }
1790 let entry = d.tags.entry(arn.clone()).or_default();
1791 for (k, v) in taglist_to_map(body.get("Tags").unwrap_or(&Value::Null)) {
1792 entry.insert(k, v);
1793 }
1794 ok(json!({}))
1795 })
1796 }
1797
1798 fn untag_resource(
1799 &self,
1800 req: &AwsRequest,
1801 body: &Value,
1802 ) -> Result<AwsResponse, AwsServiceError> {
1803 let arn = str_member(body, "ResourceArn")
1804 .unwrap_or_default()
1805 .to_string();
1806 self.with_account_mut(req, |d| {
1807 if !d.resource_exists(&arn) {
1808 return Err(resource_not_found(format!(
1809 "The specified resource ARN was not found: {arn}"
1810 )));
1811 }
1812 if let Some(entry) = d.tags.get_mut(&arn) {
1813 if let Some(keys) = body.get("TagKeys").and_then(Value::as_array) {
1814 for k in keys.iter().filter_map(Value::as_str) {
1815 entry.remove(k);
1816 }
1817 }
1818 }
1819 ok(json!({}))
1820 })
1821 }
1822
1823 fn list_tags_for_resource(
1824 &self,
1825 req: &AwsRequest,
1826 body: &Value,
1827 ) -> Result<AwsResponse, AwsServiceError> {
1828 let arn = str_member(body, "ResourceArn")
1829 .unwrap_or_default()
1830 .to_string();
1831 self.with_account_mut(req, |d| {
1832 if !d.resource_exists(&arn) {
1833 return Err(resource_not_found(format!(
1834 "The specified resource ARN was not found: {arn}"
1835 )));
1836 }
1837 let tags = d.tags.get(&arn).map(map_to_taglist).unwrap_or(json!([]));
1838 ok(json!({ "ResourceArn": arn, "Tags": tags }))
1839 })
1840 }
1841}
1842
1843#[cfg(test)]
1844mod tests {
1845 use super::*;
1846 use fakecloud_core::multi_account::MultiAccountState;
1847 use parking_lot::RwLock;
1848
1849 fn service() -> ComprehendService {
1850 ComprehendService::new(Arc::new(RwLock::new(MultiAccountState::new(
1851 "000000000000",
1852 "us-east-1",
1853 "",
1854 ))))
1855 }
1856
1857 fn req(action: &str, body: Value) -> AwsRequest {
1858 AwsRequest {
1859 service: "comprehend".to_string(),
1860 action: action.to_string(),
1861 region: "us-east-1".to_string(),
1862 account_id: "000000000000".to_string(),
1863 request_id: "test".to_string(),
1864 headers: http::HeaderMap::new(),
1865 query_params: std::collections::HashMap::new(),
1866 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1867 body_stream: parking_lot::Mutex::new(None),
1868 path_segments: Vec::new(),
1869 raw_path: String::new(),
1870 raw_query: String::new(),
1871 method: http::Method::POST,
1872 is_query_protocol: false,
1873 access_key_id: None,
1874 principal: None,
1875 }
1876 }
1877
1878 async fn body_of(resp: AwsResponse) -> Value {
1879 serde_json::from_slice(resp.body.expect_bytes()).unwrap()
1880 }
1881
1882 #[tokio::test]
1883 async fn detect_sentiment_is_neutral_default() {
1884 let svc = service();
1885 let resp = svc
1886 .handle(req(
1887 "DetectSentiment",
1888 json!({ "Text": "hello", "LanguageCode": "en" }),
1889 ))
1890 .await
1891 .unwrap();
1892 let v = body_of(resp).await;
1893 assert_eq!(v["Sentiment"], "NEUTRAL");
1894 assert_eq!(v["SentimentScore"]["Neutral"], 1.0);
1895 }
1896
1897 #[tokio::test]
1898 async fn missing_required_text_is_invalid_request() {
1899 let svc = service();
1900 let err = svc
1901 .handle(req("DetectSentiment", json!({ "LanguageCode": "en" })))
1902 .await
1903 .err()
1904 .unwrap();
1905 assert_eq!(err.code(), "InvalidRequestException");
1906 }
1907
1908 #[tokio::test]
1909 async fn sentiment_job_round_trip() {
1910 let svc = service();
1911 let start = svc
1912 .handle(req(
1913 "StartSentimentDetectionJob",
1914 json!({
1915 "InputDataConfig": { "S3Uri": "s3://in/" },
1916 "OutputDataConfig": { "S3Uri": "s3://out/" },
1917 "DataAccessRoleArn": "arn:aws:iam::000000000000:role/r",
1918 "LanguageCode": "en",
1919 "JobName": "job1"
1920 }),
1921 ))
1922 .await
1923 .unwrap();
1924 let sv = body_of(start).await;
1925 let job_id = sv["JobId"].as_str().unwrap().to_string();
1926 assert_eq!(sv["JobStatus"], "SUBMITTED");
1927
1928 let desc = svc
1930 .handle(req(
1931 "DescribeSentimentDetectionJob",
1932 json!({ "JobId": job_id }),
1933 ))
1934 .await
1935 .unwrap();
1936 let dv = body_of(desc).await;
1937 assert_eq!(
1938 dv["SentimentDetectionJobProperties"]["JobStatus"],
1939 "COMPLETED"
1940 );
1941
1942 let cross = svc
1944 .handle(req(
1945 "DescribeEntitiesDetectionJob",
1946 json!({ "JobId": "00000000000000000000000000000000" }),
1947 ))
1948 .await
1949 .err()
1950 .unwrap();
1951 assert_eq!(cross.code(), "JobNotFoundException");
1952
1953 let listed = svc
1955 .handle(req("ListSentimentDetectionJobs", json!({})))
1956 .await
1957 .unwrap();
1958 let lv = body_of(listed).await;
1959 assert_eq!(
1960 lv["SentimentDetectionJobPropertiesList"]
1961 .as_array()
1962 .unwrap()
1963 .len(),
1964 1
1965 );
1966 }
1967
1968 #[tokio::test]
1969 async fn flywheel_and_tagging_round_trip() {
1970 let svc = service();
1971 let create = svc
1972 .handle(req(
1973 "CreateFlywheel",
1974 json!({
1975 "FlywheelName": "fw1",
1976 "DataAccessRoleArn": "arn:aws:iam::000000000000:role/r",
1977 "DataLakeS3Uri": "s3://lake/"
1978 }),
1979 ))
1980 .await
1981 .unwrap();
1982 let cv = body_of(create).await;
1983 let arn = cv["FlywheelArn"].as_str().unwrap().to_string();
1984
1985 let desc = svc
1987 .handle(req("DescribeFlywheel", json!({ "FlywheelArn": arn })))
1988 .await
1989 .unwrap();
1990 let dv = body_of(desc).await;
1991 assert_eq!(dv["FlywheelProperties"]["Status"], "ACTIVE");
1992
1993 svc.handle(req(
1995 "TagResource",
1996 json!({ "ResourceArn": arn, "Tags": [{ "Key": "team", "Value": "nlp" }] }),
1997 ))
1998 .await
1999 .unwrap();
2000 let tags = svc
2001 .handle(req("ListTagsForResource", json!({ "ResourceArn": arn })))
2002 .await
2003 .unwrap();
2004 let tv = body_of(tags).await;
2005 assert_eq!(tv["Tags"][0]["Key"], "team");
2006
2007 let bad = svc
2009 .handle(req(
2010 "ListTagsForResource",
2011 json!({ "ResourceArn": "arn:aws:comprehend:us-east-1:000000000000:flywheel/nope" }),
2012 ))
2013 .await
2014 .err()
2015 .unwrap();
2016 assert_eq!(bad.code(), "ResourceNotFoundException");
2017
2018 svc.handle(req("DeleteFlywheel", json!({ "FlywheelArn": arn })))
2020 .await
2021 .unwrap();
2022 let after = svc
2023 .handle(req("DescribeFlywheel", json!({ "FlywheelArn": arn })))
2024 .await
2025 .err()
2026 .unwrap();
2027 assert_eq!(after.code(), "ResourceNotFoundException");
2028 }
2029
2030 #[tokio::test]
2031 async fn document_classifier_versions_summarized() {
2032 let svc = service();
2033 for v in ["v1", "v2"] {
2034 svc.handle(req(
2035 "CreateDocumentClassifier",
2036 json!({
2037 "DocumentClassifierName": "clf",
2038 "VersionName": v,
2039 "DataAccessRoleArn": "arn:aws:iam::000000000000:role/r",
2040 "LanguageCode": "en",
2041 "InputDataConfig": { "DataFormat": "COMPREHEND_CSV", "S3Uri": "s3://in/" }
2042 }),
2043 ))
2044 .await
2045 .unwrap();
2046 }
2047 let sums = svc
2048 .handle(req("ListDocumentClassifierSummaries", json!({})))
2049 .await
2050 .unwrap();
2051 let sv = body_of(sums).await;
2052 let list = sv["DocumentClassifierSummariesList"].as_array().unwrap();
2053 assert_eq!(list.len(), 1);
2054 assert_eq!(list[0]["DocumentClassifierName"], "clf");
2055 assert_eq!(list[0]["NumberOfVersions"], 2);
2056 }
2057
2058 #[tokio::test]
2059 async fn batch_detect_indexes_each_document() {
2060 let svc = service();
2061 let resp = svc
2062 .handle(req(
2063 "BatchDetectSentiment",
2064 json!({ "TextList": ["a", "b", "c"], "LanguageCode": "en" }),
2065 ))
2066 .await
2067 .unwrap();
2068 let v = body_of(resp).await;
2069 let results = v["ResultList"].as_array().unwrap();
2070 assert_eq!(results.len(), 3);
2071 assert_eq!(results[2]["Index"], 2);
2072 assert_eq!(results[0]["Sentiment"], "NEUTRAL");
2073 }
2074}