floe_core/vars/
resolve.rs1use std::collections::HashMap;
2
3use crate::{ConfigError, FloeResult};
4
5const MAX_DEPTH: usize = 32;
9
10pub struct VarSources<'a> {
18 pub profile: &'a HashMap<String, String>,
21 pub cli: &'a HashMap<String, String>,
24 pub config: &'a HashMap<String, String>,
27}
28
29pub fn resolve_vars(sources: VarSources<'_>) -> FloeResult<HashMap<String, String>> {
51 let raw = merge(sources);
52 expand_all(&raw)
53}
54
55fn merge(sources: VarSources<'_>) -> HashMap<String, String> {
61 let mut merged =
63 HashMap::with_capacity(sources.profile.len() + sources.cli.len() + sources.config.len());
64 merged.extend(sources.profile.iter().map(|(k, v)| (k.clone(), v.clone())));
65 merged.extend(sources.cli.iter().map(|(k, v)| (k.clone(), v.clone())));
66 merged.extend(sources.config.iter().map(|(k, v)| (k.clone(), v.clone())));
67 merged
68}
69
70fn expand_all(raw: &HashMap<String, String>) -> FloeResult<HashMap<String, String>> {
72 let mut resolved: HashMap<String, String> = HashMap::with_capacity(raw.len());
73 for key in raw.keys() {
74 if !resolved.contains_key(key.as_str()) {
75 let mut in_progress: Vec<String> = Vec::new();
76 expand_key(key, raw, &mut resolved, &mut in_progress, 0)?;
77 }
78 }
79 Ok(resolved)
80}
81
82fn expand_key(
84 key: &str,
85 raw: &HashMap<String, String>,
86 resolved: &mut HashMap<String, String>,
87 in_progress: &mut Vec<String>,
88 depth: usize,
89) -> FloeResult<String> {
90 if let Some(v) = resolved.get(key) {
92 return Ok(v.clone());
93 }
94
95 if depth > MAX_DEPTH {
97 return Err(Box::new(ConfigError(format!(
98 "variable expansion exceeded maximum depth ({MAX_DEPTH}); \
99 check for deeply nested or circular references near \"${{{key}}}\""
100 ))));
101 }
102
103 if in_progress.iter().any(|k| k == key) {
105 let chain: Vec<&str> = in_progress.iter().map(|s| s.as_str()).collect();
106 return Err(Box::new(ConfigError(format!(
107 "circular variable reference detected: {} -> {}",
108 chain.join(" -> "),
109 key
110 ))));
111 }
112
113 let raw_value = raw.get(key).ok_or_else(|| {
114 Box::new(ConfigError(format!(
115 "variable \"${{{key}}}\" is referenced but not defined"
116 ))) as Box<dyn std::error::Error + Send + Sync>
117 })?;
118
119 in_progress.push(key.to_string());
120 let expanded = expand_value(raw_value, key, raw, resolved, in_progress, depth + 1)?;
121 in_progress.pop();
122
123 resolved.insert(key.to_string(), expanded.clone());
124 Ok(expanded)
125}
126
127fn expand_value(
130 value: &str,
131 owner_key: &str,
132 raw: &HashMap<String, String>,
133 resolved: &mut HashMap<String, String>,
134 in_progress: &mut Vec<String>,
135 depth: usize,
136) -> FloeResult<String> {
137 let mut result = String::with_capacity(value.len());
138 let mut rest = value;
139
140 while let Some(start) = rest.find("${") {
141 result.push_str(&rest[..start]);
142 rest = &rest[start + 2..];
143
144 let end = rest.find('}').ok_or_else(|| {
145 Box::new(ConfigError(format!(
146 "variable \"{owner_key}\": unclosed placeholder in value {value:?}"
147 ))) as Box<dyn std::error::Error + Send + Sync>
148 })?;
149
150 let ref_key = rest[..end].trim();
151 if ref_key.is_empty() {
152 return Err(Box::new(ConfigError(format!(
153 "variable \"{owner_key}\": empty placeholder ${{}}"
154 ))));
155 }
156
157 let ref_value = expand_key(ref_key, raw, resolved, in_progress, depth)?;
158 result.push_str(&ref_value);
159 rest = &rest[end + 1..];
160 }
161
162 result.push_str(rest);
163 Ok(result)
164}