only_engine/
interpolate.rs1use std::collections::HashMap;
2
3use only_semantic::InterpolationAst;
4
5use crate::{EngineError, PlanParam};
6
7pub(crate) fn interpolate_with_parts(
9 command: &str,
10 interpolations: &[InterpolationAst],
11 params: &[PlanParam],
12) -> Result<String, EngineError> {
13 let parameter_map = parameter_map(params);
14 let mut output = String::with_capacity(command.len());
15 let mut cursor = 0usize;
16
17 for interpolation in interpolations {
18 let start = usize::from(interpolation.range.start());
19 let end = usize::from(interpolation.range.end());
20 if start < cursor
21 || start >= end
22 || end > command.len()
23 || !command.is_char_boundary(start)
24 || !command.is_char_boundary(end)
25 {
26 return Err(EngineError::Interpolation(
27 "invalid interpolation range".to_string(),
28 ));
29 }
30
31 push_literal(&mut output, &command[cursor..start]);
32 let Some(value) = parameter_map.get(interpolation.name.as_str()) else {
33 return Err(EngineError::Interpolation(format!(
34 "variable '{{{{{}}}}}' is not defined",
35 interpolation.name
36 )));
37 };
38 output.push_str(value);
39 cursor = end;
40 }
41
42 push_literal(&mut output, &command[cursor..]);
43 Ok(output)
44}
45
46pub fn interpolate(command: &str, params: &[PlanParam]) -> Result<String, EngineError> {
57 let parameter_map = parameter_map(params);
58 let mut output = String::with_capacity(command.len());
59 let mut rest = command;
60
61 while let Some(start) = rest.find("{{") {
62 push_literal(&mut output, &rest[..start]);
63
64 if marker_is_escaped(rest, start) {
65 output.pop();
66 output.push_str("{{");
67 rest = &rest[start + 2..];
68 continue;
69 }
70
71 let placeholder = &rest[start + 2..];
72 let Some(end) = placeholder.find("}}") else {
73 return Err(EngineError::Interpolation(
74 "missing `}}` in command".to_string(),
75 ));
76 };
77
78 let name = placeholder[..end].trim();
79 let Some(value) = parameter_map.get(name) else {
80 return Err(EngineError::Interpolation(format!(
81 "variable '{{{{{name}}}}}' is not defined"
82 )));
83 };
84
85 output.push_str(value);
86 rest = &placeholder[end + 2..];
87 }
88
89 push_literal(&mut output, rest);
90 Ok(output)
91}
92
93fn push_literal(output: &mut String, segment: &str) {
94 let mut offset = 0usize;
95
96 while offset < segment.len() {
97 let Some((rel, marker)) = ["{{", "}}"]
98 .iter()
99 .filter_map(|marker| segment[offset..].find(marker).map(|index| (index, *marker)))
100 .min_by_key(|(index, _)| *index)
101 else {
102 output.push_str(&segment[offset..]);
103 break;
104 };
105
106 let marker_start = offset + rel;
107 output.push_str(&segment[offset..marker_start]);
108 if marker_is_escaped(segment, marker_start) {
109 output.pop();
110 }
111 output.push_str(marker);
112 offset = marker_start + marker.len();
113 }
114}
115
116fn parameter_map(params: &[PlanParam]) -> HashMap<&str, &str> {
117 params
118 .iter()
119 .filter_map(|param| {
120 param
121 .value
122 .as_ref()
123 .or(param.default_value.as_ref())
124 .map(|value| (param.name.as_str(), value.as_str()))
125 })
126 .collect()
127}
128
129fn marker_is_escaped(text: &str, marker_start: usize) -> bool {
130 let mut slash_count = 0usize;
131 let bytes = text.as_bytes();
132 let mut index = marker_start;
133
134 while index > 0 && bytes[index - 1] == b'\\' {
135 slash_count += 1;
136 index -= 1;
137 }
138
139 slash_count % 2 == 1
140}