use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Anchors {
pub plugin_root: PathBuf,
pub plugin_data: PathBuf,
}
impl Anchors {
pub fn new(plugin_root: impl Into<PathBuf>, plugin_data: impl Into<PathBuf>) -> Self {
Self { plugin_root: plugin_root.into(), plugin_data: plugin_data.into() }
}
fn path_of(&self, placeholder: Placeholder) -> &Path {
match placeholder {
Placeholder::PluginRoot => &self.plugin_root,
Placeholder::PluginData => &self.plugin_data,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Placeholder {
PluginRoot,
PluginData,
}
impl Placeholder {
pub fn spelling(self) -> &'static str {
match self {
Self::PluginRoot => "${PLUGIN_ROOT}",
Self::PluginData => "${PLUGIN_DATA}",
}
}
}
impl fmt::Display for Placeholder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.spelling())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Segment {
Literal(String),
Anchor(Placeholder),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Template {
raw: String,
segments: Vec<Segment>,
}
impl Template {
pub fn parse(raw: &str) -> Self {
let mut segments = Vec::new();
let mut literal = String::new();
let mut rest = raw;
while let Some(dollar) = rest.find("${") {
let (before, from_dollar) = rest.split_at(dollar);
literal.push_str(before);
let placeholder = [Placeholder::PluginRoot, Placeholder::PluginData]
.into_iter()
.find(|p| from_dollar.starts_with(p.spelling()));
match placeholder {
Some(p) => {
if !literal.is_empty() {
segments.push(Segment::Literal(std::mem::take(&mut literal)));
}
segments.push(Segment::Anchor(p));
rest = &from_dollar[p.spelling().len()..];
}
None => {
literal.push_str("${");
rest = &from_dollar[2..];
}
}
}
literal.push_str(rest);
if !literal.is_empty() || segments.is_empty() {
segments.push(Segment::Literal(literal));
}
Self { raw: raw.to_owned(), segments }
}
pub fn as_raw(&self) -> &str {
&self.raw
}
pub fn segments(&self) -> &[Segment] {
&self.segments
}
pub fn has_anchors(&self) -> bool {
self.segments.iter().any(|s| matches!(s, Segment::Anchor(_)))
}
pub fn expand(&self, anchors: &Anchors) -> String {
let mut out = String::with_capacity(self.raw.len());
for segment in &self.segments {
match segment {
Segment::Literal(text) => out.push_str(text),
Segment::Anchor(p) => out.push_str(&anchors.path_of(*p).to_string_lossy()),
}
}
out
}
}
impl fmt::Display for Template {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn anchors() -> Anchors {
Anchors::new("/plugins/devtools", "/plugins/data/devtools")
}
#[test]
fn expands_both_placeholders() {
let t = Template::parse("${PLUGIN_ROOT}/config:${PLUGIN_DATA}/cache");
assert_eq!(t.expand(&anchors()), "/plugins/devtools/config:/plugins/data/devtools/cache");
}
#[test]
fn unrecognized_placeholders_stay_literal() {
let t = Template::parse("${HOME}/x ${PLUGIN_ROOT} $PLUGIN_DATA ${plugin_root}");
assert_eq!(t.expand(&anchors()), "${HOME}/x /plugins/devtools $PLUGIN_DATA ${plugin_root}");
}
#[test]
fn expansion_is_not_recursive() {
let sneaky = Anchors::new("/evil/${PLUGIN_DATA}", "/data");
let t = Template::parse("${PLUGIN_ROOT}/bin");
assert_eq!(t.expand(&sneaky), "/evil/${PLUGIN_DATA}/bin");
}
#[test]
fn adjacent_and_edge_placeholders() {
let t = Template::parse("${PLUGIN_ROOT}${PLUGIN_DATA}");
assert_eq!(t.expand(&anchors()), "/plugins/devtools/plugins/data/devtools");
let empty = Template::parse("");
assert_eq!(empty.expand(&anchors()), "");
}
}