fakecloud_translate/
shared.rs1use base64::Engine;
6
7pub fn now_epoch() -> f64 {
11 chrono::Utc::now().timestamp_millis() as f64 / 1000.0
12}
13
14pub fn hash_str(s: &str) -> u64 {
17 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
18 for b in s.as_bytes() {
19 h ^= u64::from(*b);
20 h = h.wrapping_mul(0x0000_0100_0000_01b3);
21 }
22 h
23}
24
25pub fn job_id() -> String {
30 uuid::Uuid::new_v4().simple().to_string()
31}
32
33pub fn resource_arn(region: &str, account: &str, resource_type: &str, name: &str) -> String {
39 format!("arn:aws:translate:{region}:{account}:{resource_type}/{name}")
40}
41
42pub fn parse_resource_arn(arn: &str) -> Option<(String, String)> {
45 let mut parts = arn.splitn(6, ':');
46 let tail = parts.nth(5)?;
47 let (rtype, name) = tail.split_once('/')?;
48 if rtype.is_empty() || name.is_empty() {
49 return None;
50 }
51 Some((rtype.to_string(), name.to_string()))
52}
53
54pub fn data_location(region: &str, account: &str, kind: &str, name: &str, file: &str) -> String {
58 let h = hash_str(&format!("{account}/{kind}/{name}/{file}"));
59 format!("https://aws-translate-{region}-prod.s3.{region}.amazonaws.com/{account}/{kind}/{name}/{h:016x}/{file}")
60}
61
62#[derive(Debug, Default, Clone)]
64pub struct FileFacts {
65 pub size_bytes: u64,
67 pub record_count: u64,
69 pub source_language: Option<String>,
71 pub target_languages: Vec<String>,
73}
74
75pub fn decode_blob(s: &str) -> Vec<u8> {
80 base64::engine::general_purpose::STANDARD
81 .decode(s)
82 .unwrap_or_else(|_| s.as_bytes().to_vec())
83}
84
85pub fn parse_file(bytes: &[u8], format: &str) -> FileFacts {
89 let mut facts = FileFacts {
90 size_bytes: bytes.len() as u64,
91 ..Default::default()
92 };
93 let text = match std::str::from_utf8(bytes) {
94 Ok(t) => t,
95 Err(_) => return facts,
96 };
97 let delim = match format {
98 "TSV" => '\t',
99 _ => ',',
102 };
103 let mut lines = text.lines().filter(|l| !l.trim().is_empty());
104 if let Some(header) = lines.next() {
105 let cols: Vec<&str> = header.split(delim).map(str::trim).collect();
106 if cols.len() >= 2 {
107 facts.source_language = Some(cols[0].to_string());
108 facts.target_languages = cols[1..].iter().map(|s| s.to_string()).collect();
109 }
110 }
111 facts.record_count = lines.count() as u64;
112 facts
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn arn_round_trips() {
121 let arn = resource_arn("us-east-1", "000000000000", "terminology", "my-term");
122 assert_eq!(
123 arn,
124 "arn:aws:translate:us-east-1:000000000000:terminology/my-term"
125 );
126 assert_eq!(
127 parse_resource_arn(&arn),
128 Some(("terminology".to_string(), "my-term".to_string()))
129 );
130 }
131
132 #[test]
133 fn parse_rejects_non_translate() {
134 assert_eq!(parse_resource_arn("arn:aws:s3:::bucket"), None);
135 }
136
137 #[test]
138 fn csv_terminology_parses_languages_and_count() {
139 let csv = "en,fr,es\nhello,bonjour,hola\ndog,chien,perro\n";
140 let f = parse_file(csv.as_bytes(), "CSV");
141 assert_eq!(f.source_language.as_deref(), Some("en"));
142 assert_eq!(f.target_languages, vec!["fr", "es"]);
143 assert_eq!(f.record_count, 2);
144 assert_eq!(f.size_bytes, csv.len() as u64);
145 }
146
147 #[test]
148 fn tsv_terminology_parses_tab_columns() {
149 let tsv = "en\tde\nhello\thallo\n";
150 let f = parse_file(tsv.as_bytes(), "TSV");
151 assert_eq!(f.source_language.as_deref(), Some("en"));
152 assert_eq!(f.target_languages, vec!["de"]);
153 assert_eq!(f.record_count, 1);
154 }
155
156 #[test]
157 fn decode_blob_falls_back_on_non_base64() {
158 assert_eq!(decode_blob("en,fr\n"), b"en,fr\n");
160 }
161
162 #[test]
163 fn job_id_is_32_hex() {
164 let id = job_id();
165 assert_eq!(id.len(), 32);
166 assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
167 }
168}