state_engine/common/
placeholder_resolver.rs1use regex::Regex;
2use serde_json::Value;
3use std::collections::HashMap;
4
5pub struct PlaceholderResolver;
15
16impl PlaceholderResolver {
17 pub fn extract_placeholders(template: &str) -> Vec<String> {
35 let re = Regex::new(r"\$\{([\w.]+)\}").unwrap();
37 re.captures_iter(template)
38 .map(|cap| cap[1].to_string())
39 .collect()
40 }
41
42 pub fn replace(template: &str, params: &HashMap<String, String>) -> String {
78 let re = Regex::new(r"\$\{(\w+)\}").unwrap();
83 let mut result = String::new();
84 let mut last_match = 0;
85
86 for cap in re.captures_iter(template) {
87 let m = cap.get(0).unwrap();
88 let var_name = &cap[1];
89
90 result.push_str(&template[last_match..m.start()]);
92
93 if let Some(value) = params.get(var_name) {
95 result.push_str(value);
96 } else {
97 result.push_str(m.as_str());
98 }
99
100 last_match = m.end();
101 }
102
103 result.push_str(&template[last_match..]);
105
106 result
107 }
108
109 pub fn replace_in_map(value: serde_yaml_ng::Value, params: &HashMap<String, String>) -> serde_yaml_ng::Value {
133 match value {
134 serde_yaml_ng::Value::String(s) => {
135 serde_yaml_ng::Value::String(Self::replace(&s, params))
136 }
137 serde_yaml_ng::Value::Mapping(map) => {
138 let new_map = map
139 .into_iter()
140 .map(|(k, v)| (k, Self::replace_in_map(v, params)))
141 .collect();
142 serde_yaml_ng::Value::Mapping(new_map)
143 }
144 serde_yaml_ng::Value::Sequence(seq) => {
145 let new_seq = seq
146 .into_iter()
147 .map(|v| Self::replace_in_map(v, params))
148 .collect();
149 serde_yaml_ng::Value::Sequence(new_seq)
150 }
151 other => other,
153 }
154 }
155
156 pub fn resolve_typed<F>(value: Value, resolver: &mut F) -> Value
168 where
169 F: FnMut(&str) -> Option<Value>,
170 {
171 match value {
172 Value::String(s) => {
173 let placeholders = Self::extract_placeholders(&s);
174
175 if placeholders.len() == 1 && s == format!("${{{}}}", placeholders[0]) {
176 resolver(&placeholders[0]).unwrap_or(Value::String(s))
178 } else if !placeholders.is_empty() {
179 let mut result = s.clone();
181 for ph in placeholders {
182 if let Some(resolved_value) = resolver(&ph) {
183 let replacement = match resolved_value {
185 Value::String(s) => s,
186 Value::Number(n) => n.to_string(),
187 Value::Bool(b) => b.to_string(),
188 _ => continue,
189 };
190 result = result.replace(&format!("${{{}}}", ph), &replacement);
191 }
192 }
193 Value::String(result)
194 } else {
195 Value::String(s)
197 }
198 }
199 Value::Object(map) => {
200 let mut new_map = serde_json::Map::new();
201 for (k, v) in map {
202 new_map.insert(k, Self::resolve_typed(v, resolver));
203 }
204 Value::Object(new_map)
205 }
206 Value::Array(arr) => {
207 let mut new_arr = Vec::new();
208 for v in arr {
209 new_arr.push(Self::resolve_typed(v, resolver));
210 }
211 Value::Array(new_arr)
212 }
213 other => other,
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_extract_placeholders() {
225 let template = "user:${sso_user_id}:${tenant_id}";
226 let result = PlaceholderResolver::extract_placeholders(template);
227 assert_eq!(result, vec!["sso_user_id", "tenant_id"]);
228 }
229
230 #[test]
231 fn test_extract_placeholders_empty() {
232 let template = "user:123:456";
233 let result = PlaceholderResolver::extract_placeholders(template);
234 assert_eq!(result, Vec::<String>::new());
235 }
236
237 #[test]
238 fn test_replace() {
239 let template = "user:${sso_user_id}:${tenant_id}";
240 let mut params = HashMap::new();
241 params.insert("sso_user_id".to_string(), "user001".to_string());
242 params.insert("tenant_id".to_string(), "1".to_string());
243
244 let result = PlaceholderResolver::replace(template, ¶ms);
245 assert_eq!(result, "user:user001:1");
246 }
247
248 #[test]
249 fn test_replace_prevent_recursion() {
250 let template = "${a}";
252 let mut params = HashMap::new();
253 params.insert("a".to_string(), "${b}".to_string());
254 params.insert("b".to_string(), "final".to_string());
255
256 let result = PlaceholderResolver::replace(template, ¶ms);
257 assert_eq!(result, "${b}");
259 }
260
261 #[test]
262 fn test_replace_partial_match() {
263 let template = "value: ${key}, other: ${other_key}";
264 let mut params = HashMap::new();
265 params.insert("key".to_string(), "replaced".to_string());
266
267 let result = PlaceholderResolver::replace(template, ¶ms);
268 assert_eq!(result, "value: replaced, other: ${other_key}");
270 }
271
272 #[test]
273 fn test_replace_literal_dollar() {
274 let template = "価格は$100です";
276 let mut params = HashMap::new();
277 params.insert("price".to_string(), "200".to_string());
278
279 let result = PlaceholderResolver::replace(template, ¶ms);
280 assert_eq!(result, "価格は$100です");
281 }
282
283 #[test]
284 fn test_replace_in_map_simple() {
285 use serde_yaml_ng::{Mapping, Value};
286
287 let mut map = Mapping::new();
288 map.insert(
289 Value::String("key1".to_string()),
290 Value::String("${value1}".to_string()),
291 );
292 map.insert(
293 Value::String("key2".to_string()),
294 Value::String("${value2}".to_string()),
295 );
296
297 let mut params = HashMap::new();
298 params.insert("value1".to_string(), "a".to_string());
299 params.insert("value2".to_string(), "b".to_string());
300
301 let result = PlaceholderResolver::replace_in_map(Value::Mapping(map), ¶ms);
302
303 if let Value::Mapping(result_map) = result {
304 assert_eq!(
305 result_map.get(&Value::String("key1".to_string())),
306 Some(&Value::String("a".to_string()))
307 );
308 assert_eq!(
309 result_map.get(&Value::String("key2".to_string())),
310 Some(&Value::String("b".to_string()))
311 );
312 } else {
313 panic!("Expected Mapping");
314 }
315 }
316
317 #[test]
318 fn test_replace_in_map_nested() {
319 use serde_yaml_ng::{Mapping, Value};
320
321 let mut inner = Mapping::new();
322 inner.insert(
323 Value::String("key3".to_string()),
324 Value::String("${value3}".to_string()),
325 );
326 inner.insert(
327 Value::String("literal".to_string()),
328 Value::String("no placeholder".to_string()),
329 );
330
331 let mut outer = Mapping::new();
332 outer.insert(
333 Value::String("key1".to_string()),
334 Value::String("${value1}".to_string()),
335 );
336 outer.insert(Value::String("nested".to_string()), Value::Mapping(inner));
337
338 let mut params = HashMap::new();
339 params.insert("value1".to_string(), "a".to_string());
340 params.insert("value3".to_string(), "c".to_string());
341
342 let result = PlaceholderResolver::replace_in_map(Value::Mapping(outer), ¶ms);
343
344 if let Value::Mapping(result_map) = result {
345 assert_eq!(
346 result_map.get(&Value::String("key1".to_string())),
347 Some(&Value::String("a".to_string()))
348 );
349
350 if let Some(Value::Mapping(nested_map)) =
351 result_map.get(&Value::String("nested".to_string()))
352 {
353 assert_eq!(
354 nested_map.get(&Value::String("key3".to_string())),
355 Some(&Value::String("c".to_string()))
356 );
357 assert_eq!(
358 nested_map.get(&Value::String("literal".to_string())),
359 Some(&Value::String("no placeholder".to_string()))
360 );
361 } else {
362 panic!("Expected nested Mapping");
363 }
364 } else {
365 panic!("Expected Mapping");
366 }
367 }
368
369 #[test]
370 fn test_replace_in_map_preserves_types() {
371 use serde_yaml_ng::{Mapping, Value};
372
373 let mut map = Mapping::new();
374 map.insert(
375 Value::String("string".to_string()),
376 Value::String("${key}".to_string()),
377 );
378 map.insert(Value::String("int".to_string()), Value::Number(123.into()));
379 map.insert(Value::String("bool".to_string()), Value::Bool(true));
380 map.insert(Value::String("null".to_string()), Value::Null);
381
382 let mut params = HashMap::new();
383 params.insert("key".to_string(), "replaced".to_string());
384
385 let result = PlaceholderResolver::replace_in_map(Value::Mapping(map), ¶ms);
386
387 if let Value::Mapping(result_map) = result {
388 assert_eq!(
389 result_map.get(&Value::String("string".to_string())),
390 Some(&Value::String("replaced".to_string()))
391 );
392 assert_eq!(
393 result_map.get(&Value::String("int".to_string())),
394 Some(&Value::Number(123.into()))
395 );
396 assert_eq!(
397 result_map.get(&Value::String("bool".to_string())),
398 Some(&Value::Bool(true))
399 );
400 assert_eq!(
401 result_map.get(&Value::String("null".to_string())),
402 Some(&Value::Null)
403 );
404 } else {
405 panic!("Expected Mapping");
406 }
407 }
408}