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}