Skip to main content

ares_tools/tools/
calculator.rs

1use crate::registry::Tool;
2use ares_types::Result;
3use async_trait::async_trait;
4use cordis::Service;
5use serde::{Deserialize, Serialize};
6use serde_json::{json, Value};
7
8/// Calculator tool for basic arithmetic operations.
9pub struct Calculator;
10
11#[async_trait]
12impl Tool for Calculator {
13    fn name(&self) -> &str {
14        "calculator"
15    }
16
17    fn description(&self) -> &str {
18        "Perform basic arithmetic operations"
19    }
20
21    fn parameters_schema(&self) -> Value {
22        json!({
23            "type": "object",
24            "properties": {
25                "operation": {
26                    "type": "string",
27                    "enum": ["add", "subtract", "multiply", "divide"]
28                },
29                "a": { "type": "number" },
30                "b": { "type": "number" }
31            },
32            "required": ["operation", "a", "b"]
33        })
34    }
35
36    async fn execute(&self, args: Value) -> Result<Value> {
37        let op = args["operation"].as_str().unwrap_or("add");
38        let a = args["a"].as_f64().unwrap_or(0.0);
39        let b = args["b"].as_f64().unwrap_or(0.0);
40
41        let result = match op {
42            "add" => a + b,
43            "subtract" => a - b,
44            "multiply" => a * b,
45            "divide" => a / b,
46            _ => 0.0,
47        };
48
49        Ok(json!({ "result": result }))
50    }
51}
52
53// Cordis Service wrapper, used for dependency injection via Context.
54
55/// Empty config for CalculatorService. Default suffices; Serialize and
56/// Deserialize are required for Plugin Config via RegistryService.
57#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
58pub struct CalculatorConfig;
59
60/// CalculatorService wraps Calculator for Cordis dependency injection.
61///
62/// Service provides the feature gate boundary. Tool implements the same
63/// arithmetic logic as Calculator without any behavior change.
64pub struct CalculatorService;
65
66impl CalculatorService {
67    /// Create with default config.
68    pub fn new() -> Self {
69        Self
70    }
71
72    /// Create from explicit config. Config is currently empty and kept
73    /// for Plugin compatibility.
74    pub fn with_config(_config: CalculatorConfig) -> Self {
75        Self
76    }
77}
78
79impl Default for CalculatorService {
80    fn default() -> Self {
81        Self::new()
82    }
83}
84
85impl Service for CalculatorService {
86    fn name(&self) -> &'static str {
87        "calculator"
88    }
89
90    fn init(&self, _ctx: &std::sync::Arc<cordis::Context>) -> cordis::ServiceInitFuture<'_> {
91        Box::pin(async move { Ok(None) })
92    }
93}
94
95#[async_trait]
96impl Tool for CalculatorService {
97    fn name(&self) -> &str {
98        "calculator"
99    }
100
101    fn description(&self) -> &str {
102        "Perform basic arithmetic operations"
103    }
104
105    fn parameters_schema(&self) -> Value {
106        json!({
107            "type": "object",
108            "properties": {
109                "operation": {
110                    "type": "string",
111                    "enum": ["add", "subtract", "multiply", "divide"]
112                },
113                "a": { "type": "number" },
114                "b": { "type": "number" }
115            },
116            "required": ["operation", "a", "b"]
117        })
118    }
119
120    async fn execute(&self, args: Value) -> Result<Value> {
121        let op = args["operation"].as_str().unwrap_or("add");
122        let a = args["a"].as_f64().unwrap_or(0.0);
123        let b = args["b"].as_f64().unwrap_or(0.0);
124
125        let result = match op {
126            "add" => a + b,
127            "subtract" => a - b,
128            "multiply" => a * b,
129            "divide" => a / b,
130            _ => 0.0,
131        };
132
133        Ok(json!({ "result": result }))
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use serde_json::json;
141
142    #[test]
143    fn test_name_and_description() {
144        let tool = Calculator;
145        assert_eq!(tool.name(), "calculator");
146        assert_eq!(tool.description(), "Perform basic arithmetic operations");
147    }
148
149    #[test]
150    fn test_parameters_schema() {
151        let tool = Calculator;
152        let schema = tool.parameters_schema();
153        assert_eq!(schema["type"], "object");
154        assert_eq!(
155            schema["properties"]["operation"]["enum"],
156            json!(["add", "subtract", "multiply", "divide"])
157        );
158        assert_eq!(schema["required"], json!(["operation", "a", "b"]));
159    }
160
161    #[tokio::test]
162    async fn test_add() {
163        let tool = Calculator;
164        let out = tool
165            .execute(json!({"operation": "add", "a": 10.0, "b": 3.5}))
166            .await
167            .unwrap();
168        assert_eq!(out["result"], json!(13.5));
169    }
170
171    #[tokio::test]
172    async fn test_subtract() {
173        let tool = Calculator;
174        let out = tool
175            .execute(json!({"operation": "subtract", "a": 10.0, "b": 3.0}))
176            .await
177            .unwrap();
178        assert_eq!(out["result"], json!(7.0));
179    }
180
181    #[tokio::test]
182    async fn test_multiply() {
183        let tool = Calculator;
184        let out = tool
185            .execute(json!({"operation": "multiply", "a": 4.0, "b": 2.5}))
186            .await
187            .unwrap();
188        assert_eq!(out["result"], json!(10.0));
189    }
190
191    #[tokio::test]
192    async fn test_divide() {
193        let tool = Calculator;
194        let out = tool
195            .execute(json!({"operation": "divide", "a": 10.0, "b": 4.0}))
196            .await
197            .unwrap();
198        assert_eq!(out["result"], json!(2.5));
199    }
200
201    #[tokio::test]
202    async fn test_unknown_operation_returns_zero() {
203        let tool = Calculator;
204        let out = tool
205            .execute(json!({"operation": "modulo", "a": 10.0, "b": 3.0}))
206            .await
207            .unwrap();
208        assert_eq!(out["result"], json!(0.0));
209    }
210
211    #[tokio::test]
212    async fn test_missing_operation_defaults_to_add() {
213        let tool = Calculator;
214        let out = tool.execute(json!({"a": 2.0, "b": 3.0})).await.unwrap();
215        assert_eq!(out["result"], json!(5.0));
216    }
217
218    #[tokio::test]
219    async fn test_missing_operands_default_to_zero() {
220        let tool = Calculator;
221        let out = tool.execute(json!({"operation": "add"})).await.unwrap();
222        assert_eq!(out["result"], json!(0.0));
223    }
224
225    #[tokio::test]
226    async fn test_divide_by_zero_serializes_as_null() {
227        let tool = Calculator;
228        let out = tool
229            .execute(json!({"operation": "divide", "a": 1.0, "b": 0.0}))
230            .await
231            .unwrap();
232        // serde_json cannot represent infinity; division by zero yields null.
233        assert!(out["result"].is_null());
234    }
235
236    // Verify Calculator is resolved through Tools on Context.
237
238    #[test]
239    fn test_calculator_service_via_context() {
240        use crate::registry::ToolRegistry;
241        use crate::Tools;
242        use cordis::Context;
243        use std::sync::Arc;
244
245        let mut registry = ToolRegistry::new();
246        registry.register(Arc::new(CalculatorService));
247        let tools = Arc::new(Tools::new(Arc::new(registry)));
248        let ctx = Context::new_root();
249        ctx.provide_arc(Arc::clone(&tools));
250
251        let resolved = ctx.get::<Tools>().expect("Tools should be in context");
252        let tool = resolved
253            .resolve(&ctx, "calculator")
254            .expect("calculator should resolve");
255        assert_eq!(tool.name(), "calculator");
256
257        let list = resolved.list(&ctx);
258        assert!(list.iter().any(|d| d.name == "calculator"));
259        assert!(resolved.resolve(&ctx, "unknown").is_none());
260
261        let isolated = ctx.isolate::<Tools>("tenant:acme");
262        assert!(resolved.resolve(&isolated, "calculator").is_some());
263        assert!(resolved
264            .list(&isolated)
265            .iter()
266            .any(|d| d.name == "calculator"));
267    }
268
269    #[test]
270    fn test_calculator_via_tools_list_resolve() {
271        use crate::registry::ToolRegistry;
272        use crate::Tools;
273        use cordis::Context;
274        use std::sync::Arc;
275
276        let mut registry = ToolRegistry::new();
277        registry.register(Arc::new(CalculatorService));
278        let svc = Tools::new(Arc::new(registry));
279        let ctx = Context::new_root().isolate::<Tools>("tenant:acme");
280        let tool = svc.resolve(&ctx, "calculator").unwrap();
281        assert_eq!(tool.name(), "calculator");
282        assert_eq!(tool.description(), "Perform basic arithmetic operations");
283        let defs = svc.list(&ctx);
284        assert_eq!(defs.len(), 1);
285        assert_eq!(defs[0].name, "calculator");
286    }
287}