rskit_util/strings/
resolve.rs1use std::fmt;
13
14#[derive(Debug, Clone, Eq, PartialEq)]
20pub struct Ambiguity<T> {
21 pub input: String,
23 pub matches: Vec<T>,
25}
26
27impl<T: fmt::Display> fmt::Display for Ambiguity<T> {
28 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(formatter, "'{}' is ambiguous; matched ", self.input)?;
30 for (index, candidate) in self.matches.iter().enumerate() {
31 if index > 0 {
32 formatter.write_str(", ")?;
33 }
34 write!(formatter, "{candidate}")?;
35 }
36 Ok(())
37 }
38}
39
40impl<T: fmt::Debug + fmt::Display> std::error::Error for Ambiguity<T> {}
41
42pub fn resolve_unique<T, I, F>(
69 input: &str,
70 candidates: I,
71 key_of: F,
72) -> Result<Option<T>, Ambiguity<T>>
73where
74 I: IntoIterator<Item = T>,
75 F: Fn(&T) -> &str,
76{
77 let mut hits = candidates
78 .into_iter()
79 .filter(|candidate| key_of(candidate) == input);
80 let Some(first) = hits.next() else {
81 return Ok(None);
82 };
83 let Some(second) = hits.next() else {
84 return Ok(Some(first));
85 };
86 let mut matches = vec![first, second];
87 matches.extend(hits);
88 Err(Ambiguity {
89 input: input.to_owned(),
90 matches,
91 })
92}
93
94#[cfg(test)]
95mod tests {
96 use super::{Ambiguity, resolve_unique};
97
98 fn tail<'a>(candidate: &'a &str) -> &'a str {
99 candidate.rsplit('/').next().unwrap_or(candidate)
100 }
101
102 #[test]
103 fn unique_match_resolves() {
104 let candidates = ["service/user", "job/report"];
105 assert_eq!(
106 resolve_unique("user", candidates, tail),
107 Ok(Some("service/user"))
108 );
109 }
110
111 #[test]
112 fn no_match_is_none() {
113 let candidates = ["service/user", "job/report"];
114 assert_eq!(resolve_unique("ghost", candidates, tail), Ok(None));
115 }
116
117 #[test]
118 fn several_matches_are_ambiguous_in_order() {
119 let candidates = ["service/report", "job/report"];
120 let error = resolve_unique("report", candidates, tail).unwrap_err();
121 assert_eq!(
122 error,
123 Ambiguity {
124 input: "report".to_owned(),
125 matches: vec!["service/report", "job/report"],
126 }
127 );
128 }
129
130 #[test]
131 fn ambiguity_renders_the_candidates() {
132 let candidates = ["service/report", "job/report"];
133 let error = resolve_unique("report", candidates, tail).unwrap_err();
134 assert_eq!(
135 error.to_string(),
136 "'report' is ambiguous; matched service/report, job/report"
137 );
138 }
139
140 #[test]
141 fn ambiguity_is_a_standard_error() {
142 fn wrap(error: impl std::error::Error) -> String {
143 error.to_string()
144 }
145 let candidates = ["service/report", "job/report"];
146 let error = resolve_unique("report", candidates, tail).unwrap_err();
147 assert_eq!(
148 wrap(error),
149 "'report' is ambiguous; matched service/report, job/report"
150 );
151 }
152
153 #[test]
154 fn comparison_is_case_sensitive() {
155 let candidates = ["service/User"];
156 assert_eq!(resolve_unique("user", candidates, tail), Ok(None));
157 }
158}