use crate::error::{Error, Result};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum Interpolation {
Literal(String),
Resolver {
name: String,
args: Vec<InterpolationArg>,
kwargs: HashMap<String, InterpolationArg>,
},
SelfRef {
path: String,
relative: bool,
},
Concat(Vec<Interpolation>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum InterpolationArg {
Literal(String),
Nested(Box<Interpolation>),
}
impl InterpolationArg {
pub fn is_literal(&self) -> bool {
matches!(self, InterpolationArg::Literal(_))
}
pub fn as_literal(&self) -> Option<&str> {
match self {
InterpolationArg::Literal(s) => Some(s),
_ => None,
}
}
}
pub struct InterpolationParser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> InterpolationParser<'a> {
pub fn new(input: &'a str) -> Self {
Self { input, pos: 0 }
}
pub fn parse(&mut self) -> Result<Interpolation> {
let mut parts = Vec::new();
while !self.is_eof() {
if self.check_escape() {
self.advance(); self.advance(); self.advance(); parts.push(Interpolation::Literal("${".to_string()));
} else if self.check_interpolation_start() {
parts.push(self.parse_interpolation()?);
} else {
let literal = self.collect_literal();
if !literal.is_empty() {
parts.push(Interpolation::Literal(literal));
}
}
}
match parts.len() {
0 => Ok(Interpolation::Literal(String::new())),
1 => Ok(parts.remove(0)),
_ => {
let merged = merge_adjacent_literals(parts);
if merged.len() == 1 {
Ok(merged.into_iter().next().unwrap())
} else {
Ok(Interpolation::Concat(merged))
}
}
}
}
fn is_eof(&self) -> bool {
self.pos >= self.input.len()
}
fn current(&self) -> Option<char> {
self.input[self.pos..].chars().next()
}
fn peek(&self) -> Option<char> {
let mut chars = self.input[self.pos..].chars();
chars.next();
chars.next()
}
fn peek_n(&self, n: usize) -> Option<char> {
self.input[self.pos..].chars().nth(n)
}
fn advance(&mut self) {
if let Some(c) = self.current() {
self.pos += c.len_utf8();
}
}
fn check_escape(&self) -> bool {
self.current() == Some('\\') && self.peek() == Some('$') && self.peek_n(2) == Some('{')
}
fn check_interpolation_start(&self) -> bool {
self.current() == Some('$') && self.peek() == Some('{')
}
fn collect_literal(&mut self) -> String {
let mut result = String::new();
while !self.is_eof() {
if self.check_escape() {
break;
}
if self.check_interpolation_start() {
break;
}
if let Some(c) = self.current() {
result.push(c);
self.advance();
}
}
result
}
fn parse_interpolation(&mut self) -> Result<Interpolation> {
self.advance(); self.advance();
self.skip_whitespace();
if self.is_eof() {
return Err(Error::parse("Unexpected end of input in interpolation"));
}
if self.current() == Some('.') {
return self.parse_self_ref(true);
}
let identifier = self.collect_identifier();
if identifier.is_empty() {
return Err(Error::parse("Empty interpolation expression"));
}
self.skip_whitespace();
match self.current() {
Some(':') => {
self.advance(); self.parse_resolver_call(identifier)
}
Some('}') => {
self.advance(); Ok(Interpolation::Resolver {
name: "ref".to_string(),
args: vec![InterpolationArg::Literal(identifier)],
kwargs: HashMap::new(),
})
}
Some(',') => {
self.advance(); self.parse_resolver_call_with_first_arg("ref".to_string(), identifier)
}
Some(c) => Err(Error::parse(format!(
"Unexpected character '{}' in interpolation",
c
))),
None => Err(Error::parse("Unexpected end of input in interpolation")),
}
}
fn parse_self_ref(&mut self, relative: bool) -> Result<Interpolation> {
let mut path = String::new();
while !self.is_eof() {
match self.current() {
Some('}') => {
self.advance();
break;
}
Some(c) if c.is_alphanumeric() || c == '_' || c == '.' || c == '[' || c == ']' => {
path.push(c);
self.advance();
}
Some(c) => {
return Err(Error::parse(format!("Invalid character '{}' in path", c)));
}
None => {
return Err(Error::parse("Unexpected end of input in path"));
}
}
}
Ok(Interpolation::SelfRef { path, relative })
}
fn parse_resolver_call(&mut self, name: String) -> Result<Interpolation> {
let mut args = Vec::new();
let mut kwargs = HashMap::new();
loop {
self.skip_whitespace();
if self.current() == Some('}') {
self.advance();
break;
}
if !args.is_empty() || !kwargs.is_empty() {
if self.current() != Some(',') {
return Err(Error::parse("Expected ',' between arguments"));
}
self.advance(); self.skip_whitespace();
}
if let Some((key, value_arg)) = self.try_parse_kwarg()? {
kwargs.insert(key, value_arg);
} else {
let arg = self.parse_argument()?;
args.push(arg);
}
}
Ok(Interpolation::Resolver { name, args, kwargs })
}
fn parse_resolver_call_with_first_arg(
&mut self,
name: String,
first_arg: String,
) -> Result<Interpolation> {
let mut args = vec![InterpolationArg::Literal(first_arg)];
let mut kwargs = HashMap::new();
loop {
self.skip_whitespace();
if self.current() == Some('}') {
self.advance();
break;
}
self.skip_whitespace();
if let Some((key, value_arg)) = self.try_parse_kwarg()? {
kwargs.insert(key, value_arg);
} else {
let arg = self.parse_argument()?;
args.push(arg);
}
self.skip_whitespace();
match self.current() {
Some(',') => {
self.advance();
continue;
}
Some('}') => {
continue;
}
Some(c) => {
return Err(Error::parse(format!(
"Expected ',' or '}}' but found '{}'",
c
)));
}
None => {
return Err(Error::parse("Unexpected end of input in interpolation"));
}
}
}
Ok(Interpolation::Resolver { name, args, kwargs })
}
fn try_parse_kwarg(&mut self) -> Result<Option<(String, InterpolationArg)>> {
let start_pos = self.pos;
let mut key = String::new();
while !self.is_eof() {
match self.current() {
Some(c) if c.is_alphanumeric() || c == '_' => {
key.push(c);
self.advance();
}
Some('=') if !key.is_empty() => {
self.advance(); let value = self.parse_argument()?;
return Ok(Some((key, value)));
}
_ => {
self.pos = start_pos;
return Ok(None);
}
}
}
self.pos = start_pos;
Ok(None)
}
fn parse_argument(&mut self) -> Result<InterpolationArg> {
self.skip_whitespace();
if self.check_interpolation_start() {
let nested = self.parse_interpolation()?;
Ok(InterpolationArg::Nested(Box::new(nested)))
} else {
let mut value = String::new();
let mut depth = 0;
while !self.is_eof() {
match self.current() {
Some('$') if self.peek() == Some('{') => {
let nested = self.parse_interpolation()?;
return Ok(InterpolationArg::Nested(Box::new(if value.is_empty() {
nested
} else {
Interpolation::Concat(vec![Interpolation::Literal(value), nested])
})));
}
Some('{') => {
depth += 1;
value.push('{');
self.advance();
}
Some('}') => {
if depth == 0 {
break;
}
depth -= 1;
value.push('}');
self.advance();
}
Some(',') if depth == 0 => {
break;
}
Some(c) => {
value.push(c);
self.advance();
}
None => break,
}
}
Ok(InterpolationArg::Literal(value.trim().to_string()))
}
}
fn collect_identifier(&mut self) -> String {
let mut result = String::new();
while !self.is_eof() {
match self.current() {
Some(c) if c.is_alphanumeric() || c == '_' || c == '.' || c == '[' || c == ']' => {
result.push(c);
self.advance();
}
_ => break,
}
}
result
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.current() {
if c.is_whitespace() {
self.advance();
} else {
break;
}
}
}
}
fn merge_adjacent_literals(parts: Vec<Interpolation>) -> Vec<Interpolation> {
let mut result = Vec::new();
let mut current_literal = String::new();
for part in parts {
match part {
Interpolation::Literal(s) => {
current_literal.push_str(&s);
}
other => {
if !current_literal.is_empty() {
result.push(Interpolation::Literal(current_literal));
current_literal = String::new();
}
result.push(other);
}
}
}
if !current_literal.is_empty() {
result.push(Interpolation::Literal(current_literal));
}
result
}
pub fn parse(input: &str) -> Result<Interpolation> {
InterpolationParser::new(input).parse()
}
pub fn contains_interpolation(input: &str) -> bool {
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
chars.next();
} else if c == '$' && chars.peek() == Some(&'{') {
return true;
}
}
false
}
pub fn needs_processing(input: &str) -> bool {
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' && chars.peek() == Some(&'$') {
return true;
} else if c == '$' && chars.peek() == Some(&'{') {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_literal() {
let result = parse("hello world").unwrap();
assert_eq!(result, Interpolation::Literal("hello world".into()));
}
#[test]
fn test_parse_empty() {
let result = parse("").unwrap();
assert_eq!(result, Interpolation::Literal("".into()));
}
#[test]
fn test_parse_env_resolver() {
let result = parse("${env:MY_VAR}").unwrap();
assert_eq!(
result,
Interpolation::Resolver {
name: "env".into(),
args: vec![InterpolationArg::Literal("MY_VAR".into())],
kwargs: HashMap::new(),
}
);
}
#[test]
fn test_parse_env_with_default() {
let result = parse("${env:MY_VAR,default_value}").unwrap();
assert_eq!(
result,
Interpolation::Resolver {
name: "env".into(),
args: vec![
InterpolationArg::Literal("MY_VAR".into()),
InterpolationArg::Literal("default_value".into()),
],
kwargs: HashMap::new(),
}
);
}
#[test]
fn test_parse_self_reference() {
let result = parse("${database.host}").unwrap();
assert_eq!(
result,
Interpolation::Resolver {
name: "ref".into(),
args: vec![InterpolationArg::Literal("database.host".into())],
kwargs: HashMap::new(),
}
);
}
#[test]
fn test_parse_self_reference_with_default() {
let result = parse("${database.host,default=fallback}").unwrap();
if let Interpolation::Resolver { name, args, kwargs } = result {
assert_eq!(name, "ref");
assert_eq!(args.len(), 1);
assert_eq!(args[0].as_literal(), Some("database.host"));
assert_eq!(kwargs.len(), 1);
assert!(kwargs.contains_key("default"));
assert_eq!(
kwargs.get("default").and_then(|v| v.as_literal()),
Some("fallback")
);
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_relative_self_reference() {
let result = parse("${.sibling}").unwrap();
assert_eq!(
result,
Interpolation::SelfRef {
path: ".sibling".into(),
relative: true,
}
);
}
#[test]
fn test_parse_array_access() {
let result = parse("${servers[0].host}").unwrap();
assert_eq!(
result,
Interpolation::Resolver {
name: "ref".into(),
args: vec![InterpolationArg::Literal("servers[0].host".into())],
kwargs: HashMap::new(),
}
);
}
#[test]
fn test_parse_escaped() {
let result = parse(r"\${not_interpolated}").unwrap();
assert_eq!(result, Interpolation::Literal("${not_interpolated}".into()));
}
#[test]
fn test_parse_concatenation() {
let result = parse("prefix_${env:VAR}_suffix").unwrap();
assert!(matches!(result, Interpolation::Concat(_)));
if let Interpolation::Concat(parts) = result {
assert_eq!(parts.len(), 3);
assert_eq!(parts[0], Interpolation::Literal("prefix_".into()));
assert!(matches!(parts[1], Interpolation::Resolver { .. }));
assert_eq!(parts[2], Interpolation::Literal("_suffix".into()));
}
}
#[test]
fn test_parse_nested_interpolation() {
let result = parse("${env:VAR,${env:DEFAULT,fallback}}").unwrap();
if let Interpolation::Resolver { name, args, .. } = result {
assert_eq!(name, "env");
assert_eq!(args.len(), 2);
assert!(matches!(args[0], InterpolationArg::Literal(_)));
assert!(matches!(args[1], InterpolationArg::Nested(_)));
} else {
panic!("Expected Resolver, got {:?}", result);
}
}
#[test]
fn test_parse_kwargs() {
let result = parse("${file:./config.yaml,parse=text}").unwrap();
if let Interpolation::Resolver { kwargs, .. } = result {
assert!(kwargs.contains_key("parse"));
if let Some(InterpolationArg::Literal(value)) = kwargs.get("parse") {
assert_eq!(value, "text");
} else {
panic!("Expected literal value for parse kwarg");
}
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_contains_interpolation() {
assert!(contains_interpolation("${env:VAR}"));
assert!(contains_interpolation("prefix ${env:VAR} suffix"));
assert!(!contains_interpolation("no interpolation"));
assert!(!contains_interpolation(r"\${escaped}"));
assert!(!contains_interpolation("just $dollar"));
}
#[test]
fn test_parse_unclosed_interpolation() {
let result = parse("${env:VAR");
assert!(result.is_err());
}
#[test]
fn test_parse_empty_interpolation() {
let result = parse("${}");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("Empty"));
}
#[test]
fn test_parse_resolver_no_args() {
let result = parse("${env:}").unwrap();
if let Interpolation::Resolver { name, args, .. } = result {
assert_eq!(name, "env");
assert!(
args.is_empty()
|| (args.len() == 1 && args[0] == InterpolationArg::Literal("".into()))
);
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_whitespace_in_interpolation() {
let result = parse("${ env:VAR }").unwrap();
if let Interpolation::Resolver { name, .. } = result {
assert_eq!(name, "env");
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_multiple_escapes() {
let result = parse(r"\${first}\${second}").unwrap();
assert_eq!(result, Interpolation::Literal("${first}${second}".into()));
}
#[test]
fn test_parse_interpolation_at_start() {
let result = parse("${env:VAR}suffix").unwrap();
if let Interpolation::Concat(parts) = result {
assert_eq!(parts.len(), 2);
assert!(matches!(parts[0], Interpolation::Resolver { .. }));
assert_eq!(parts[1], Interpolation::Literal("suffix".into()));
} else {
panic!("Expected Concat");
}
}
#[test]
fn test_parse_interpolation_at_end() {
let result = parse("prefix${env:VAR}").unwrap();
if let Interpolation::Concat(parts) = result {
assert_eq!(parts.len(), 2);
assert_eq!(parts[0], Interpolation::Literal("prefix".into()));
assert!(matches!(parts[1], Interpolation::Resolver { .. }));
} else {
panic!("Expected Concat");
}
}
#[test]
fn test_parse_adjacent_interpolations() {
let result = parse("${env:A}${env:B}").unwrap();
if let Interpolation::Concat(parts) = result {
assert_eq!(parts.len(), 2);
assert!(matches!(parts[0], Interpolation::Resolver { .. }));
assert!(matches!(parts[1], Interpolation::Resolver { .. }));
} else {
panic!("Expected Concat");
}
}
#[test]
fn test_parse_deeply_nested_path() {
let result = parse("${a.b.c.d.e.f.g.h}").unwrap();
if let Interpolation::Resolver { name, args, .. } = result {
assert_eq!(name, "ref");
assert_eq!(args.len(), 1);
assert_eq!(args[0].as_literal(), Some("a.b.c.d.e.f.g.h"));
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_multiple_array_indices() {
let result = parse("${matrix[0][1][2]}").unwrap();
if let Interpolation::Resolver { name, args, .. } = result {
assert_eq!(name, "ref");
assert_eq!(args[0].as_literal(), Some("matrix[0][1][2]"));
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_mixed_path_and_array() {
let result = parse("${data.items[0].nested[1].value}").unwrap();
if let Interpolation::Resolver { name, args, .. } = result {
assert_eq!(name, "ref");
assert_eq!(args[0].as_literal(), Some("data.items[0].nested[1].value"));
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_underscore_in_identifiers() {
let result = parse("${my_var.some_path}").unwrap();
if let Interpolation::Resolver { name, args, .. } = result {
assert_eq!(name, "ref");
assert_eq!(args[0].as_literal(), Some("my_var.some_path"));
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_resolver_with_multiple_args() {
let result = parse("${resolver:arg1,arg2,arg3}").unwrap();
if let Interpolation::Resolver { name, args, .. } = result {
assert_eq!(name, "resolver");
assert_eq!(args.len(), 3);
} else {
panic!("Expected Resolver");
}
}
#[test]
fn test_parse_mixed_escaped_and_interpolation() {
let result = parse(r"literal \${escaped} ${env:VAR} more").unwrap();
if let Interpolation::Concat(parts) = result {
assert!(parts.len() >= 3);
} else {
panic!("Expected Concat");
}
}
#[test]
fn test_needs_processing() {
assert!(needs_processing("${env:VAR}"));
assert!(needs_processing(r"\${escaped}"));
assert!(!needs_processing("no special chars"));
assert!(!needs_processing("just $dollar"));
}
#[test]
fn test_parse_invalid_char_in_path() {
let result = parse("${path!invalid}");
assert!(result.is_err());
}
#[test]
fn test_interpolation_arg_methods() {
let lit = InterpolationArg::Literal("test".into());
assert!(lit.is_literal());
assert_eq!(lit.as_literal(), Some("test"));
let nested = InterpolationArg::Nested(Box::new(Interpolation::Literal("x".into())));
assert!(!nested.is_literal());
assert_eq!(nested.as_literal(), None);
}
#[test]
fn test_parse_relative_parent_reference() {
let result = parse("${..parent.value}").unwrap();
if let Interpolation::SelfRef { path, relative } = result {
assert!(relative);
assert_eq!(path, "..parent.value");
} else {
panic!("Expected relative SelfRef");
}
}
}