use base64::Engine;
pub fn now_epoch() -> f64 {
chrono::Utc::now().timestamp_millis() as f64 / 1000.0
}
pub fn hash_str(s: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
pub fn job_id() -> String {
uuid::Uuid::new_v4().simple().to_string()
}
pub fn resource_arn(region: &str, account: &str, resource_type: &str, name: &str) -> String {
format!("arn:aws:translate:{region}:{account}:{resource_type}/{name}")
}
pub fn parse_resource_arn(arn: &str) -> Option<(String, String)> {
let mut parts = arn.splitn(6, ':');
let tail = parts.nth(5)?;
let (rtype, name) = tail.split_once('/')?;
if rtype.is_empty() || name.is_empty() {
return None;
}
Some((rtype.to_string(), name.to_string()))
}
pub fn data_location(region: &str, account: &str, kind: &str, name: &str, file: &str) -> String {
let h = hash_str(&format!("{account}/{kind}/{name}/{file}"));
format!("https://aws-translate-{region}-prod.s3.{region}.amazonaws.com/{account}/{kind}/{name}/{h:016x}/{file}")
}
#[derive(Debug, Default, Clone)]
pub struct FileFacts {
pub size_bytes: u64,
pub record_count: u64,
pub source_language: Option<String>,
pub target_languages: Vec<String>,
}
pub fn decode_blob(s: &str) -> Vec<u8> {
base64::engine::general_purpose::STANDARD
.decode(s)
.unwrap_or_else(|_| s.as_bytes().to_vec())
}
pub fn parse_file(bytes: &[u8], format: &str) -> FileFacts {
let mut facts = FileFacts {
size_bytes: bytes.len() as u64,
..Default::default()
};
let text = match std::str::from_utf8(bytes) {
Ok(t) => t,
Err(_) => return facts,
};
let delim = match format {
"TSV" => '\t',
_ => ',',
};
let mut lines = text.lines().filter(|l| !l.trim().is_empty());
if let Some(header) = lines.next() {
let cols: Vec<&str> = header.split(delim).map(str::trim).collect();
if cols.len() >= 2 {
facts.source_language = Some(cols[0].to_string());
facts.target_languages = cols[1..].iter().map(|s| s.to_string()).collect();
}
}
facts.record_count = lines.count() as u64;
facts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn arn_round_trips() {
let arn = resource_arn("us-east-1", "000000000000", "terminology", "my-term");
assert_eq!(
arn,
"arn:aws:translate:us-east-1:000000000000:terminology/my-term"
);
assert_eq!(
parse_resource_arn(&arn),
Some(("terminology".to_string(), "my-term".to_string()))
);
}
#[test]
fn parse_rejects_non_translate() {
assert_eq!(parse_resource_arn("arn:aws:s3:::bucket"), None);
}
#[test]
fn csv_terminology_parses_languages_and_count() {
let csv = "en,fr,es\nhello,bonjour,hola\ndog,chien,perro\n";
let f = parse_file(csv.as_bytes(), "CSV");
assert_eq!(f.source_language.as_deref(), Some("en"));
assert_eq!(f.target_languages, vec!["fr", "es"]);
assert_eq!(f.record_count, 2);
assert_eq!(f.size_bytes, csv.len() as u64);
}
#[test]
fn tsv_terminology_parses_tab_columns() {
let tsv = "en\tde\nhello\thallo\n";
let f = parse_file(tsv.as_bytes(), "TSV");
assert_eq!(f.source_language.as_deref(), Some("en"));
assert_eq!(f.target_languages, vec!["de"]);
assert_eq!(f.record_count, 1);
}
#[test]
fn decode_blob_falls_back_on_non_base64() {
assert_eq!(decode_blob("en,fr\n"), b"en,fr\n");
}
#[test]
fn job_id_is_32_hex() {
let id = job_id();
assert_eq!(id.len(), 32);
assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
}
}