gemini_adk_fluent_rs/
flow_macros.rs

1//! Typed graph macros — compile-time-checked names for flow authoring.
2//!
3//! Flow/stage/tool/slot names are stringly by nature, which makes typos a runtime
4//! surprise. [`voice_flow!`](crate::voice_flow) generates a module of `&'static str`
5//! name constants so the names are declared once and every reference is checked at
6//! compile time — a typo'd `booking::collct` is a build error, not a silent
7//! never-matching guard.
8//!
9//! ```
10//! use gemini_adk_fluent_rs::voice_flow;
11//!
12//! voice_flow! {
13//!     mod booking {
14//!         steps: [collect, confirm, done];
15//!         tools: [book];
16//!         slots: [party_size];
17//!     }
18//! }
19//!
20//! assert_eq!(booking::collect, "collect");
21//! assert_eq!(booking::book, "book");
22//! assert_eq!(booking::party_size, "party_size");
23//! ```
24//!
25//! Use the constants throughout the [`Conversation`](crate::conversation::Conversation)
26//! builder (`.stage(booking::collect)`, `.commit(booking::book, ..)`,
27//! `Guard::captured([booking::party_size])`) so renaming a name is one edit and
28//! mistyping it never compiles. (A full declarative `voice_flow!` body — stages,
29//! transitions, and guards in macro syntax — is a planned follow-up; this is the
30//! name-checking core.)
31
32/// Generate a module of compile-time-checked flow name constants.
33///
34/// See the [module docs](crate::flow_macros) for usage.
35#[macro_export]
36macro_rules! voice_flow {
37    (
38        mod $module:ident {
39            $( steps: [ $($step:ident),* $(,)? ]; )?
40            $( tools: [ $($tool:ident),* $(,)? ]; )?
41            $( slots: [ $($slot:ident),* $(,)? ]; )?
42        }
43    ) => {
44        /// Compile-time-checked flow names generated by `voice_flow!`.
45        #[allow(non_upper_case_globals)]
46        pub mod $module {
47            $( $(
48                #[doc = concat!("Step id `", stringify!($step), "`.")]
49                pub const $step: &str = stringify!($step);
50            )* )?
51            $( $(
52                #[doc = concat!("Tool name `", stringify!($tool), "`.")]
53                pub const $tool: &str = stringify!($tool);
54            )* )?
55            $( $(
56                #[doc = concat!("Slot/state key `", stringify!($slot), "`.")]
57                pub const $slot: &str = stringify!($slot);
58            )* )?
59        }
60    };
61}
62
63#[cfg(test)]
64mod tests {
65    use crate::conversation::Conversation;
66    use gemini_adk_rs::flow::Guard;
67
68    voice_flow! {
69        mod booking {
70            steps: [collect, confirm, done];
71            tools: [book];
72            slots: [party_size];
73        }
74    }
75
76    #[test]
77    fn generated_constants_have_string_values() {
78        assert_eq!(booking::collect, "collect");
79        assert_eq!(booking::confirm, "confirm");
80        assert_eq!(booking::book, "book");
81        assert_eq!(booking::party_size, "party_size");
82    }
83
84    #[test]
85    fn names_drive_a_real_conversation() {
86        // Every name is the typed constant — a typo would not compile.
87        let convo = Conversation::new("booking")
88            .stage(booking::collect)
89            .collect([booking::party_size])
90            .next(booking::confirm, Guard::captured([booking::party_size]))
91            .stage(booking::confirm)
92            .commit(booking::book, Guard::is_true("user_confirmed"))
93            .next(booking::done, Guard::called_ok(booking::book))
94            .stage(booking::done)
95            .terminal()
96            .require([booking::done])
97            .compile()
98            .expect("compiles");
99        assert!(convo.flow().tool_policy().tools.contains(booking::book));
100    }
101
102    // Sections are optional and order-independent.
103    voice_flow! {
104        mod tools_only {
105            tools: [transfer];
106        }
107    }
108
109    #[test]
110    fn optional_sections() {
111        assert_eq!(tools_only::transfer, "transfer");
112    }
113}