agent_plugins/
template.rs1use std::fmt;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Anchors {
15 pub plugin_root: PathBuf,
17 pub plugin_data: PathBuf,
20}
21
22impl Anchors {
23 pub fn new(plugin_root: impl Into<PathBuf>, plugin_data: impl Into<PathBuf>) -> Self {
25 Self { plugin_root: plugin_root.into(), plugin_data: plugin_data.into() }
26 }
27
28 fn path_of(&self, placeholder: Placeholder) -> &Path {
29 match placeholder {
30 Placeholder::PluginRoot => &self.plugin_root,
31 Placeholder::PluginData => &self.plugin_data,
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum Placeholder {
39 PluginRoot,
41 PluginData,
43}
44
45impl Placeholder {
46 pub fn spelling(self) -> &'static str {
48 match self {
49 Self::PluginRoot => "${PLUGIN_ROOT}",
50 Self::PluginData => "${PLUGIN_DATA}",
51 }
52 }
53}
54
55impl fmt::Display for Placeholder {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(self.spelling())
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum Segment {
64 Literal(String),
67 Anchor(Placeholder),
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct Template {
78 raw: String,
79 segments: Vec<Segment>,
80}
81
82impl Template {
83 pub fn parse(raw: &str) -> Self {
85 let mut segments = Vec::new();
86 let mut literal = String::new();
87 let mut rest = raw;
88 while let Some(dollar) = rest.find("${") {
89 let (before, from_dollar) = rest.split_at(dollar);
90 literal.push_str(before);
91 let placeholder = [Placeholder::PluginRoot, Placeholder::PluginData]
92 .into_iter()
93 .find(|p| from_dollar.starts_with(p.spelling()));
94 match placeholder {
95 Some(p) => {
96 if !literal.is_empty() {
97 segments.push(Segment::Literal(std::mem::take(&mut literal)));
98 }
99 segments.push(Segment::Anchor(p));
100 rest = &from_dollar[p.spelling().len()..];
101 }
102 None => {
103 literal.push_str("${");
105 rest = &from_dollar[2..];
106 }
107 }
108 }
109 literal.push_str(rest);
110 if !literal.is_empty() || segments.is_empty() {
111 segments.push(Segment::Literal(literal));
112 }
113 Self { raw: raw.to_owned(), segments }
114 }
115
116 pub fn as_raw(&self) -> &str {
118 &self.raw
119 }
120
121 pub fn segments(&self) -> &[Segment] {
123 &self.segments
124 }
125
126 pub fn has_anchors(&self) -> bool {
128 self.segments.iter().any(|s| matches!(s, Segment::Anchor(_)))
129 }
130
131 pub fn expand(&self, anchors: &Anchors) -> String {
136 let mut out = String::with_capacity(self.raw.len());
137 for segment in &self.segments {
138 match segment {
139 Segment::Literal(text) => out.push_str(text),
140 Segment::Anchor(p) => out.push_str(&anchors.path_of(*p).to_string_lossy()),
141 }
142 }
143 out
144 }
145}
146
147impl fmt::Display for Template {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 f.write_str(&self.raw)
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn anchors() -> Anchors {
158 Anchors::new("/plugins/devtools", "/plugins/data/devtools")
159 }
160
161 #[test]
162 fn expands_both_placeholders() {
163 let t = Template::parse("${PLUGIN_ROOT}/config:${PLUGIN_DATA}/cache");
164 assert_eq!(t.expand(&anchors()), "/plugins/devtools/config:/plugins/data/devtools/cache");
165 }
166
167 #[test]
168 fn unrecognized_placeholders_stay_literal() {
169 let t = Template::parse("${HOME}/x ${PLUGIN_ROOT} $PLUGIN_DATA ${plugin_root}");
170 assert_eq!(t.expand(&anchors()), "${HOME}/x /plugins/devtools $PLUGIN_DATA ${plugin_root}");
171 }
172
173 #[test]
174 fn expansion_is_not_recursive() {
175 let sneaky = Anchors::new("/evil/${PLUGIN_DATA}", "/data");
178 let t = Template::parse("${PLUGIN_ROOT}/bin");
179 assert_eq!(t.expand(&sneaky), "/evil/${PLUGIN_DATA}/bin");
180 }
181
182 #[test]
183 fn adjacent_and_edge_placeholders() {
184 let t = Template::parse("${PLUGIN_ROOT}${PLUGIN_DATA}");
185 assert_eq!(t.expand(&anchors()), "/plugins/devtools/plugins/data/devtools");
186 let empty = Template::parse("");
187 assert_eq!(empty.expand(&anchors()), "");
188 }
189}