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}