1#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
2use std::{borrow::Cow, collections::HashMap, fmt::Formatter};
9
10#[derive(Clone, Debug, Hash, Eq, PartialEq)]
14pub struct Interpolation {
15 parts: Vec<(String, String)>,
19 end: String,
21}
22
23impl Interpolation {
24 const REASONABLE_INTERPOLATION_PREALLOC_BYTES: usize = 128;
25
26 pub fn new(input: impl AsRef<str>) -> Result<Self, ParseError> {
31 InterpolationCompiler::compile(input.as_ref())
32 }
33
34 fn output_string(&self) -> String {
36 String::with_capacity(
37 self.parts
38 .iter()
39 .map(|v| v.0.len() + Self::REASONABLE_INTERPOLATION_PREALLOC_BYTES)
40 .sum(),
41 )
42 }
43
44 #[must_use]
48 pub fn render(&self, args: &HashMap<Cow<str>, Cow<str>>) -> String {
49 let mut output = self.output_string();
50 for (raw, interpolation_key) in &self.parts {
51 output.push_str(raw);
52 let interpolation_value = args.get(interpolation_key.as_str());
53 output.push_str(interpolation_value.unwrap_or(&Cow::Borrowed("")));
54 }
55 output.push_str(&self.end);
56 output
57 }
58
59 pub fn try_render(&self, args: &HashMap<Cow<str>, Cow<str>>) -> Result<String, RenderError> {
64 let mut output = self.output_string();
65 for (raw, interpolation_key) in &self.parts {
66 output.push_str(raw);
67 let Some(interpolation_value) = args.get(interpolation_key.as_str()) else {
68 return Err(RenderError::UnknownVariables(
69 self.listify_unknown_args(args),
70 ));
71 };
72 output.push_str(interpolation_value);
73 }
74 output.push_str(&self.end);
75 Ok(output)
76 }
77
78 fn listify_unknown_args<T>(&self, args: &HashMap<Cow<str>, T>) -> Vec<&str> {
80 let mut output = Vec::with_capacity(args.len());
81 for (_, key) in &self.parts {
82 if !args.contains_key(key.as_str()) {
83 output.push(key.as_str());
84 }
85 }
86 output
87 }
88
89 pub fn variables_used(&self) -> impl Iterator<Item = &str> {
92 UsedVariablesIterator {
93 inner: self.parts.as_slice(),
94 current: 0,
95 }
96 }
97
98 #[must_use]
100 pub fn input_value(&self) -> String {
101 fn push_escape(s: &mut String, txt: &str) {
102 for next in txt.chars() {
103 if next == '{' || next == '\\' {
104 s.push('\\');
105 }
106 s.push(next);
107 }
108 }
109
110 let mut output = self.output_string();
111 for (text, key) in &self.parts {
112 push_escape(&mut output, text);
113 output.push('{');
114 output.push_str(key);
115 output.push('}');
116 }
117 push_escape(&mut output, &self.end);
118 output
119 }
120}
121
122struct UsedVariablesIterator<'a> {
123 inner: &'a [(String, String)],
124 current: usize,
125}
126
127impl<'a> Iterator for UsedVariablesIterator<'a> {
128 type Item = &'a str;
129
130 fn next(&mut self) -> Option<Self::Item> {
131 let next = self.inner.get(self.current)?.1.as_str();
132 self.current += 1;
133 Some(next)
134 }
135}
136
137struct InterpolationCompiler {
138 chars: Vec<char>,
139 parts: Vec<(String, String)>,
140 index: usize,
141 next: String,
142 escaped: bool,
143}
144
145impl InterpolationCompiler {
146 fn compile(input: &str) -> Result<Interpolation, ParseError> {
147 let mut compiler = Self {
148 chars: input.chars().collect(),
149 parts: Vec::new(),
150 index: 0,
151 next: String::new(),
152 escaped: false,
153 };
154
155 while let Some(character) = compiler.chars.get(compiler.index).copied() {
158 compiler.handle_char(character)?;
159 }
160
161 compiler.shrink();
162
163 Ok(Interpolation {
164 parts: compiler.parts,
165 end: compiler.next,
166 })
167 }
168
169 fn handle_char(&mut self, ch: char) -> Result<(), ParseError> {
170 if self.escaped && ch != '{' && ch != '\\' {
171 return Err(ParseError::InvalidEscape(ch, self.index));
172 } else if self.escaped {
173 self.next.push(ch);
174 self.escaped = false;
175 } else if ch == '\\' {
176 self.escaped = true;
177 } else if ch == '{' {
178 self.index += 1;
179 let mut ident = self.make_identifier()?;
180 let mut to_push = std::mem::take(&mut self.next);
181 ident.shrink_to_fit();
182 to_push.shrink_to_fit();
183 self.parts.push((to_push, ident));
184 } else {
185 self.next.push(ch);
186 }
187 self.index += 1;
188 Ok(())
189 }
190
191 #[inline]
192 const fn valid_ident_char(ch: char) -> bool {
193 matches!(ch, 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-')
194 }
195
196 fn make_identifier(&mut self) -> Result<String, ParseError> {
197 let mut identifier = String::new();
198 let start = self.index;
199 loop {
200 let identifier_part = self
201 .chars
202 .get(self.index)
203 .copied()
204 .ok_or(ParseError::UnclosedIdentifier(start))?;
205 if identifier_part == '}' {
206 break;
207 }
208 if !Self::valid_ident_char(identifier_part) {
209 return Err(ParseError::InvalidCharInIdentifier(
210 identifier_part,
211 self.index,
212 ));
213 }
214 identifier.push(identifier_part);
215 self.index += 1;
216 }
217 Ok(identifier)
218 }
219
220 fn shrink(&mut self) {
221 self.parts.shrink_to_fit();
222
223 for (a, b) in &mut self.parts {
224 a.shrink_to_fit();
225 b.shrink_to_fit();
226 }
227
228 self.next.shrink_to_fit();
229 }
230}
231
232#[derive(Debug, PartialEq, Eq)]
234pub enum ParseError {
235 UnclosedIdentifier(usize),
237 InvalidCharInIdentifier(char, usize),
239 InvalidEscape(char, usize),
241}
242
243impl std::fmt::Display for ParseError {
244 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
245 match self {
246 Self::UnclosedIdentifier(at) => {
247 write!(f, "Unclosed identifier (mismatched pair at {})", at + 1)
248 }
249 Self::InvalidCharInIdentifier(c, at) => {
250 write!(f, "Invalid character `{c:?}` in identifier at {}", at + 1)
251 }
252 Self::InvalidEscape(c, at) => {
253 write!(
254 f,
255 "`{c:?}` at position {} cannot be escaped, only `{{` and `\\` can",
256 at + 1
257 )
258 }
259 }
260 }
261}
262
263impl std::error::Error for ParseError {}
264
265#[derive(Debug, PartialEq, Eq)]
267pub enum RenderError<'a> {
268 UnknownVariables(Vec<&'a str>),
270}
271
272impl std::fmt::Display for RenderError<'_> {
273 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
274 match self {
275 Self::UnknownVariables(vars) => {
276 write!(f, "Unknown variables used: ")?;
277 for (idx, item) in vars.iter().enumerate() {
278 if idx == vars.len() - 1 {
279 write!(f, "{item}")?;
280 } else {
281 write!(f, "{item}, ")?;
282 }
283 }
284 Ok(())
285 }
286 }
287 }
288}
289
290impl std::error::Error for RenderError<'_> {}
291
292#[cfg(test)]
293mod tests {
294 #![allow(clippy::literal_string_with_formatting_args)]
295 use std::collections::HashMap;
296
297 use super::*;
298
299 fn get_example_args() -> HashMap<Cow<'static, str>, Cow<'static, str>> {
300 let mut hm = HashMap::new();
301 hm.insert(
302 Cow::Borrowed("interpolation"),
303 Cow::Borrowed("Interpolation"),
304 );
305 hm.insert(Cow::Borrowed("unused"), Cow::Borrowed("ERROR"));
306 hm
307 }
308 #[test]
309 fn basic() {
310 let interpolation =
311 Interpolation::new("This is an example string for {interpolation}!").unwrap();
312 println!("{interpolation:?}");
313 let rendered = interpolation.render(&get_example_args());
314 assert_eq!("This is an example string for Interpolation!", rendered);
315 }
316 #[test]
317 fn escapes() {
318 let initial = "This is an example string for \\{interpolation} escapes!";
319 let target = "This is an example string for {interpolation} escapes!";
320 let interpolation = Interpolation::new(initial).unwrap();
321 println!("{interpolation:?}");
322 assert_eq!(target, interpolation.render(&HashMap::new()));
323 }
324 #[test]
325 fn recursive_escapes() {
326 let initial = "This is an example string for \\\\{interpolation} recursive escapes!";
327 let target = "This is an example string for \\Interpolation recursive escapes!";
328 let interpolation = Interpolation::new(initial).unwrap();
329 println!("{interpolation:?}");
330 assert_eq!(target, interpolation.render(&get_example_args()));
331 }
332 #[test]
333 fn variables_are_right() {
334 let interpolation =
335 Interpolation::new("This is an example string for {interpolation} variable listing!")
336 .unwrap();
337 println!("{interpolation:?}");
338 assert_eq!(
339 interpolation.variables_used().collect::<Vec<&str>>(),
340 vec!["interpolation"]
341 );
342 }
343 #[test]
344 fn basic_roundtrip() {
345 let roundtrip = "This is an example string for {interpolation}!";
346 let interpolation = Interpolation::new(roundtrip).unwrap();
347 println!("{interpolation:?}");
348 assert_eq!(roundtrip, interpolation.input_value());
349 }
350 #[test]
351 fn escapes_roundtrip() {
352 let roundtrip = "This is an example string for \\{interpolation} escapes!";
353 let interpolation = Interpolation::new(roundtrip).unwrap();
354 println!("{interpolation:?}");
355 assert_eq!(roundtrip, interpolation.input_value());
356 }
357 #[test]
358 fn recursive_escapes_roundtrip() {
359 let roundtrip = "This is an example string for \\\\{interpolation} recursive escapes!";
360 let interpolation = Interpolation::new(roundtrip).unwrap();
361 println!("{interpolation:?}");
362 assert_eq!(roundtrip, interpolation.input_value());
363 }
364 #[test]
365 fn no_interpolation() {
366 let unchanged = "This is an example string for a lack of interpolation!";
367 let interpolation = Interpolation::new(unchanged).unwrap();
368 println!("{interpolation:?}");
369 assert_eq!(unchanged, interpolation.render(&HashMap::new()));
370 }
371 #[test]
372 fn error_nonexistents_found() {
373 let one_interp = "{nonexistent}";
374 let interpolation = Interpolation::new(one_interp).unwrap();
375 println!("{interpolation:?}");
376 assert_eq!(
377 Err(RenderError::UnknownVariables(vec!["nonexistent"])),
378 interpolation.try_render(&HashMap::new())
379 );
380 }
381 #[test]
382 fn error_nonexistents_found_2() {
383 let one_interp = "{nonexistent} {nonexistent2}";
384 let interpolation = Interpolation::new(one_interp).unwrap();
385 println!("{interpolation:?}");
386 assert_eq!(
387 Err(RenderError::UnknownVariables(vec![
388 "nonexistent",
389 "nonexistent2"
390 ])),
391 interpolation.try_render(&HashMap::new())
392 );
393 }
394 #[test]
395 fn error_bad_ident() {
396 let bad_template = "{a)";
397 let interpolation = Interpolation::new(bad_template);
398 assert_eq!(
399 interpolation,
400 Err(ParseError::InvalidCharInIdentifier(')', 2))
401 );
402 }
403 #[test]
404 fn error_unclosed() {
405 let bad_template = "{a";
406 let interpolation = Interpolation::new(bad_template);
407 assert_eq!(interpolation, Err(ParseError::UnclosedIdentifier(1)));
408 }
409 #[test]
410 fn error_bad_escape() {
411 let bad_template = "\\a";
412 let interpolation = Interpolation::new(bad_template);
413 assert_eq!(interpolation, Err(ParseError::InvalidEscape('a', 1)));
414 }
415}