use std::collections::HashMap;
use super::error::{AutosaveError, AutosaveResult};
#[derive(Debug, Clone, Default)]
pub struct MacroContext {
macros: HashMap<String, String>,
}
impl MacroContext {
pub fn new() -> Self {
Self::default()
}
pub fn from_map(macros: HashMap<String, String>) -> Self {
Self { macros }
}
pub fn parse_inline(s: &str) -> HashMap<String, String> {
let mut map = HashMap::new();
if s.trim().is_empty() {
return map;
}
for pair in s.split(',') {
let pair = pair.trim();
if let Some(eq_pos) = pair.find('=') {
let key = pair[..eq_pos].trim().to_string();
let val = pair[eq_pos + 1..].trim().to_string();
if !key.is_empty() {
map.insert(key, val);
}
}
}
map
}
pub fn with_overrides(&self, overrides: &HashMap<String, String>) -> Self {
let mut merged = self.macros.clone();
merged.extend(overrides.iter().map(|(k, v)| (k.clone(), v.clone())));
Self { macros: merged }
}
pub fn expand(&self, input: &str, source: &str, line: usize) -> AutosaveResult<String> {
let result = crate::server::db_loader::expand_macros(
input,
&self.macros,
crate::server::db_loader::MacroExpandOptions {
env_fallback: true,
dollar_escape: true,
},
);
if let Some(key) = result.undefined.first() {
return Err(AutosaveError::UndefinedMacro {
key: key.clone(),
source: source.to_string(),
line,
});
}
Ok(result.text)
}
pub fn get(&self, key: &str) -> Option<&str> {
self.macros.get(key).map(|s| s.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_expand() {
let ctx = MacroContext::from_map([("P".into(), "IOC:".into())].into());
assert_eq!(ctx.expand("$(P)temp", "test", 1).unwrap(), "IOC:temp");
}
#[test]
fn test_default_value() {
let ctx = MacroContext::new();
assert_eq!(ctx.expand("$(P=DEFAULT)", "test", 1).unwrap(), "DEFAULT");
}
#[test]
fn test_undefined_error() {
let ctx = MacroContext::new();
let err = ctx.expand("$(UNDEF)", "test.req", 5).unwrap_err();
match err {
AutosaveError::UndefinedMacro { key, source, line } => {
assert_eq!(key, "UNDEF");
assert_eq!(source, "test.req");
assert_eq!(line, 5);
}
_ => panic!("expected UndefinedMacro"),
}
}
#[test]
fn test_parse_inline() {
let map = MacroContext::parse_inline("P=IOC:,M=m1");
assert_eq!(map.get("P").unwrap(), "IOC:");
assert_eq!(map.get("M").unwrap(), "m1");
}
#[test]
fn test_dollar_literal() {
let ctx = MacroContext::new();
assert_eq!(ctx.expand("$$100", "test", 1).unwrap(), "$100");
}
#[test]
fn test_both_pv_and_path() {
let ctx = MacroContext::from_map(
[
("P".into(), "IOC:".into()),
("FILE".into(), "settings".into()),
]
.into(),
);
assert_eq!(
ctx.expand("${FILE}/$(P)temp", "test", 1).unwrap(),
"settings/IOC:temp"
);
}
#[test]
fn nested_default_is_expanded() {
let ctx = MacroContext::from_map([("FOO".into(), "fromfoo".into())].into());
assert_eq!(ctx.expand("${BAR=${FOO}}", "test", 1).unwrap(), "fromfoo");
}
#[test]
fn scoped_definition_is_honored() {
let ctx = MacroContext::from_map(
[
("INNER".into(), "$(A)".into()),
("FOO".into(), "scoped".into()),
]
.into(),
);
assert_eq!(
ctx.expand("$(INNER,A=$(FOO))", "test", 1).unwrap(),
"scoped"
);
}
#[test]
fn resolved_value_is_chained() {
let ctx = MacroContext::from_map(
[("P".into(), "$(Q)".into()), ("Q".into(), "IOC:".into())].into(),
);
assert_eq!(ctx.expand("$(P)TEMP", "test", 1).unwrap(), "IOC:TEMP");
}
#[test]
fn undefined_without_default_still_errors() {
let ctx = MacroContext::new();
let err = ctx.expand("$(NOPE)", "f.req", 7).unwrap_err();
match err {
AutosaveError::UndefinedMacro { key, source, line } => {
assert_eq!(key, "NOPE");
assert_eq!(source, "f.req");
assert_eq!(line, 7);
}
other => panic!("expected UndefinedMacro, got {other:?}"),
}
}
}