helios_engine/
tool_macro.rs

1//! # Tool Macro Module
2//!
3//! This module provides macros to make tool creation as simple as possible.
4//! Just define your parameters and logic - everything else is automatic!
5
6/// Quick tool creation with auto-derived types.
7///
8/// This is the simplest way to create a tool - just provide the function signature
9/// and body, and everything else is handled automatically.
10///
11/// # Example
12///
13/// ```rust
14/// use helios_engine::quick_tool;
15///
16/// let tool = quick_tool! {
17///     name: calculate_volume,
18///     description: "Calculate the volume of a box",
19///     params: (width: f64, height: f64, depth: f64),
20///     execute: |width, height, depth| {
21///         format!("Volume: {} cubic meters", width * height * depth)
22///     }
23/// };
24/// ```
25#[macro_export]
26macro_rules! quick_tool {
27    (
28        name: $name:ident,
29        description: $desc:expr,
30        params: ($($param_name:ident: $param_type:tt),* $(,)?),
31        execute: |$($param_var:ident),*| $body:expr
32    ) => {
33        {
34            let param_str = vec![
35                $(
36                    format!(
37                        "{}:{}:{}",
38                        stringify!($param_name),
39                        stringify!($param_type),
40                        stringify!($param_name).replace('_', " ")
41                    )
42                ),*
43            ].join(", ");
44
45            $crate::ToolBuilder::from_fn(
46                stringify!($name),
47                $desc,
48                param_str,
49                |args| {
50                    $(
51                        let $param_var = $crate::quick_tool!(@extract args, stringify!($param_name), $param_type)?;
52                    )*
53                    let result = $body;
54                    Ok($crate::ToolResult::success(result))
55                }
56            ).build()
57        }
58    };
59
60    // Extract helpers - return errors for missing required parameters
61    (@extract $args:ident, $name:expr, i32) => {
62        $args.get($name)
63            .and_then(|v| v.as_i64())
64            .map(|v| v as i32)
65            .ok_or_else(|| $crate::error::HeliosError::ToolError(
66                format!("Missing or invalid required parameter '{}'", $name)
67            ))
68    };
69    (@extract $args:ident, $name:expr, i64) => {
70        $args.get($name)
71            .and_then(|v| v.as_i64())
72            .ok_or_else(|| $crate::error::HeliosError::ToolError(
73                format!("Missing or invalid required parameter '{}'", $name)
74            ))
75    };
76    (@extract $args:ident, $name:expr, u32) => {
77        $args.get($name)
78            .and_then(|v| v.as_u64())
79            .map(|v| v as u32)
80            .ok_or_else(|| $crate::error::HeliosError::ToolError(
81                format!("Missing or invalid required parameter '{}'", $name)
82            ))
83    };
84    (@extract $args:ident, $name:expr, u64) => {
85        $args.get($name)
86            .and_then(|v| v.as_u64())
87            .ok_or_else(|| $crate::error::HeliosError::ToolError(
88                format!("Missing or invalid required parameter '{}'", $name)
89            ))
90    };
91    (@extract $args:ident, $name:expr, f32) => {
92        $args.get($name)
93            .and_then(|v| v.as_f64())
94            .map(|v| v as f32)
95            .ok_or_else(|| $crate::error::HeliosError::ToolError(
96                format!("Missing or invalid required parameter '{}'", $name)
97            ))
98    };
99    (@extract $args:ident, $name:expr, f64) => {
100        $args.get($name)
101            .and_then(|v| v.as_f64())
102            .ok_or_else(|| $crate::error::HeliosError::ToolError(
103                format!("Missing or invalid required parameter '{}'", $name)
104            ))
105    };
106    (@extract $args:ident, $name:expr, bool) => {
107        $args.get($name)
108            .and_then(|v| v.as_bool())
109            .ok_or_else(|| $crate::error::HeliosError::ToolError(
110                format!("Missing or invalid required parameter '{}'", $name)
111            ))
112    };
113    (@extract $args:ident, $name:expr, String) => {
114        $args.get($name)
115            .and_then(|v| v.as_str())
116            .map(|s| s.to_string())
117            .ok_or_else(|| $crate::error::HeliosError::ToolError(
118                format!("Missing or invalid required parameter '{}'", $name)
119            ))
120    };
121}
122
123#[cfg(test)]
124mod tests {
125    use crate::quick_tool;
126    use serde_json::json;
127
128    #[test]
129    fn test_quick_tool_with_valid_parameters() {
130        let tool = quick_tool! {
131            name: test_add,
132            description: "Add two numbers",
133            params: (x: i32, y: i32),
134            execute: |x, y| {
135                format!("Result: {}", x + y)
136            }
137        };
138
139        assert_eq!(tool.name(), "test_add");
140        assert_eq!(tool.description(), "Add two numbers");
141    }
142
143    #[tokio::test]
144    async fn test_quick_tool_execution_with_valid_args() {
145        let tool = quick_tool! {
146            name: test_multiply,
147            description: "Multiply two numbers",
148            params: (a: i32, b: i32),
149            execute: |a, b| {
150                format!("{}", a * b)
151            }
152        };
153
154        let args = json!({"a": 5, "b": 3});
155        let result = tool.execute(args).await.unwrap();
156        assert!(result.success);
157        assert_eq!(result.output, "15");
158    }
159
160    #[tokio::test]
161    async fn test_quick_tool_missing_parameter_returns_error() {
162        let tool = quick_tool! {
163            name: test_divide,
164            description: "Divide two numbers",
165            params: (numerator: f64, denominator: f64),
166            execute: |num, den| {
167                format!("{}", num / den)
168            }
169        };
170
171        // Missing 'denominator' parameter
172        let args = json!({"numerator": 10.0});
173        let result = tool.execute(args).await;
174
175        assert!(result.is_err());
176        let error_msg = format!("{}", result.unwrap_err());
177        assert!(error_msg.contains("Missing or invalid required parameter"));
178    }
179
180    #[tokio::test]
181    async fn test_quick_tool_invalid_type_returns_error() {
182        let tool = quick_tool! {
183            name: test_type_check,
184            description: "Test type checking",
185            params: (value: i32),
186            execute: |v| {
187                format!("{}", v)
188            }
189        };
190
191        // Passing string instead of integer
192        let args = json!({"value": "not a number"});
193        let result = tool.execute(args).await;
194
195        assert!(result.is_err());
196        let error_msg = format!("{}", result.unwrap_err());
197        assert!(error_msg.contains("Missing or invalid required parameter"));
198    }
199
200    #[tokio::test]
201    async fn test_quick_tool_with_string_and_bool() {
202        let tool = quick_tool! {
203            name: test_greet,
204            description: "Greet someone",
205            params: (name: String, formal: bool),
206            execute: |name, formal| {
207                if formal {
208                    format!("Good day, {}", name)
209                } else {
210                    format!("Hey {}", name)
211                }
212            }
213        };
214
215        let args = json!({"name": "Alice", "formal": true});
216        let result = tool.execute(args).await.unwrap();
217        assert!(result.success);
218        assert_eq!(result.output, "Good day, Alice");
219
220        let args = json!({"name": "Bob", "formal": false});
221        let result = tool.execute(args).await.unwrap();
222        assert!(result.success);
223        assert_eq!(result.output, "Hey Bob");
224    }
225
226    #[tokio::test]
227    async fn test_quick_tool_prevents_division_by_zero_default() {
228        // This test verifies that missing parameters cause errors
229        // instead of defaulting to 0, which would cause division by zero
230        let tool = quick_tool! {
231            name: safe_divide,
232            description: "Safely divide two numbers",
233            params: (x: f64, y: f64),
234            execute: |x, y| {
235                format!("{}", x / y)
236            }
237        };
238
239        // Missing divisor should return error, not default to 0.0
240        let args = json!({"x": 10.0});
241        let result = tool.execute(args).await;
242
243        assert!(
244            result.is_err(),
245            "Should error on missing divisor, not default to 0"
246        );
247    }
248}