Skip to main content

fakecloud_comprehend/
shared.rs

1//! Primitives shared across the Amazon Comprehend handlers: ARN synthesis,
2//! deterministic id derivation, and timestamps. Kept in one place so the
3//! create / describe paths cannot diverge on wire format.
4
5/// Current time as awsJson1_1 epoch-seconds (a floating-point number). The
6/// Comprehend `Timestamp` shape carries no `@timestampFormat`, so awsJson1_1's
7/// default epoch-seconds applies.
8pub fn now_epoch() -> f64 {
9    chrono::Utc::now().timestamp_millis() as f64 / 1000.0
10}
11
12/// FNV-1a hash for deterministic synthesis of ids from a seed so a given
13/// resource's derived value is stable across reads and restarts.
14pub fn hash_str(s: &str) -> u64 {
15    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
16    for b in s.as_bytes() {
17        h ^= u64::from(*b);
18        h = h.wrapping_mul(0x0000_0100_0000_01b3);
19    }
20    h
21}
22
23/// A stable, AWS-shaped 32-hex-character id (as used for job ids) derived from a
24/// seed, or a fresh random one when `seed` is empty.
25pub fn hex32(seed: &str) -> String {
26    if seed.is_empty() {
27        return uuid::Uuid::new_v4().simple().to_string();
28    }
29    let a = hash_str(seed);
30    let b = hash_str(&format!("{seed}/salt"));
31    format!("{a:016x}{b:016x}")
32}
33
34/// Amazon Comprehend resource ARN,
35/// `arn:aws:comprehend:{region}:{account}:{resource_type}/{tail}`.
36///
37/// `resource_type` is one of the type names the live service uses
38/// (`document-classifier`, `entity-recognizer`, `document-classifier-endpoint`,
39/// `entity-recognizer-endpoint`, `flywheel`, `sentiment-detection-job`, ...).
40pub fn resource_arn(region: &str, account: &str, resource_type: &str, tail: &str) -> String {
41    format!("arn:aws:comprehend:{region}:{account}:{resource_type}/{tail}")
42}
43
44/// Split a Comprehend ARN into `(resource_type, tail)`.
45/// `arn:aws:comprehend:{region}:{account}:{type}/{tail}` -> `(type, tail)`.
46/// `tail` keeps any further `/`-separated segments (version, dataset, ...).
47pub fn parse_resource_arn(arn: &str) -> Option<(String, String)> {
48    let mut parts = arn.splitn(6, ':');
49    let tail = parts.nth(5)?;
50    let (rtype, rest) = tail.split_once('/')?;
51    if rtype.is_empty() || rest.is_empty() {
52        return None;
53    }
54    Some((rtype.to_string(), rest.to_string()))
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn arn_round_trips() {
63        let arn = resource_arn("us-east-1", "000000000000", "flywheel", "my-fw");
64        assert_eq!(
65            arn,
66            "arn:aws:comprehend:us-east-1:000000000000:flywheel/my-fw"
67        );
68        assert_eq!(
69            parse_resource_arn(&arn),
70            Some(("flywheel".to_string(), "my-fw".to_string()))
71        );
72    }
73
74    #[test]
75    fn arn_keeps_versioned_tail() {
76        let arn = "arn:aws:comprehend:us-east-1:000000000000:document-classifier/name/version/v1";
77        assert_eq!(
78            parse_resource_arn(arn),
79            Some((
80                "document-classifier".to_string(),
81                "name/version/v1".to_string()
82            ))
83        );
84    }
85
86    #[test]
87    fn parse_rejects_non_comprehend() {
88        assert_eq!(parse_resource_arn("arn:aws:s3:::bucket"), None);
89    }
90
91    #[test]
92    fn hex32_is_stable_and_shaped() {
93        let a = hex32("seed");
94        let b = hex32("seed");
95        assert_eq!(a, b);
96        assert_eq!(a.len(), 32);
97        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
98    }
99}