use anyhow::Result;
use std::{collections::HashMap, env};
macro_rules! regex {
($re:literal $(,)?) => {{
static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new();
RE.get_or_init(|| regex::Regex::new($re).unwrap())
}};
}
pub fn resolve_tags(raw_text: &str, dict: &HashMap<String, String>) -> Result<String> {
let mut index: usize = 0;
let mut parsed_text: String = "".to_string();
while index < raw_text.len() {
let source_text = &raw_text[index..];
let result = try_consume(source_text)?;
index += match result {
ParseResult::Nothing => {
parsed_text.push_str(source_text);
source_text.len()
}
ParseResult::Found {
directive,
key,
default,
start,
end,
} => {
let replacement = match directive.as_str() {
"ENV" => resolve_env(&key, default),
"REF" => resolve_ref(&key, dict),
_ => Err(anyhow::anyhow!(
"the directive: ` {}` is not supported.",
directive
)),
}?;
if start > 0 {
parsed_text.push_str(&source_text[..start]);
}
parsed_text.push_str(&replacement);
end
}
};
}
Ok(parsed_text)
}
fn resolve_ref(key: &str, dict: &HashMap<String, String>) -> Result<String> {
dict.get(key)
.map(|value| value.to_owned())
.ok_or_else(|| anyhow::anyhow!("failed to idintify a record referred by the key: `{key}`"))
}
#[derive(PartialEq, Debug)]
enum ParseResult {
Found {
directive: String,
key: String,
default: Option<String>,
start: usize, end: usize, },
Nothing, }
fn resolve_env(key: &str, defalut: Option<String>) -> Result<String> {
env::var(key).or_else(|_| match defalut {
Some(value) => Ok(value),
None => Err(anyhow::anyhow!(
"environment variable: `{}` is not found",
key
)),
})
}
fn try_consume(source: &str) -> Result<ParseResult> {
let re = regex!(
r#"\$\{\{\s*(?P<directive>[[:alnum:]]+)\(\s*(?P<key>[[:alnum:]_-]+)(\s*:-\s*(?P<default>([[:alnum:]]+|"[^"[:cntrl:]]+")))?\s*\)\s*\}\}"#
);
let captures = match re.captures(source) {
Some(captures) => captures,
None => return Ok(ParseResult::Nothing),
};
let directive = captures
.name("directive")
.map(|matched| matched.as_str().to_string());
let key = captures
.name("key")
.map(|matched| matched.as_str().to_string());
let default = captures
.name("default")
.map(|matched| matched.as_str().to_string());
let base_capture = captures.get(0);
let start = base_capture.map(|matched| matched.start());
let end = base_capture.map(|matched| matched.end());
match (directive, key, start, end) {
(Some(directive), Some(key), Some(start), Some(end)) => Ok(ParseResult::Found {
directive,
key,
default,
start,
end,
}),
_ => Err(anyhow::anyhow!(
"match failed for unknown reasons: check that the regex has valid form"
)),
}
}
#[cfg(test)]
mod tests {
use crate::resolver::*;
use std::env;
#[test]
fn test_resolve_tags() {
let raw_text =
"The quick brown ${{ ENV(FOX) }} jumps over\nthe lazy ${{ REF(dog) }}".to_string();
env::set_var("FOX", "🦊");
let dict = HashMap::from([
("swan".to_string(), "🦢".to_string()),
("dog".to_string(), "🐕".to_string()),
]);
let parsed_text = resolve_tags(&raw_text, &dict).unwrap();
assert_eq!(parsed_text, "The quick brown 🦊 jumps over\nthe lazy 🐕");
let dict = HashMap::from([
("swan".to_string(), "🦢".to_string()),
("dolphin".to_string(), "🐬".to_string()),
]);
let parsed_text = resolve_tags(&raw_text, &dict);
assert!(parsed_text.is_err());
let dict = HashMap::new();
let parsed_text = resolve_tags(&raw_text, &dict);
assert!(parsed_text.is_err());
env::remove_var("FOX");
let dict = HashMap::from([
("swan".to_string(), "🦢".to_string()),
("dog".to_string(), "🐕".to_string()),
]);
let parsed_text = resolve_tags(&raw_text, &dict);
assert!(parsed_text.is_err());
let raw_text = "The quick brown ${{ENV(FOX?)}} jumps over\nthe lazy {REF(dog)}".to_string();
let parsed_text = resolve_tags(&raw_text, &dict).unwrap();
assert_eq!(
parsed_text,
"The quick brown ${{ENV(FOX?)}} jumps over\nthe lazy {REF(dog)}".to_string()
);
let raw_text = "The quick brown ${{REFERENCE(fox_id)}} jumps over the lazy dog".to_string();
let parsed_text = resolve_tags(&raw_text, &dict);
assert!(parsed_text.is_err());
}
#[test]
fn test_resolve_ref() {
let dict = HashMap::from([
("foo".to_string(), "bar".to_string()),
("umi".to_string(), "yama".to_string()),
]);
let value = resolve_ref("foo", &dict).unwrap();
assert_eq!(value, "bar");
let value = resolve_ref("BAZ", &dict);
assert!(value.is_err());
let dict = HashMap::new();
let value = resolve_ref("foo", &dict);
assert!(value.is_err());
}
#[test]
fn test_resolve_env() {
let key = "FOO";
env::remove_var(key);
assert!(resolve_env(key, None).is_err());
let value = resolve_env(key, Some("default".to_string())).unwrap();
assert_eq!(value, "default");
env::set_var(key, "SOME_VALUE");
assert_eq!(resolve_env(key, None).unwrap(), "SOME_VALUE");
let value = resolve_env(key, Some("default".to_string())).unwrap();
assert_eq!(value, "SOME_VALUE");
}
#[test]
fn test_try_consume() {
let source_text = "abc${{ SomeDirective(key-is-here) }}xyz";
let result = try_consume(source_text).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "SomeDirective".to_string(),
key: "key-is-here".to_string(),
default: None,
start: 3,
end: 37,
}
);
let source_text = r#"abc${{ SomeDirective(key-is-here:-DEFAULT1) }}xyz"#;
let result = try_consume(source_text).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "SomeDirective".to_string(),
key: "key-is-here".to_string(),
default: Some("DEFAULT1".to_string()),
start: 3,
end: 47,
}
);
let source_text = r#"abc${{ SomeDirective(key-is-here:-"See? th|s @lso fa!!s b/\ck to .. `default` value 🏡") }}xyz"#;
let result = try_consume(source_text).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "SomeDirective".to_string(),
key: "key-is-here".to_string(),
default: Some(
r#""See? th|s @lso fa!!s b/\ck to .. `default` value 🏡""#.to_string()
),
start: 3,
end: 94,
}
);
let source_text =
"abc${{ SomeDirective(key-is-here) }}xyz${{ SomeOtherDirective(key) }}pqrs${{FOO(bar)}}";
let result = try_consume(source_text).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "SomeDirective".to_string(),
key: "key-is-here".to_string(),
default: None,
start: 3,
end: 37,
}
);
let source_text = "${{ FOOOOO( \t bar ) \t }}";
let result = try_consume(source_text).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "FOOOOO".to_string(),
key: "bar".to_string(),
default: None,
start: 0,
end: 36,
}
);
let source_text = "123456789${{Hoge(fuga)}}";
let result = try_consume(source_text).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "Hoge".to_string(),
key: "fuga".to_string(),
default: None,
start: 9,
end: 24,
}
);
let result = try_consume(&source_text[9..]).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "Hoge".to_string(),
key: "fuga".to_string(),
default: None,
start: 0,
end: 15,
}
);
let source_text = "${{A1(key1)}} ${{A2(key2)}} ${{A3(key3)}}";
let result = try_consume(source_text).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "A1".to_string(),
key: "key1".to_string(),
default: None,
start: 0,
end: 13,
}
);
let result = try_consume(&source_text[1..]).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "A2".to_string(),
key: "key2".to_string(),
default: None,
start: 14,
end: 27,
}
);
let result = try_consume(&source_text[16..]).unwrap();
assert_eq!(
result,
ParseResult::Found {
directive: "A3".to_string(),
key: "key3".to_string(),
default: None,
start: 13,
end: 26,
}
);
let result = try_consume(&source_text[30..]).unwrap();
assert_eq!(result, ParseResult::Nothing);
let source_text = "foo bar baz{ hoge: fuga }";
let result = try_consume(source_text).unwrap();
assert_eq!(result, ParseResult::Nothing);
let source_text = "{not(a-tag)}} ${{not(a-tag-too)} }";
let result = try_consume(source_text).unwrap();
assert_eq!(result, ParseResult::Nothing);
let source_text = "${{F-O-O(Bar)}}";
let result = try_consume(source_text).unwrap();
assert_eq!(result, ParseResult::Nothing);
let source_text = "${{no-directive-here}}";
let result = try_consume(source_text).unwrap();
assert_eq!(result, ParseResult::Nothing);
let source_text = "${{foo(bar)(baz)}} ${{foo(hoge}}";
let result = try_consume(source_text).unwrap();
assert_eq!(result, ParseResult::Nothing);
}
}