use std::fmt;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Ambiguity<T> {
pub input: String,
pub matches: Vec<T>,
}
impl<T: fmt::Display> fmt::Display for Ambiguity<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "'{}' is ambiguous; matched ", self.input)?;
for (index, candidate) in self.matches.iter().enumerate() {
if index > 0 {
formatter.write_str(", ")?;
}
write!(formatter, "{candidate}")?;
}
Ok(())
}
}
impl<T: fmt::Debug + fmt::Display> std::error::Error for Ambiguity<T> {}
pub fn resolve_unique<T, I, F>(
input: &str,
candidates: I,
key_of: F,
) -> Result<Option<T>, Ambiguity<T>>
where
I: IntoIterator<Item = T>,
F: Fn(&T) -> &str,
{
let mut hits = candidates
.into_iter()
.filter(|candidate| key_of(candidate) == input);
let Some(first) = hits.next() else {
return Ok(None);
};
let Some(second) = hits.next() else {
return Ok(Some(first));
};
let mut matches = vec![first, second];
matches.extend(hits);
Err(Ambiguity {
input: input.to_owned(),
matches,
})
}
#[cfg(test)]
mod tests {
use super::{Ambiguity, resolve_unique};
fn tail<'a>(candidate: &'a &str) -> &'a str {
candidate.rsplit('/').next().unwrap_or(candidate)
}
#[test]
fn unique_match_resolves() {
let candidates = ["service/user", "job/report"];
assert_eq!(
resolve_unique("user", candidates, tail),
Ok(Some("service/user"))
);
}
#[test]
fn no_match_is_none() {
let candidates = ["service/user", "job/report"];
assert_eq!(resolve_unique("ghost", candidates, tail), Ok(None));
}
#[test]
fn several_matches_are_ambiguous_in_order() {
let candidates = ["service/report", "job/report"];
let error = resolve_unique("report", candidates, tail).unwrap_err();
assert_eq!(
error,
Ambiguity {
input: "report".to_owned(),
matches: vec!["service/report", "job/report"],
}
);
}
#[test]
fn ambiguity_renders_the_candidates() {
let candidates = ["service/report", "job/report"];
let error = resolve_unique("report", candidates, tail).unwrap_err();
assert_eq!(
error.to_string(),
"'report' is ambiguous; matched service/report, job/report"
);
}
#[test]
fn ambiguity_is_a_standard_error() {
fn wrap(error: impl std::error::Error) -> String {
error.to_string()
}
let candidates = ["service/report", "job/report"];
let error = resolve_unique("report", candidates, tail).unwrap_err();
assert_eq!(
wrap(error),
"'report' is ambiguous; matched service/report, job/report"
);
}
#[test]
fn comparison_is_case_sensitive() {
let candidates = ["service/User"];
assert_eq!(resolve_unique("user", candidates, tail), Ok(None));
}
}