Skip to main content

state_engine/common/
placeholder_resolver.rs

1use regex::Regex;
2use serde_json::Value;
3use std::collections::HashMap;
4
5/// PlaceholderResolver - プレースホルダー抽出・置換ユーティリティ
6///
7/// 依存関係を持たない純粋な文字列処理ユーティリティ。
8/// 値の解決は呼び出し側の責務。
9///
10/// 設計方針:
11/// - エスケープは不要(${} は予約語、YAML DSLとして割り切る)
12/// - 再帰置換を防止(置換後の値が再度置換されない)
13/// - ドット記法は不要(フラットキーで十分、ParameterBuilderの責務)
14pub struct PlaceholderResolver;
15
16impl PlaceholderResolver {
17    /// テンプレート文字列からプレースホルダ名を抽出
18    ///
19    /// ドット記法を含む placeholder にも対応(例: ${connection.tenant})
20    ///
21    /// # Examples
22    ///
23    /// ```
24    /// use state_engine::common::placeholder_resolver::PlaceholderResolver;
25    ///
26    /// let template = "user:${sso_user_id}:${tenant_id}";
27    /// let result = PlaceholderResolver::extract_placeholders(template);
28    /// assert_eq!(result, vec!["sso_user_id", "tenant_id"]);
29    ///
30    /// let template2 = "db:${connection.tenant}";
31    /// let result2 = PlaceholderResolver::extract_placeholders(template2);
32    /// assert_eq!(result2, vec!["connection.tenant"]);
33    /// ```
34    pub fn extract_placeholders(template: &str) -> Vec<String> {
35        // ドット記法対応: \w+ から [\w.]+ に変更
36        let re = Regex::new(r"\$\{([\w.]+)\}").unwrap();
37        re.captures_iter(template)
38            .map(|cap| cap[1].to_string())
39            .collect()
40    }
41
42    /// プレースホルダを値で置換(再帰置換を防止)
43    ///
44    /// 置換は一度のみ実行され、置換後の値が再度置換されることはない。
45    /// 未定義のプレースホルダーはそのまま残される。
46    ///
47    /// # Examples
48    ///
49    /// ```
50    /// use state_engine::common::placeholder_resolver::PlaceholderResolver;
51    /// use std::collections::HashMap;
52    ///
53    /// let template = "user:${sso_user_id}:${tenant_id}";
54    /// let mut params = HashMap::new();
55    /// params.insert("sso_user_id".to_string(), "user001".to_string());
56    /// params.insert("tenant_id".to_string(), "1".to_string());
57    ///
58    /// let result = PlaceholderResolver::replace(template, &params);
59    /// assert_eq!(result, "user:user001:1");
60    /// ```
61    ///
62    /// # 再帰置換の防止
63    ///
64    /// ```
65    /// use state_engine::common::placeholder_resolver::PlaceholderResolver;
66    /// use std::collections::HashMap;
67    ///
68    /// let template = "${a}";
69    /// let mut params = HashMap::new();
70    /// params.insert("a".to_string(), "${b}".to_string());
71    /// params.insert("b".to_string(), "final".to_string());
72    ///
73    /// let result = PlaceholderResolver::replace(template, &params);
74    /// // 'final' にはならず '${b}' のまま(意図的)
75    /// assert_eq!(result, "${b}");
76    /// ```
77    pub fn replace(template: &str, params: &HashMap<String, String>) -> String {
78        // PHPの strtr() と同等の挙動を実装
79        // すべてのプレースホルダーを一度のパスで置換することで、
80        // 置換後の値が再度置換されることを防ぐ
81
82        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            // マッチ前の部分を追加
91            result.push_str(&template[last_match..m.start()]);
92
93            // プレースホルダーを置換(paramsに存在すれば値で、なければそのまま)
94            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        // 残りの部分を追加
104        result.push_str(&template[last_match..]);
105
106        result
107    }
108
109    /// 配列の値でプレースホルダを一括置換(再帰的)
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use state_engine::common::placeholder_resolver::PlaceholderResolver;
115    /// use std::collections::HashMap;
116    /// use serde_yaml_ng::Value;
117    ///
118    /// let mut values = HashMap::new();
119    /// values.insert("key1".to_string(), Value::String("${value1}".to_string()));
120    /// values.insert("key2".to_string(), Value::String("${value2}".to_string()));
121    ///
122    /// let mut params = HashMap::new();
123    /// params.insert("value1".to_string(), "a".to_string());
124    /// params.insert("value2".to_string(), "b".to_string());
125    ///
126    /// let result = PlaceholderResolver::replace_in_map(Value::Mapping(
127    ///     values.into_iter().map(|(k, v)| (Value::String(k), v)).collect()
128    /// ), &params);
129    ///
130    /// // result["key1"] == "a", result["key2"] == "b"
131    /// ```
132    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            // その他の型(Number, Bool, Null)はそのまま
152            other => other,
153        }
154    }
155
156    /// 型付きプレースホルダー解決
157    ///
158    /// callback を使って値を解決し、型を保持する。
159    /// 値全体が ${...} のみの場合は型を保持、文字列の一部なら文字列置換。
160    ///
161    /// # Arguments
162    /// * `value` - 解決対象の値
163    /// * `resolver` - プレースホルダー名から値を解決する callback
164    ///
165    /// # Returns
166    /// * 解決後の値(型保持)
167    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                    // 単一 placeholder → 型を保持して解決
177                    resolver(&placeholders[0]).unwrap_or(Value::String(s))
178                } else if !placeholders.is_empty() {
179                    // 複数 or 文字列内 placeholder → 文字列置換
180                    let mut result = s.clone();
181                    for ph in placeholders {
182                        if let Some(resolved_value) = resolver(&ph) {
183                            // 値を文字列に変換
184                            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                    // placeholder なし → そのまま
196                    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            // その他の型(Number, Bool, Null)はそのまま
214            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, &params);
245        assert_eq!(result, "user:user001:1");
246    }
247
248    #[test]
249    fn test_replace_prevent_recursion() {
250        // 再帰置換を防止(置換後の値が再度置換されない)
251        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, &params);
257        // 'final' にはならず '${b}' のまま(意図的)
258        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, &params);
268        // 未定義のプレースホルダーはそのまま
269        assert_eq!(result, "value: replaced, other: ${other_key}");
270    }
271
272    #[test]
273    fn test_replace_literal_dollar() {
274        // $ 単体は問題ない(${} でなければ置換されない)
275        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, &params);
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), &params);
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), &params);
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), &params);
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}