Skip to main content

only_engine/
interpolate.rs

1use std::collections::HashMap;
2
3use only_semantic::InterpolationAst;
4
5use crate::{EngineError, PlanParam};
6
7/// Renders a command using interpolation ranges produced by semantic analysis.
8pub(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
46/// Renders one command string by replacing semantic interpolation placeholders.
47///
48/// Supports `\{\{` and `\}\}` escape sequences to produce literal `{{` and `}}`.
49///
50/// Args:
51/// command: Raw command text from the execution plan.
52/// params: Bound plan parameters available to interpolation.
53///
54/// Returns:
55/// Rendered command text or an engine error when interpolation is invalid.
56pub 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}