zenops_expand/
expand_lookup.rs1use std::fmt;
2
3pub trait ExpandLookup {
20 fn write_value<'a>(
26 &self,
27 name: &'a str,
28 f: &mut dyn fmt::Write,
29 ) -> Result<(), ExpandLookupError<'a>>;
30}
31
32impl<const SIZE: usize, T: ExpandLookup + ?Sized> ExpandLookup for [&T; SIZE] {
57 fn write_value<'a>(
58 &self,
59 name: &'a str,
60 f: &mut dyn fmt::Write,
61 ) -> Result<(), ExpandLookupError<'a>> {
62 for expander in self {
63 match expander.write_value(name, f) {
64 Ok(()) => return Ok(()),
65 Err(ExpandLookupError::Unresolved(_)) => continue,
66 Err(ExpandLookupError::WriteFmt(e)) => return Err(ExpandLookupError::WriteFmt(e)),
67 }
68 }
69 Err(ExpandLookupError::Unresolved(name))
70 }
71}
72
73#[cfg(feature = "indexmap")]
76impl<K, V> ExpandLookup for indexmap::IndexMap<K, V>
77where
78 K: std::borrow::Borrow<str>,
79 V: AsRef<str>,
80{
81 fn write_value<'a>(
82 &self,
83 name: &'a str,
84 f: &mut dyn fmt::Write,
85 ) -> Result<(), ExpandLookupError<'a>> {
86 if let Some(value) = self.get(name) {
87 f.write_str(value.as_ref())?;
88 Ok(())
89 } else {
90 Err(ExpandLookupError::Unresolved(name))
91 }
92 }
93}
94
95impl<K, V> ExpandLookup for std::collections::BTreeMap<K, V>
97where
98 K: Ord + std::borrow::Borrow<str>,
99 V: AsRef<str>,
100{
101 fn write_value<'a>(
102 &self,
103 name: &'a str,
104 f: &mut dyn fmt::Write,
105 ) -> Result<(), ExpandLookupError<'a>> {
106 if let Some(value) = self.get(name) {
107 f.write_str(value.as_ref())?;
108 Ok(())
109 } else {
110 Err(ExpandLookupError::Unresolved(name))
111 }
112 }
113}
114
115impl<K, V> ExpandLookup for std::collections::HashMap<K, V>
117where
118 K: Eq + std::hash::Hash + std::borrow::Borrow<str>,
119 V: AsRef<str>,
120{
121 fn write_value<'a>(
122 &self,
123 name: &'a str,
124 f: &mut dyn fmt::Write,
125 ) -> Result<(), ExpandLookupError<'a>> {
126 if let Some(value) = self.get(name) {
127 f.write_str(value.as_ref())?;
128 Ok(())
129 } else {
130 Err(ExpandLookupError::Unresolved(name))
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
143pub enum ExpandLookupError<'a> {
144 #[error("Failed to resolve `${{{0}}}`")]
146 Unresolved(&'a str),
147 #[error(transparent)]
149 WriteFmt(#[from] fmt::Error),
150}
151
152impl<'a> From<ExpandLookupError<'a>> for crate::ExpandError {
153 fn from(value: ExpandLookupError<'a>) -> Self {
154 match value {
155 ExpandLookupError::Unresolved(name) => crate::ExpandError::Unresolved(name.into()),
156 ExpandLookupError::WriteFmt(e) => crate::ExpandError::WriteFmt(e),
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 fn write_to_string<L: ExpandLookup>(lookup: &L, key: &str) -> Result<String, String> {
166 let mut buf = String::new();
167 lookup
168 .write_value(key, &mut buf)
169 .map(|()| buf)
170 .map_err(|e| e.to_string())
171 }
172
173 #[test]
174 fn btreemap_writes_value_for_known_key() {
175 let mut m = std::collections::BTreeMap::new();
176 m.insert("name", "alice");
177 assert_eq!(write_to_string(&m, "name").unwrap(), "alice");
178 }
179
180 #[test]
181 fn btreemap_returns_unresolved_for_missing_key() {
182 let m: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
183 let mut buf = String::new();
184 let err = m.write_value("missing", &mut buf).unwrap_err();
185 assert_eq!(err, ExpandLookupError::Unresolved("missing"));
186 assert!(buf.is_empty(), "lookup must not write on miss");
187 }
188
189 #[test]
190 fn hashmap_writes_value_for_known_key() {
191 let mut m = std::collections::HashMap::new();
192 m.insert("name", "alice");
193 assert_eq!(write_to_string(&m, "name").unwrap(), "alice");
194 }
195
196 #[test]
197 fn hashmap_returns_unresolved_for_missing_key() {
198 let m: std::collections::HashMap<&str, &str> = std::collections::HashMap::new();
199 let mut buf = String::new();
200 let err = m.write_value("missing", &mut buf).unwrap_err();
201 assert_eq!(err, ExpandLookupError::Unresolved("missing"));
202 assert!(buf.is_empty());
203 }
204
205 #[cfg(feature = "indexmap")]
206 #[test]
207 fn indexmap_writes_value_for_known_key() {
208 let mut m = indexmap::IndexMap::new();
209 m.insert("name", "alice");
210 assert_eq!(write_to_string(&m, "name").unwrap(), "alice");
211 }
212
213 #[cfg(feature = "indexmap")]
214 #[test]
215 fn indexmap_returns_unresolved_for_missing_key() {
216 let m: indexmap::IndexMap<&str, &str> = indexmap::IndexMap::new();
217 let mut buf = String::new();
218 let err = m.write_value("missing", &mut buf).unwrap_err();
219 assert_eq!(err, ExpandLookupError::Unresolved("missing"));
220 assert!(buf.is_empty());
221 }
222
223 #[test]
224 fn array_chain_returns_first_hit() {
225 let mut a = std::collections::BTreeMap::new();
226 a.insert("k", "from_a");
227 let mut b = std::collections::BTreeMap::new();
228 b.insert("k", "from_b");
229
230 let chain: [&dyn ExpandLookup; 2] = [&a, &b];
231 let mut buf = String::new();
232 chain.write_value("k", &mut buf).unwrap();
233 assert_eq!(buf, "from_a");
234 }
235
236 #[test]
237 fn array_chain_falls_through_to_second_lookup() {
238 let a: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
239 let mut b = std::collections::BTreeMap::new();
240 b.insert("k", "from_b");
241
242 let chain: [&dyn ExpandLookup; 2] = [&a, &b];
243 let mut buf = String::new();
244 chain.write_value("k", &mut buf).unwrap();
245 assert_eq!(buf, "from_b");
246 }
247
248 #[test]
249 fn array_chain_returns_unresolved_when_no_lookup_has_key() {
250 let a: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
251 let b: std::collections::BTreeMap<&str, &str> = std::collections::BTreeMap::new();
252
253 let chain: [&dyn ExpandLookup; 2] = [&a, &b];
254 let mut buf = String::new();
255 let err = chain.write_value("missing", &mut buf).unwrap_err();
256 assert_eq!(err, ExpandLookupError::Unresolved("missing"));
257 }
258}