use std::collections::BTreeSet;
use crate::remote::{pairs, pick, Record};
pub const PREFERRED_HUB: &str = "en";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Route {
Direct { src: String, trg: String },
Pivot {
src: String,
pivot: String,
trg: String,
},
}
pub fn resolve_route(records: &[Record], src: &str, trg: &str) -> Result<Route, String> {
if pick(records, "model", src, trg).is_some() {
return Ok(Route::Direct {
src: src.into(),
trg: trg.into(),
});
}
let langs: BTreeSet<String> = pairs(records)
.into_iter()
.flat_map(|(s, t)| [s, t])
.collect();
let bridges: Vec<&String> = langs
.iter()
.filter(|p| p.as_str() != src && p.as_str() != trg)
.filter(|p| {
pick(records, "model", src, p).is_some() && pick(records, "model", p, trg).is_some()
})
.collect();
let pivot = bridges
.iter()
.find(|p| p.as_str() == PREFERRED_HUB)
.or_else(|| bridges.first())
.ok_or_else(|| format!("no model or pivot route for {src}-{trg} in Remote Settings"))?;
Ok(Route::Pivot {
src: src.into(),
pivot: (*pivot).clone(),
trg: trg.into(),
})
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct Catalog {
pub bidirectional: Vec<String>,
pub source_only: Vec<String>,
pub target_only: Vec<String>,
}
pub fn catalog(records: &[Record], hub: &str) -> Catalog {
let ps = pairs(records);
let into_hub: BTreeSet<&str> = ps
.iter()
.filter(|(_, t)| t == hub)
.map(|(s, _)| s.as_str())
.collect();
let from_hub: BTreeSet<&str> = ps
.iter()
.filter(|(s, _)| s == hub)
.map(|(_, t)| t.as_str())
.collect();
let mut cat = Catalog::default();
if !from_hub.is_empty() && !into_hub.is_empty() {
cat.bidirectional.push(hub.to_string());
}
for lang in into_hub.union(&from_hub) {
if *lang == hub {
continue;
}
match (into_hub.contains(lang), from_hub.contains(lang)) {
(true, true) => cat.bidirectional.push(lang.to_string()),
(true, false) => cat.source_only.push(lang.to_string()),
(false, true) => cat.target_only.push(lang.to_string()),
(false, false) => unreachable!("lang came from a union of the two sets"),
}
}
cat.bidirectional.sort();
cat.source_only.sort();
cat.target_only.sort();
cat
}
#[cfg(test)]
mod tests {
use super::*;
fn model(src: &str, trg: &str) -> Record {
Record {
name: format!("{src}{trg}"),
file_type: "model".into(),
src: src.into(),
trg: trg.into(),
version: "3.0".into(),
architecture: None,
decompressed_hash: None,
location: String::new(),
}
}
fn en_hub() -> Vec<Record> {
vec![
model("en", "es"),
model("es", "en"),
model("en", "fr"),
model("fr", "en"),
]
}
#[test]
fn direct_pair_resolves_directly() {
let recs = en_hub();
assert_eq!(
resolve_route(&recs, "en", "es").unwrap(),
Route::Direct {
src: "en".into(),
trg: "es".into()
}
);
}
#[test]
fn non_hub_pair_pivots_through_english() {
let recs = en_hub();
assert_eq!(
resolve_route(&recs, "es", "fr").unwrap(),
Route::Pivot {
src: "es".into(),
pivot: "en".into(),
trg: "fr".into()
}
);
}
#[test]
fn direct_model_wins_over_an_available_pivot() {
let mut recs = en_hub();
recs.push(model("es", "fr"));
assert_eq!(
resolve_route(&recs, "es", "fr").unwrap(),
Route::Direct {
src: "es".into(),
trg: "fr".into()
}
);
}
#[test]
fn pivots_through_a_non_english_hub() {
let recs = vec![
model("a", "x"),
model("x", "a"),
model("b", "x"),
model("x", "b"),
];
assert_eq!(
resolve_route(&recs, "a", "b").unwrap(),
Route::Pivot {
src: "a".into(),
pivot: "x".into(),
trg: "b".into()
}
);
}
#[test]
fn prefers_english_when_several_hubs_bridge() {
let recs = vec![
model("es", "en"),
model("en", "fr"),
model("es", "zz"),
model("zz", "fr"),
];
match resolve_route(&recs, "es", "fr").unwrap() {
Route::Pivot { pivot, .. } => assert_eq!(pivot, "en"),
other => panic!("expected pivot, got {other:?}"),
}
}
#[test]
fn unreachable_pair_errors() {
let err = resolve_route(&en_hub(), "es", "zu").unwrap_err();
assert!(err.contains("es-zu"), "error names the pair: {err}");
}
#[test]
fn catalog_classifies_directions() {
let recs = vec![
model("en", "es"),
model("es", "en"),
model("en", "fr"),
model("fr", "en"),
model("en", "nn"),
model("is", "en"),
];
let cat = catalog(&recs, "en");
assert_eq!(cat.bidirectional, vec!["en", "es", "fr"]);
assert_eq!(cat.target_only, vec!["nn"]);
assert_eq!(cat.source_only, vec!["is"]);
}
#[test]
fn catalog_omits_hub_when_only_one_direction_exists() {
let cat = catalog(&[model("es", "en")], "en");
assert!(
cat.bidirectional.is_empty(),
"hub not bidirectional one-way"
);
assert_eq!(cat.source_only, vec!["es"]);
assert!(cat.target_only.is_empty());
}
}