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