zeroclaw_tools/
report_template_tool.rs1use super::report_templates;
7use async_trait::async_trait;
8use serde_json::json;
9use std::collections::HashMap;
10use zeroclaw_api::tool::{Tool, ToolResult};
11
12pub struct ReportTemplateTool;
18
19impl ReportTemplateTool {
20 pub fn new() -> Self {
21 Self
22 }
23}
24
25impl Default for ReportTemplateTool {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31#[async_trait]
32impl Tool for ReportTemplateTool {
33 fn name(&self) -> &str {
34 "report_template"
35 }
36
37 fn description(&self) -> &str {
38 "Render a report template with custom variables. Supports weekly_status, sprint_review, risk_register, milestone_report in en/de/fr/it."
39 }
40
41 fn parameters_schema(&self) -> serde_json::Value {
42 json!({
43 "type": "object",
44 "properties": {
45 "template": {
46 "type": "string",
47 "enum": ["weekly_status", "sprint_review", "risk_register", "milestone_report"],
48 "description": "Template name"
49 },
50 "language": {
51 "type": "string",
52 "enum": ["en", "de", "fr", "it"],
53 "default": "en",
54 "description": "Language code"
55 },
56 "variables": {
57 "type": "object",
58 "description": "Map of placeholder names to values (e.g., {\"project_name\": \"Acme\"})"
59 }
60 },
61 "required": ["template", "variables"]
62 })
63 }
64
65 async fn execute(&self, params: serde_json::Value) -> anyhow::Result<ToolResult> {
66 let template = params
67 .get("template")
68 .and_then(|v| v.as_str())
69 .ok_or_else(|| {
70 ::zeroclaw_log::record!(
71 WARN,
72 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
73 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
74 .with_attrs(::serde_json::json!({"param": "template"})),
75 "report_template_tool: missing template parameter"
76 );
77 anyhow::Error::msg("missing template")
78 })?;
79
80 let language = params
81 .get("language")
82 .and_then(|v| v.as_str())
83 .unwrap_or("en");
84
85 let variables = params
86 .get("variables")
87 .and_then(|v| v.as_object())
88 .ok_or_else(|| {
89 ::zeroclaw_log::record!(
90 WARN,
91 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
92 .with_outcome(::zeroclaw_log::EventOutcome::Failure)
93 .with_attrs(::serde_json::json!({"param": "variables"})),
94 "report_template_tool: variables must be an object"
95 );
96 anyhow::Error::msg("variables must be object")
97 })?;
98
99 let var_map: HashMap<String, String> = variables
102 .iter()
103 .map(|(k, v)| {
104 let value_str = match v {
105 serde_json::Value::String(s) => s.clone(),
106 serde_json::Value::Number(n) => n.to_string(),
107 serde_json::Value::Bool(b) => b.to_string(),
108 serde_json::Value::Null
109 | serde_json::Value::Array(_)
110 | serde_json::Value::Object(_) => String::new(),
111 };
112 (k.clone(), value_str)
113 })
114 .collect();
115
116 let rendered = report_templates::render_template(template, language, &var_map)?;
117
118 Ok(ToolResult {
119 success: true,
120 output: rendered,
121 error: None,
122 })
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129
130 #[tokio::test]
131 async fn tool_name_is_report_template() {
132 let tool = ReportTemplateTool::new();
133 assert_eq!(tool.name(), "report_template");
134 }
135
136 #[tokio::test]
137 async fn tool_has_description() {
138 let tool = ReportTemplateTool::new();
139 assert!(!tool.description().is_empty());
140 }
141
142 #[tokio::test]
143 async fn tool_has_parameters_schema() {
144 let tool = ReportTemplateTool::new();
145 let schema = tool.parameters_schema();
146 assert!(schema.is_object());
147 assert!(schema["properties"].is_object());
148 assert!(schema["required"].is_array());
149 }
150
151 #[tokio::test]
152 async fn execute_renders_weekly_status() {
153 let tool = ReportTemplateTool::new();
154 let params = json!({
155 "template": "weekly_status",
156 "language": "en",
157 "variables": {
158 "project_name": "Test",
159 "period": "W1",
160 "completed": "Done",
161 "in_progress": "WIP",
162 "blocked": "None",
163 "next_steps": "Next"
164 }
165 });
166
167 let result = tool.execute(params).await.unwrap();
168 assert!(result.success);
169 assert!(result.output.contains("Project: Test"));
170 }
171
172 #[tokio::test]
173 async fn execute_defaults_to_english() {
174 let tool = ReportTemplateTool::new();
175 let params = json!({
176 "template": "weekly_status",
177 "variables": {
178 "project_name": "Test"
179 }
180 });
181
182 let result = tool.execute(params).await.unwrap();
183 assert!(result.success);
184 assert!(result.output.contains("## Summary"));
185 }
186
187 #[tokio::test]
188 async fn execute_fails_on_missing_template() {
189 let tool = ReportTemplateTool::new();
190 let params = json!({
191 "variables": {
192 "project_name": "Test"
193 }
194 });
195
196 let result = tool.execute(params).await;
197 assert!(result.is_err());
198 }
199
200 #[tokio::test]
201 async fn execute_fails_on_missing_variables() {
202 let tool = ReportTemplateTool::new();
203 let params = json!({
204 "template": "weekly_status"
205 });
206
207 let result = tool.execute(params).await;
208 assert!(result.is_err());
209 }
210
211 #[tokio::test]
212 async fn execute_fails_on_invalid_template() {
213 let tool = ReportTemplateTool::new();
214 let params = json!({
215 "template": "unknown",
216 "variables": {}
217 });
218
219 let result = tool.execute(params).await;
220 assert!(result.is_err());
221 }
222}