Skip to main content

pray_core/
substitute.rs

1use crate::{PrayError, PrayResult};
2use std::collections::BTreeMap;
3
4/// Strict placeholder form: `((pray:<path>))` with no spaces.
5/// Resolver is fixed to `pray` in v1; grammar leaves room for other resolvers later.
6const PLACEHOLDER_PREFIX: &str = "((pray:";
7const PLACEHOLDER_SUFFIX: &str = "))";
8
9pub fn is_pray_symbol_key(key: &str) -> bool {
10    if key.is_empty() {
11        return false;
12    }
13    key.chars().all(|character| {
14        character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '/' | '-')
15    })
16}
17
18pub fn substitute_pray_symbols(
19    text: &str,
20    symbols: &BTreeMap<String, String>,
21) -> PrayResult<String> {
22    let mut output = String::with_capacity(text.len());
23    let mut rest = text;
24
25    while let Some(start) = rest.find(PLACEHOLDER_PREFIX) {
26        output.push_str(&rest[..start]);
27        let after_prefix = &rest[start + PLACEHOLDER_PREFIX.len()..];
28        let Some(end) = after_prefix.find(PLACEHOLDER_SUFFIX) else {
29            return Err(PrayError::Render(
30                "unclosed ((pray:...) placeholder".to_string(),
31            ));
32        };
33        let path = &after_prefix[..end];
34        if !is_pray_symbol_key(path) {
35            return Err(PrayError::Render(format!(
36                "invalid ((pray:...)) path `{path}`"
37            )));
38        }
39        let Some(value) = symbols.get(path) else {
40            return Err(PrayError::Render(format!(
41                "unknown pray symbol `{path}`; declare it in `pray do ... end`"
42            )));
43        };
44        output.push_str(value);
45        rest = &after_prefix[end + PLACEHOLDER_SUFFIX.len()..];
46    }
47
48    output.push_str(rest);
49    Ok(output)
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn replaces_known_symbols() {
58        let mut symbols = BTreeMap::new();
59        symbols.insert("support_email".to_string(), "a@example.com".to_string());
60        symbols.insert("security_email".to_string(), "b@example.com".to_string());
61        let text = "write ((pray:support_email)) or ((pray:security_email))";
62        assert_eq!(
63            substitute_pray_symbols(text, &symbols).expect("ok"),
64            "write a@example.com or b@example.com"
65        );
66    }
67
68    #[test]
69    fn rejects_unknown_symbol() {
70        let symbols = BTreeMap::new();
71        let error = substitute_pray_symbols("((pray:missing))", &symbols).unwrap_err();
72        assert!(error.to_string().contains("unknown pray symbol"));
73    }
74
75    #[test]
76    fn ignores_spaced_forms() {
77        let mut symbols = BTreeMap::new();
78        symbols.insert("email".to_string(), "a@example.com".to_string());
79        let text = "(( pray:email )) ((pray : email))";
80        assert_eq!(substitute_pray_symbols(text, &symbols).expect("ok"), text);
81    }
82}