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    fakecloud_aws::arn::Arn::regional(
42        "comprehend",
43        region,
44        account,
45        &format!("{resource_type}/{tail}"),
46    )
47    .to_string()
48}
49
50/// Split a Comprehend ARN into `(resource_type, tail)`.
51/// `arn:aws:comprehend:{region}:{account}:{type}/{tail}` -> `(type, tail)`.
52/// `tail` keeps any further `/`-separated segments (version, dataset, ...).
53pub fn parse_resource_arn(arn: &str) -> Option<(String, String)> {
54    let mut parts = arn.splitn(6, ':');
55    let tail = parts.nth(5)?;
56    let (rtype, rest) = tail.split_once('/')?;
57    if rtype.is_empty() || rest.is_empty() {
58        return None;
59    }
60    Some((rtype.to_string(), rest.to_string()))
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn arn_round_trips() {
69        let arn = resource_arn("us-east-1", "000000000000", "flywheel", "my-fw");
70        assert_eq!(
71            arn,
72            "arn:aws:comprehend:us-east-1:000000000000:flywheel/my-fw"
73        );
74        assert_eq!(
75            parse_resource_arn(&arn),
76            Some(("flywheel".to_string(), "my-fw".to_string()))
77        );
78    }
79
80    #[test]
81    fn arn_keeps_versioned_tail() {
82        let arn = "arn:aws:comprehend:us-east-1:000000000000:document-classifier/name/version/v1";
83        assert_eq!(
84            parse_resource_arn(arn),
85            Some((
86                "document-classifier".to_string(),
87                "name/version/v1".to_string()
88            ))
89        );
90    }
91
92    #[test]
93    fn parse_rejects_non_comprehend() {
94        assert_eq!(parse_resource_arn("arn:aws:s3:::bucket"), None);
95    }
96
97    #[test]
98    fn hex32_is_stable_and_shaped() {
99        let a = hex32("seed");
100        let b = hex32("seed");
101        assert_eq!(a, b);
102        assert_eq!(a.len(), 32);
103        assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
104    }
105}