1use std::time::{SystemTime, UNIX_EPOCH};
12
13#[derive(Debug, Clone)]
15pub struct ToolResult {
16 pub success: bool,
17 pub output: String,
18 pub tool_name: String,
19}
20
21const NATIVE_EXECUTOR_TOOLS: &[&str] = &[
29 "Calculator",
30 "DateTimeTool",
31 "DocumentRenderer",
32 "DocumentReader",
34 "DocumentEditor",
35 "PDFExtractor",
39 "ImageTextExtractor",
40 "ImageAnalyzer",
41];
42
43pub fn dispatch(tool_name: &str, argument: &str) -> Option<ToolResult> {
48 match tool_name {
49 "Calculator" => Some(calculator_execute(argument)),
50 "DateTimeTool" => Some(datetime_execute(argument)),
51 "DocumentRenderer" => Some(document_render_execute(argument)),
55 "DocumentReader" => Some(document_read_execute(argument)),
58 "DocumentEditor" => Some(document_edit_execute(argument)),
61 "PDFExtractor" => Some(extraction_execute("pdf:text", argument)),
67 "ImageTextExtractor" => Some(extraction_execute("image:text", argument)),
68 "ImageAnalyzer" => Some(extraction_execute("image:description", argument)),
69 _ => None, }
71}
72
73pub fn dispatch_or_reject(tool_name: &str, argument: &str) -> Result<Option<ToolResult>, String> {
81 if let Some(r) = dispatch(tool_name, argument) {
82 return Ok(Some(r));
83 }
84 let declared = crate::stdlib::TOOLS.iter().any(|t| t.name == tool_name);
86 if declared && !NATIVE_EXECUTOR_TOOLS.contains(&tool_name) {
87 let has_provider = crate::stdlib::TOOLS
92 .iter()
93 .find(|t| t.name == tool_name)
94 .map(|t| !t.provider.is_empty() || t.requires_api_key)
95 .unwrap_or(false);
96 if !has_provider {
97 return Err(format!(
98 "tool '{tool_name}' is declared in the stdlib catalog but has no native \
99 implementation and no provider — refusing to silently hand the call to the \
100 model, which would fabricate its output. Implement the tool, give \
101 it a provider, or remove it from the catalog."
102 ));
103 }
104 }
105 Ok(None)
106}
107
108#[cfg(not(feature = "documents"))]
117fn document_render_execute(_argument: &str) -> ToolResult {
118 ToolResult {
119 success: false,
120 output: "DocumentRenderer: this build was compiled without the `documents` feature, so the OOXML engine is absent. Reinstall with: cargo install axon-lang --features documents"
121 .to_string(),
122 tool_name: "DocumentRenderer".to_string(),
123 }
124}
125
126#[cfg(feature = "documents")]
127fn document_render_execute(argument: &str) -> ToolResult {
128 use base64::Engine;
129 let name = "DocumentRenderer";
130 let parsed: serde_json::Value = match serde_json::from_str(argument) {
131 Ok(v) => v,
132 Err(e) => return err(name, format!("invalid render request JSON: {e}")),
133 };
134 let doc_val = match parsed.get("document") {
135 Some(d) => d.clone(),
136 None => return err(name, "render request missing `document`".into()),
137 };
138 let spec: crate::ooxml::DocumentSpec = match serde_json::from_value(doc_val) {
139 Ok(s) => s,
140 Err(e) => return err(name, format!("malformed document IR: {e}")),
141 };
142 let values: std::collections::BTreeMap<String, String> = parsed
143 .get("values")
144 .and_then(|v| serde_json::from_value(v.clone()).ok())
145 .unwrap_or_default();
146 match crate::ooxml::render(&spec, &values) {
147 Ok(out) => {
148 let b64 = base64::engine::general_purpose::STANDARD.encode(&out.bytes);
149 let descriptor = serde_json::json!({
150 "sha256": out.sha256_hex,
151 "content_type": out.content_type,
152 "extension": out.extension,
153 "size_bytes": out.bytes.len(),
154 "bytes_base64": b64,
155 });
156 ToolResult {
157 success: true,
158 output: descriptor.to_string(),
159 tool_name: name.to_string(),
160 }
161 }
162 Err(e) => err(name, e.to_string()),
163 }
164}
165
166fn extraction_execute(transform: &str, argument: &str) -> ToolResult {
177 use base64::Engine;
178 let name = match transform {
179 "pdf:text" => "PDFExtractor",
180 "image:text" => "ImageTextExtractor",
181 _ => "ImageAnalyzer",
182 };
183 let parsed: serde_json::Value = match serde_json::from_str(argument) {
184 Ok(v) => v,
185 Err(e) => return err(name, format!("invalid extraction request JSON: {e}")),
186 };
187 let bytes = match parsed.get("bytes_base64").and_then(|v| v.as_str()) {
188 Some(b64) => match base64::engine::general_purpose::STANDARD.decode(b64) {
189 Ok(b) => b,
190 Err(e) => return err(name, format!("bytes_base64 is not valid base64: {e}")),
191 },
192 None => return err(name, "extraction request missing `bytes_base64`".into()),
193 };
194 let str_field = |k: &str| parsed.get(k).and_then(|v| v.as_str()).map(|s| s.to_string());
195 let hint = crate::extraction::ExtractionHint {
196 format: str_field("format"),
197 transform: Some(transform.to_string()),
198 target_field: str_field("target_field"),
199 language: str_field("language"),
200 };
201 match crate::extraction::run_active(&bytes, &hint, &crate::extraction::ExtractionBounds::default())
202 {
203 Ok(result) => {
204 let spans: Vec<serde_json::Value> = result
205 .spans
206 .iter()
207 .map(|s| {
208 serde_json::json!({
209 "text": s.text,
210 "confidence": s.confidence,
211 "page": s.page,
212 "bbox": { "x": s.bbox.x, "y": s.bbox.y, "w": s.bbox.w, "h": s.bbox.h },
213 })
214 })
215 .collect();
216 let out = serde_json::json!({
217 "engine": result.engine,
218 "engine_version": result.engine_version,
219 "transform": transform,
220 "provenance": "inferred", "taint": "untrusted", "epistemic_ceiling": "believe", "mean_confidence": result.mean_confidence(),
224 "page_count": result.page_count(),
225 "spans": spans,
226 });
227 ToolResult { success: true, output: out.to_string(), tool_name: name.to_string() }
228 }
229 Err(e) => err(name, format!("{}: {}", e.slug(), e)),
231 }
232}
233
234fn err(tool: &str, msg: String) -> ToolResult {
235 ToolResult {
236 success: false,
237 output: format!("DocumentRenderer: {msg}"),
238 tool_name: tool.to_string(),
239 }
240}
241
242#[cfg(not(feature = "documents"))]
251fn document_read_execute(_argument: &str) -> ToolResult {
252 ToolResult {
253 success: false,
254 output: "DocumentReader: this build was compiled without the `documents` feature, so the OOXML engine is absent. Reinstall with: cargo install axon-lang --features documents"
255 .to_string(),
256 tool_name: "DocumentReader".to_string(),
257 }
258}
259
260#[cfg(feature = "documents")]
261fn document_read_execute(argument: &str) -> ToolResult {
262 use base64::Engine;
263 let name = "DocumentReader";
264 let parsed: serde_json::Value = match serde_json::from_str(argument) {
265 Ok(v) => v,
266 Err(e) => return err_named(name, format!("invalid request JSON: {e}")),
267 };
268 let bytes: Vec<u8> = if let Some(b64) = parsed.get("bytes_base64").and_then(|v| v.as_str()) {
270 match base64::engine::general_purpose::STANDARD.decode(b64) {
271 Ok(b) => b,
272 Err(e) => return err_named(name, format!("invalid base64: {e}")),
273 }
274 } else if let Some(path) = parsed.get("path").and_then(|v| v.as_str()) {
275 let roots: Vec<std::path::PathBuf> = parsed
276 .get("roots")
277 .and_then(|v| v.as_array())
278 .map(|a| a.iter().filter_map(|x| x.as_str().map(std::path::PathBuf::from)).collect())
279 .unwrap_or_default();
280 let sandbox = crate::fs_sandbox::PathSandbox::new(roots);
281 match sandbox.resolve(path) {
282 Ok(resolved) => match std::fs::read(&resolved) {
283 Ok(b) => b,
284 Err(e) => return err_named(name, format!("read failed: {e}")),
285 },
286 Err(e) => return err_named(name, e.to_string()),
287 }
288 } else {
289 return err_named(name, "request needs `bytes_base64` or `path`".into());
290 };
291
292 match crate::ooxml_read::read_ooxml(&bytes, &crate::ooxml_read::IngestBounds::default()) {
293 Ok(doc) => {
294 let text: Vec<serde_json::Value> = doc
295 .text
296 .iter()
297 .map(|r| serde_json::json!({ "part": r.part, "text": r.text }))
298 .collect();
299 let out = serde_json::json!({
300 "format": doc.format,
301 "taint": doc.taint.as_str(),
303 "provenance": doc.provenance.as_str(),
304 "epistemic_ceiling": doc.provenance.epistemic_ceiling(),
305 "text": text,
306 "full_text": doc.full_text(),
307 "part_hashes": doc.part_hashes,
308 });
309 ToolResult { success: true, output: out.to_string(), tool_name: name.to_string() }
310 }
311 Err(e) => err_named(name, e.to_string()),
312 }
313}
314
315#[cfg(not(feature = "documents"))]
324fn document_edit_execute(_argument: &str) -> ToolResult {
325 ToolResult {
326 success: false,
327 output: "DocumentEditor: this build was compiled without the `documents` feature, so the OOXML engine is absent. Reinstall with: cargo install axon-lang --features documents"
328 .to_string(),
329 tool_name: "DocumentEditor".to_string(),
330 }
331}
332
333#[cfg(feature = "documents")]
334fn document_edit_execute(argument: &str) -> ToolResult {
335 use base64::Engine;
336 let name = "DocumentEditor";
337 let parsed: serde_json::Value = match serde_json::from_str(argument) {
338 Ok(v) => v,
339 Err(e) => return err_named(name, format!("invalid request JSON: {e}")),
340 };
341 let b64 = match parsed.get("bytes_base64").and_then(|v| v.as_str()) {
342 Some(b) => b,
343 None => return err_named(name, "request needs `bytes_base64`".into()),
344 };
345 let bytes = match base64::engine::general_purpose::STANDARD.decode(b64) {
346 Ok(b) => b,
347 Err(e) => return err_named(name, format!("invalid base64: {e}")),
348 };
349 let doc = match crate::ooxml_read::read_ooxml(&bytes, &crate::ooxml_read::IngestBounds::default()) {
350 Ok(d) => d,
351 Err(e) => return err_named(name, format!("read failed: {e}")),
352 };
353 let mut edits = Vec::new();
355 for e in parsed.get("edits").and_then(|v| v.as_array()).cloned().unwrap_or_default() {
356 let part = e.get("part").and_then(|v| v.as_str()).unwrap_or("").to_string();
357 match e.get("kind").and_then(|v| v.as_str()) {
358 Some("replace_text") => edits.push(crate::ooxml_edit::PartEdit::ReplaceText {
359 part,
360 find: e.get("find").and_then(|v| v.as_str()).unwrap_or("").to_string(),
361 replace: e.get("replace").and_then(|v| v.as_str()).unwrap_or("").to_string(),
362 }),
363 Some("replace") => {
364 let nb = e
365 .get("new_bytes_base64")
366 .and_then(|v| v.as_str())
367 .and_then(|s| base64::engine::general_purpose::STANDARD.decode(s).ok())
368 .unwrap_or_default();
369 edits.push(crate::ooxml_edit::PartEdit::Replace { part, new_bytes: nb });
370 }
371 other => return err_named(name, format!("unknown edit kind '{}'", other.unwrap_or("<none>"))),
372 }
373 }
374 match crate::ooxml_edit::edit_document(&doc, &edits) {
375 Ok(out) => {
376 let manifest: Vec<serde_json::Value> = out
377 .manifest
378 .iter()
379 .map(|m| serde_json::json!({
380 "part": m.part, "before": m.before_sha256, "after": m.after_sha256, "touched": m.touched,
381 }))
382 .collect();
383 let result = serde_json::json!({
384 "sha256": out.sha256_hex,
385 "taint": out.taint.as_str(),
387 "touched_parts": out.touched_parts(),
388 "manifest": manifest,
389 "bytes_base64": base64::engine::general_purpose::STANDARD.encode(&out.bytes),
390 });
391 ToolResult { success: true, output: result.to_string(), tool_name: name.to_string() }
392 }
393 Err(e) => err_named(name, e.to_string()),
394 }
395}
396
397#[cfg_attr(not(feature = "postgres"), allow(dead_code))]
398fn err_named(tool: &str, msg: String) -> ToolResult {
399 ToolResult { success: false, output: format!("{tool}: {msg}"), tool_name: tool.to_string() }
400}
401
402pub fn calculator_execute(expression: &str) -> ToolResult {
410 let expr = expression.trim();
411 if expr.is_empty() {
412 return ToolResult {
413 success: false,
414 output: "Empty expression".to_string(),
415 tool_name: "Calculator".to_string(),
416 };
417 }
418
419 match eval_expr(expr) {
420 Ok(val) => {
421 let formatted = if val.fract() == 0.0 && val.abs() < 1e15 {
423 format!("{}", val as i64)
424 } else {
425 format!("{}", val)
426 };
427 ToolResult {
428 success: true,
429 output: formatted,
430 tool_name: "Calculator".to_string(),
431 }
432 }
433 Err(e) => ToolResult {
434 success: false,
435 output: format!("Calculator error: {e}"),
436 tool_name: "Calculator".to_string(),
437 },
438 }
439}
440
441struct CalcParser<'a> {
444 input: &'a [u8],
445 pos: usize,
446}
447
448impl<'a> CalcParser<'a> {
449 fn new(input: &'a str) -> Self {
450 Self {
451 input: input.as_bytes(),
452 pos: 0,
453 }
454 }
455
456 fn skip_ws(&mut self) {
457 while self.pos < self.input.len() && self.input[self.pos].is_ascii_whitespace() {
458 self.pos += 1;
459 }
460 }
461
462 fn peek(&mut self) -> Option<u8> {
463 self.skip_ws();
464 self.input.get(self.pos).copied()
465 }
466
467 fn consume(&mut self, expected: u8) -> bool {
468 self.skip_ws();
469 if self.pos < self.input.len() && self.input[self.pos] == expected {
470 self.pos += 1;
471 true
472 } else {
473 false
474 }
475 }
476
477 fn parse_expr(&mut self) -> Result<f64, String> {
479 let mut result = self.parse_term()?;
480 loop {
481 self.skip_ws();
482 if self.consume(b'+') {
483 result += self.parse_term()?;
484 } else if self.consume(b'-') {
485 result -= self.parse_term()?;
486 } else {
487 break;
488 }
489 }
490 Ok(result)
491 }
492
493 fn parse_term(&mut self) -> Result<f64, String> {
495 let mut result = self.parse_power()?;
496 loop {
497 self.skip_ws();
498 if self.consume(b'*') {
499 if self.consume(b'*') {
500 self.pos -= 2;
502 break;
503 }
504 result *= self.parse_power()?;
505 } else if self.consume(b'/') {
506 let divisor = self.parse_power()?;
507 if divisor == 0.0 {
508 return Err("Division by zero".to_string());
509 }
510 result /= divisor;
511 } else if self.consume(b'%') {
512 let modulus = self.parse_power()?;
513 if modulus == 0.0 {
514 return Err("Modulo by zero".to_string());
515 }
516 result %= modulus;
517 } else {
518 break;
519 }
520 }
521 Ok(result)
522 }
523
524 fn parse_power(&mut self) -> Result<f64, String> {
526 let base = self.parse_unary()?;
527 self.skip_ws();
528 if self.pos + 1 < self.input.len()
529 && self.input[self.pos] == b'*'
530 && self.input[self.pos + 1] == b'*'
531 {
532 self.pos += 2;
533 let exp = self.parse_power()?; Ok(base.powf(exp))
535 } else {
536 Ok(base)
537 }
538 }
539
540 fn parse_unary(&mut self) -> Result<f64, String> {
542 self.skip_ws();
543 if self.consume(b'-') {
544 Ok(-self.parse_unary()?)
545 } else if self.consume(b'+') {
546 self.parse_unary()
547 } else {
548 self.parse_atom()
549 }
550 }
551
552 fn parse_atom(&mut self) -> Result<f64, String> {
554 self.skip_ws();
555
556 if self.consume(b'(') {
558 let val = self.parse_expr()?;
559 if !self.consume(b')') {
560 return Err("Missing closing parenthesis".to_string());
561 }
562 return Ok(val);
563 }
564
565 if self.pos < self.input.len()
567 && (self.input[self.pos].is_ascii_digit() || self.input[self.pos] == b'.')
568 {
569 return self.parse_number();
570 }
571
572 if self.pos < self.input.len() && self.input[self.pos].is_ascii_alphabetic() {
574 let name = self.parse_ident();
575 return self.resolve_ident(&name);
576 }
577
578 Err(format!(
579 "Unexpected character at position {}",
580 self.pos
581 ))
582 }
583
584 fn parse_number(&mut self) -> Result<f64, String> {
585 let start = self.pos;
586 while self.pos < self.input.len()
587 && (self.input[self.pos].is_ascii_digit() || self.input[self.pos] == b'.')
588 {
589 self.pos += 1;
590 }
591 if self.pos < self.input.len()
593 && (self.input[self.pos] == b'e' || self.input[self.pos] == b'E')
594 {
595 self.pos += 1;
596 if self.pos < self.input.len()
597 && (self.input[self.pos] == b'+' || self.input[self.pos] == b'-')
598 {
599 self.pos += 1;
600 }
601 while self.pos < self.input.len() && self.input[self.pos].is_ascii_digit() {
602 self.pos += 1;
603 }
604 }
605 let s = std::str::from_utf8(&self.input[start..self.pos])
606 .map_err(|_| "Invalid UTF-8 in number")?;
607 s.parse::<f64>()
608 .map_err(|_| format!("Invalid number: '{s}'"))
609 }
610
611 fn parse_ident(&mut self) -> String {
612 let start = self.pos;
613 while self.pos < self.input.len()
614 && (self.input[self.pos].is_ascii_alphanumeric() || self.input[self.pos] == b'_')
615 {
616 self.pos += 1;
617 }
618 String::from_utf8_lossy(&self.input[start..self.pos]).to_string()
619 }
620
621 fn resolve_ident(&mut self, name: &str) -> Result<f64, String> {
622 match name {
624 "pi" | "PI" => return Ok(std::f64::consts::PI),
625 "e" | "E" => return Ok(std::f64::consts::E),
626 "tau" | "TAU" => return Ok(std::f64::consts::TAU),
627 "inf" => return Ok(f64::INFINITY),
628 _ => {}
629 }
630
631 self.skip_ws();
633 if !self.consume(b'(') {
634 return Err(format!("Unknown identifier: '{name}'"));
635 }
636
637 let args = self.parse_args()?;
638
639 if !self.consume(b')') {
640 return Err(format!("Missing ')' after {name}(...)"));
641 }
642
643 match (name, args.len()) {
644 ("sqrt", 1) => Ok(args[0].sqrt()),
645 ("abs", 1) => Ok(args[0].abs()),
646 ("round", 1) => Ok(args[0].round()),
647 ("ceil", 1) => Ok(args[0].ceil()),
648 ("floor", 1) => Ok(args[0].floor()),
649 ("sin", 1) => Ok(args[0].sin()),
650 ("cos", 1) => Ok(args[0].cos()),
651 ("tan", 1) => Ok(args[0].tan()),
652 ("asin", 1) => Ok(args[0].asin()),
653 ("acos", 1) => Ok(args[0].acos()),
654 ("atan", 1) => Ok(args[0].atan()),
655 ("log", 1) | ("log10", 1) => Ok(args[0].log10()),
656 ("ln", 1) => Ok(args[0].ln()),
657 ("log2", 1) => Ok(args[0].log2()),
658 ("exp", 1) => Ok(args[0].exp()),
659 ("pow", 2) => Ok(args[0].powf(args[1])),
660 ("min", 2) => Ok(args[0].min(args[1])),
661 ("max", 2) => Ok(args[0].max(args[1])),
662 ("atan2", 2) => Ok(args[0].atan2(args[1])),
663 _ => Err(format!("Unknown function: '{name}' with {} args", args.len())),
664 }
665 }
666
667 fn parse_args(&mut self) -> Result<Vec<f64>, String> {
668 let mut args = Vec::new();
669 self.skip_ws();
670 if self.peek() == Some(b')') {
671 return Ok(args);
672 }
673 args.push(self.parse_expr()?);
674 while self.consume(b',') {
675 args.push(self.parse_expr()?);
676 }
677 Ok(args)
678 }
679}
680
681fn eval_expr(expr: &str) -> Result<f64, String> {
682 let mut parser = CalcParser::new(expr);
683 let result = parser.parse_expr()?;
684 parser.skip_ws();
685 if parser.pos < parser.input.len() {
686 return Err(format!(
687 "Unexpected trailing characters at position {}",
688 parser.pos
689 ));
690 }
691 if result.is_nan() {
692 return Err("Result is NaN".to_string());
693 }
694 Ok(result)
695}
696
697pub fn datetime_execute(query: &str) -> ToolResult {
704 let query = query.trim().to_lowercase();
705
706 let now = SystemTime::now()
707 .duration_since(UNIX_EPOCH)
708 .unwrap_or_default();
709
710 let secs = now.as_secs();
711 let (year, month, day, hour, min, sec, weekday) = unix_to_utc(secs);
712
713 let output = match query.as_str() {
714 "now" | "iso" | "datetime" => format!(
715 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
716 year, month, day, hour, min, sec
717 ),
718 "today" | "date" => format!("{:04}-{:02}-{:02}", year, month, day),
719 "time" => format!("{:02}:{:02}:{:02}Z", hour, min, sec),
720 "timestamp" | "unix" | "epoch" => format!("{}", secs),
721 "year" => format!("{}", year),
722 "month" => format!("{}", month),
723 "day" => format!("{}", day),
724 "hour" => format!("{}", hour),
725 "minute" => format!("{}", min),
726 "second" => format!("{}", sec),
727 "weekday" => weekday_name(weekday).to_string(),
728 _ => format!(
729 "Unknown query '{}'. Supported: now, today, timestamp, year, month, day, weekday, iso, time, hour, minute, second",
730 query
731 ),
732 };
733
734 ToolResult {
735 success: true,
736 output,
737 tool_name: "DateTimeTool".to_string(),
738 }
739}
740
741fn unix_to_utc(secs: u64) -> (i32, u32, u32, u32, u32, u32, u32) {
744 let days = (secs / 86400) as i64;
745 let time_of_day = secs % 86400;
746
747 let hour = (time_of_day / 3600) as u32;
748 let min = ((time_of_day % 3600) / 60) as u32;
749 let sec = (time_of_day % 60) as u32;
750
751 let weekday = ((days + 4) % 7) as u32;
753
754 let z = days + 719468;
756 let era = if z >= 0 { z } else { z - 146096 } / 146097;
757 let doe = (z - era * 146097) as u32;
758 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
759 let y = (yoe as i64 + era * 400) as i32;
760 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
761 let mp = (5 * doy + 2) / 153;
762 let d = doy - (153 * mp + 2) / 5 + 1;
763 let m = if mp < 10 { mp + 3 } else { mp - 9 };
764 let year = if m <= 2 { y + 1 } else { y };
765
766 (year, m, d, hour, min, sec, weekday)
767}
768
769fn weekday_name(weekday: u32) -> &'static str {
770 match weekday {
771 0 => "Sunday",
772 1 => "Monday",
773 2 => "Tuesday",
774 3 => "Wednesday",
775 4 => "Thursday",
776 5 => "Friday",
777 6 => "Saturday",
778 _ => "Unknown",
779 }
780}
781
782#[cfg(test)]
785mod tests {
786 use super::*;
787
788 #[test]
791 fn calc_basic_arithmetic() {
792 assert_eq!(eval_expr("2 + 3").unwrap(), 5.0);
793 assert_eq!(eval_expr("10 - 4").unwrap(), 6.0);
794 assert_eq!(eval_expr("3 * 7").unwrap(), 21.0);
795 assert_eq!(eval_expr("20 / 4").unwrap(), 5.0);
796 }
797
798 #[test]
799 fn calc_operator_precedence() {
800 assert_eq!(eval_expr("2 + 3 * 4").unwrap(), 14.0);
801 assert_eq!(eval_expr("(2 + 3) * 4").unwrap(), 20.0);
802 }
803
804 #[test]
805 fn calc_power() {
806 assert_eq!(eval_expr("2 ** 10").unwrap(), 1024.0);
807 assert_eq!(eval_expr("3 ** 2").unwrap(), 9.0);
808 }
809
810 #[test]
811 fn calc_modulo() {
812 assert_eq!(eval_expr("17 % 5").unwrap(), 2.0);
813 }
814
815 #[test]
816 fn calc_unary_minus() {
817 assert_eq!(eval_expr("-5").unwrap(), -5.0);
818 assert_eq!(eval_expr("-3 + 7").unwrap(), 4.0);
819 assert_eq!(eval_expr("-(2 + 3)").unwrap(), -5.0);
820 }
821
822 #[test]
823 fn calc_constants() {
824 assert!((eval_expr("pi").unwrap() - std::f64::consts::PI).abs() < 1e-10);
825 assert!((eval_expr("e").unwrap() - std::f64::consts::E).abs() < 1e-10);
826 }
827
828 #[test]
829 fn calc_functions() {
830 assert_eq!(eval_expr("sqrt(16)").unwrap(), 4.0);
831 assert_eq!(eval_expr("abs(-5)").unwrap(), 5.0);
832 assert_eq!(eval_expr("round(3.7)").unwrap(), 4.0);
833 assert_eq!(eval_expr("ceil(3.2)").unwrap(), 4.0);
834 assert_eq!(eval_expr("floor(3.8)").unwrap(), 3.0);
835 assert_eq!(eval_expr("pow(2, 8)").unwrap(), 256.0);
836 assert_eq!(eval_expr("min(3, 7)").unwrap(), 3.0);
837 assert_eq!(eval_expr("max(3, 7)").unwrap(), 7.0);
838 }
839
840 #[test]
841 fn calc_trig() {
842 assert!((eval_expr("sin(0)").unwrap()).abs() < 1e-10);
843 assert!((eval_expr("cos(0)").unwrap() - 1.0).abs() < 1e-10);
844 }
845
846 #[test]
847 fn calc_logarithm() {
848 assert!((eval_expr("log(100)").unwrap() - 2.0).abs() < 1e-10);
849 assert!((eval_expr("ln(e)").unwrap() - 1.0).abs() < 1e-10);
850 }
851
852 #[test]
853 fn calc_nested() {
854 assert_eq!(eval_expr("sqrt(pow(3, 2) + pow(4, 2))").unwrap(), 5.0);
855 }
856
857 #[test]
858 fn calc_scientific_notation() {
859 assert_eq!(eval_expr("1e3").unwrap(), 1000.0);
860 assert_eq!(eval_expr("2.5e2").unwrap(), 250.0);
861 }
862
863 #[test]
864 fn calc_division_by_zero() {
865 assert!(eval_expr("1 / 0").is_err());
866 }
867
868 #[test]
869 fn calc_empty_expression() {
870 let r = calculator_execute("");
871 assert!(!r.success);
872 }
873
874 #[test]
875 fn calc_invalid_expression() {
876 assert!(eval_expr("2 +").is_err());
877 }
878
879 #[test]
880 fn calc_integer_output() {
881 let r = calculator_execute("2 + 3");
882 assert!(r.success);
883 assert_eq!(r.output, "5");
884 }
885
886 #[test]
887 fn calc_float_output() {
888 let r = calculator_execute("1 / 3");
889 assert!(r.success);
890 assert!(r.output.starts_with("0.333"));
891 }
892
893 #[test]
896 fn datetime_now_iso_format() {
897 let r = datetime_execute("now");
898 assert!(r.success);
899 assert!(r.output.contains('T'));
900 assert!(r.output.ends_with('Z'));
901 }
902
903 #[test]
904 fn datetime_today() {
905 let r = datetime_execute("today");
906 assert!(r.success);
907 assert_eq!(r.output.len(), 10); assert!(r.output.contains('-'));
909 }
910
911 #[test]
912 fn datetime_timestamp() {
913 let r = datetime_execute("timestamp");
914 assert!(r.success);
915 let ts: u64 = r.output.parse().expect("should be a number");
916 assert!(ts > 1700000000); }
918
919 #[test]
920 fn datetime_year() {
921 let r = datetime_execute("year");
922 assert!(r.success);
923 let y: i32 = r.output.parse().expect("should be a number");
924 assert!(y >= 2024);
925 }
926
927 #[test]
928 fn datetime_weekday() {
929 let r = datetime_execute("weekday");
930 assert!(r.success);
931 let valid = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
932 assert!(valid.contains(&r.output.as_str()));
933 }
934
935 #[test]
936 fn datetime_unknown_query() {
937 let r = datetime_execute("foobar");
938 assert!(r.success);
939 assert!(r.output.contains("Unknown query"));
940 }
941
942 #[test]
945 fn dispatch_calculator() {
946 let r = dispatch("Calculator", "2 + 2");
947 assert!(r.is_some());
948 let r = r.unwrap();
949 assert!(r.success);
950 assert_eq!(r.output, "4");
951 }
952
953 #[test]
954 fn dispatch_datetime() {
955 let r = dispatch("DateTimeTool", "now");
956 assert!(r.is_some());
957 assert!(r.unwrap().success);
958 }
959
960 #[test]
961 fn dispatch_unknown_tool() {
962 assert!(dispatch("WebSearch", "query").is_none());
963 }
964}