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        #[allow(
47            unreachable_pub,
48            reason = "exported macro: the module is public API in the caller's crate, and \
49                      whether it is reachable from that crate's root depends on where the \
50                      caller expands it"
51        )]
52        pub mod $module {
53            $( $(
54                #[doc = concat!("Step id `", stringify!($step), "`.")]
55                pub const $step: &str = stringify!($step);
56            )* )?
57            $( $(
58                #[doc = concat!("Tool name `", stringify!($tool), "`.")]
59                pub const $tool: &str = stringify!($tool);
60            )* )?
61            $( $(
62                #[doc = concat!("Slot/state key `", stringify!($slot), "`.")]
63                pub const $slot: &str = stringify!($slot);
64            )* )?
65        }
66    };
67}
68
69#[cfg(test)]
70mod tests {
71    use crate::conversation::Conversation;
72    use gemini_adk_rs::flow::Guard;
73
74    voice_flow! {
75        mod booking {
76            steps: [collect, confirm, done];
77            tools: [book];
78            slots: [party_size];
79        }
80    }
81
82    #[test]
83    fn generated_constants_have_string_values() {
84        assert_eq!(booking::collect, "collect");
85        assert_eq!(booking::confirm, "confirm");
86        assert_eq!(booking::book, "book");
87        assert_eq!(booking::party_size, "party_size");
88    }
89
90    #[test]
91    fn names_drive_a_real_conversation() {
92        // Every name is the typed constant — a typo would not compile.
93        let convo = Conversation::new("booking")
94            .stage(booking::collect)
95            .collect([booking::party_size])
96            .next(booking::confirm, Guard::captured([booking::party_size]))
97            .stage(booking::confirm)
98            .commit(booking::book, Guard::is_true("user_confirmed"))
99            .next(booking::done, Guard::called_ok(booking::book))
100            .stage(booking::done)
101            .terminal()
102            .require([booking::done])
103            .compile()
104            .expect("compiles");
105        assert!(convo.flow().tool_surface().tools.contains(booking::book));
106    }
107
108    // Sections are optional and order-independent.
109    voice_flow! {
110        mod tools_only {
111            tools: [transfer];
112        }
113    }
114
115    #[test]
116    fn optional_sections() {
117        assert_eq!(tools_only::transfer, "transfer");
118    }
119}