gemini_adk_rs/
instruction.rs

1//! Instruction templating — inject state values into instruction strings.
2//!
3//! Replaces `{key}` placeholders with values from the state container.
4//! Supports optional `{key?}` syntax that resolves to empty string if missing.
5
6use regex::Regex;
7use std::sync::LazyLock;
8
9use crate::state::State;
10
11static PLACEHOLDER_RE: LazyLock<Regex> =
12    LazyLock::new(|| Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_:]*)\??\}").unwrap());
13
14/// Replace `{key}` placeholders in `template` with values from `state`.
15///
16/// - `{key}` — required: if present in state, replaced with the string representation;
17///   if missing, left as-is (e.g., `{unknown}` stays `{unknown}`)
18/// - `{key?}` — optional: if present in state, replaced; if missing, replaced with `""`
19/// - Prefix keys are supported: `{app:flag}`, `{user:name}`, etc.
20pub fn inject_session_state(template: &str, state: &State) -> String {
21    PLACEHOLDER_RE
22        .replace_all(template, |caps: &regex::Captures| {
23            let full_match = &caps[0];
24            let key = &caps[1];
25            let optional = full_match.ends_with("?}");
26
27            match state.get_raw(key) {
28                Some(value) => value_to_string(&value),
29                None => {
30                    if optional {
31                        String::new()
32                    } else {
33                        full_match.to_string()
34                    }
35                }
36            }
37        })
38        .into_owned()
39}
40
41fn value_to_string(value: &serde_json::Value) -> String {
42    match value {
43        serde_json::Value::String(s) => s.clone(),
44        serde_json::Value::Null => String::new(),
45        other => other.to_string(),
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn simple_substitution() {
55        let state = State::new();
56        state.set("name", "Alice");
57        let result = inject_session_state("Hello, {name}!", &state);
58        assert_eq!(result, "Hello, Alice!");
59    }
60
61    #[test]
62    fn optional_key_present() {
63        let state = State::new();
64        state.set("title", "Dr.");
65        let result = inject_session_state("Hello, {title?} Smith!", &state);
66        assert_eq!(result, "Hello, Dr. Smith!");
67    }
68
69    #[test]
70    fn optional_key_missing() {
71        let state = State::new();
72        let result = inject_session_state("Hello, {title?}Smith!", &state);
73        assert_eq!(result, "Hello, Smith!");
74    }
75
76    #[test]
77    fn missing_required_key_left_as_is() {
78        let state = State::new();
79        let result = inject_session_state("Hello, {unknown}!", &state);
80        assert_eq!(result, "Hello, {unknown}!");
81    }
82
83    #[test]
84    fn multiple_keys() {
85        let state = State::new();
86        state.set("first", "Alice");
87        state.set("last", "Smith");
88        let result = inject_session_state("{first} {last}", &state);
89        assert_eq!(result, "Alice Smith");
90    }
91
92    #[test]
93    fn prefix_key() {
94        let state = State::new();
95        state.app().set("flag", true);
96        let result = inject_session_state("Flag is {app:flag}", &state);
97        assert_eq!(result, "Flag is true");
98    }
99
100    #[test]
101    fn no_placeholders_passthrough() {
102        let state = State::new();
103        let template = "No placeholders here.";
104        assert_eq!(inject_session_state(template, &state), template);
105    }
106
107    #[test]
108    fn numeric_value() {
109        let state = State::new();
110        state.set("count", 42);
111        let result = inject_session_state("Count: {count}", &state);
112        assert_eq!(result, "Count: 42");
113    }
114
115    #[test]
116    fn empty_template() {
117        let state = State::new();
118        assert_eq!(inject_session_state("", &state), "");
119    }
120}