use std::collections::BTreeMap;
pub(crate) struct ResolutionValues<'a> {
source: &'a dyn ValueSource,
environment: std::sync::Mutex<BTreeMap<String, Option<String>>>,
commands: std::sync::Mutex<BTreeMap<String, Option<String>>>,
now: i64,
}
impl<'a> ResolutionValues<'a> {
pub(crate) fn new(source: &'a dyn ValueSource) -> Self {
Self {
source,
environment: Default::default(),
commands: Default::default(),
now: source.now_unix_seconds(),
}
}
}
impl ValueSource for ResolutionValues<'_> {
fn env(&self, name: &str) -> Option<String> {
self.environment
.lock()
.unwrap()
.entry(name.into())
.or_insert_with(|| self.source.env(name))
.clone()
}
fn command(&self, command: &str, cached: bool) -> Option<String> {
if !cached {
return self.source.command(command, false);
}
self.commands
.lock()
.unwrap()
.entry(command.into())
.or_insert_with(|| self.source.command(command, true))
.clone()
}
fn now_unix_seconds(&self) -> i64 {
self.now
}
}
pub trait ValueSource: Send + Sync {
fn env(&self, name: &str) -> Option<String>;
fn command(&self, _command: &str, _cached: bool) -> Option<String> {
None
}
fn now_unix_seconds(&self) -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|duration| duration.as_secs() as i64)
.unwrap_or(i64::MAX)
}
}
impl ValueSource for BTreeMap<String, String> {
fn env(&self, name: &str) -> Option<String> {
self.get(name).cloned()
}
}
pub fn resolve_config_value(
raw: &str,
overlay: Option<&BTreeMap<String, String>>,
values: &dyn ValueSource,
cached: bool,
) -> Option<String> {
match raw.strip_prefix('!') {
Some(command) => values.command(command, cached),
None => resolve_template(raw, overlay, values),
}
}
pub fn resolve_headers(
headers: &BTreeMap<String, String>,
overlay: Option<&BTreeMap<String, String>>,
values: &dyn ValueSource,
) -> BTreeMap<String, String> {
headers
.iter()
.filter_map(|(name, value)| {
resolve_config_value(value, overlay, values, false).map(|value| (name.clone(), value))
})
.collect()
}
enum TemplatePart {
Literal(String),
Env(String),
}
fn parse_template(config: &str) -> Vec<TemplatePart> {
let mut parts: Vec<TemplatePart> = Vec::new();
let bytes = config.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
match config[i..].find('$') {
None => {
push_literal(&mut parts, &config[i..]);
break;
}
Some(offset) => {
let dollar = i + offset;
push_literal(&mut parts, &config[i..dollar]);
let after = dollar + 1;
let next = bytes.get(after).copied();
if next == Some(b'$') || next == Some(b'!') {
push_literal(&mut parts, &config[after..after + 1]);
i = after + 1;
continue;
}
if next == Some(b'{') {
if let Some(end_rel) = config[after + 1..].find('}') {
let end = after + 1 + end_rel;
let name = &config[after + 1..end];
if is_env_name(name) {
parts.push(TemplatePart::Env(name.to_string()));
} else {
push_literal(&mut parts, &config[dollar..=end]);
}
i = end + 1;
continue;
}
push_literal(&mut parts, "$");
i = after;
continue;
}
if let Some(name) = env_name_prefix(&config[after..]) {
parts.push(TemplatePart::Env(name.to_string()));
i = after + name.len();
} else {
push_literal(&mut parts, "$");
i = after;
}
}
}
}
parts
}
fn push_literal(parts: &mut Vec<TemplatePart>, value: &str) {
if value.is_empty() {
return;
}
if let Some(TemplatePart::Literal(s)) = parts.last_mut() {
s.push_str(value);
} else {
parts.push(TemplatePart::Literal(value.to_string()));
}
}
fn is_env_name(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn env_name_prefix(s: &str) -> Option<&str> {
let mut chars = s.char_indices();
match chars.next() {
Some((_, c)) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return None,
}
let end = chars
.find(|(_, c)| !(c.is_ascii_alphanumeric() || *c == '_'))
.map(|(idx, _)| idx)
.unwrap_or(s.len());
Some(&s[..end])
}
pub fn resolve_template(
config: &str,
env_overlay: Option<&BTreeMap<String, String>>,
values: &dyn ValueSource,
) -> Option<String> {
let parts = parse_template(config);
let mut out = String::with_capacity(config.len());
for part in parts {
match part {
TemplatePart::Literal(s) => out.push_str(&s),
TemplatePart::Env(name) => {
let v = env_overlay
.and_then(|overlay| overlay.get(&name).cloned())
.or_else(|| values.env(&name))?;
out.push_str(&v)
}
}
}
Some(out)
}