only_engine/
interpolate.rs1use std::collections::HashMap;
2
3use crate::{EngineError, PlanParam};
4
5pub fn interpolate(command: &str, params: &[PlanParam]) -> Result<String, EngineError> {
16 let parameter_map = params
17 .iter()
18 .filter_map(|param| {
19 param
20 .value
21 .as_ref()
22 .or(param.default_value.as_ref())
23 .map(|value| (param.name.as_str(), value.as_str()))
24 })
25 .collect::<HashMap<_, _>>();
26 let mut output = String::with_capacity(command.len());
27 let mut rest = command;
28
29 while let Some(start) = rest.find("{{") {
30 push_literal(&mut output, &rest[..start]);
31
32 if marker_is_escaped(rest, start) {
33 output.pop();
34 output.push_str("{{");
35 rest = &rest[start + 2..];
36 continue;
37 }
38
39 let placeholder = &rest[start + 2..];
40 let Some(end) = placeholder.find("}}") else {
41 return Err(EngineError::Interpolation(
42 "missing `}}` in command".to_string(),
43 ));
44 };
45
46 let name = placeholder[..end].trim();
47 let Some(value) = parameter_map.get(name) else {
48 return Err(EngineError::Interpolation(format!(
49 "variable '{{{{{name}}}}}' is not defined"
50 )));
51 };
52
53 output.push_str(value);
54 rest = &placeholder[end + 2..];
55 }
56
57 push_literal(&mut output, rest);
58 Ok(output)
59}
60
61fn push_literal(output: &mut String, segment: &str) {
62 let mut offset = 0usize;
63
64 while let Some(rel) = segment[offset..].find("}}") {
65 let close = offset + rel;
66 output.push_str(&segment[offset..close]);
67 if marker_is_escaped(segment, close) {
68 output.pop();
69 }
70 output.push_str("}}");
71 offset = close + 2;
72 }
73
74 output.push_str(&segment[offset..]);
75}
76
77fn marker_is_escaped(text: &str, marker_start: usize) -> bool {
78 let mut slash_count = 0usize;
79 let bytes = text.as_bytes();
80 let mut index = marker_start;
81
82 while index > 0 && bytes[index - 1] == b'\\' {
83 slash_count += 1;
84 index -= 1;
85 }
86
87 slash_count % 2 == 1
88}