Skip to main content

zai_rs/toolkits/
core.rs

1//! Core traits and concrete types for defining executable tools.
2
3use std::{borrow::Cow, collections::HashMap};
4
5#[cfg(feature = "toolkits")]
6use std::sync::{Arc, LazyLock};
7
8use crate::toolkits::error::{ToolResult, error_context};
9use async_trait::async_trait;
10use serde::Serialize;
11
12/// Compiled JSON-Schema validator used for tool-argument validation.
13///
14#[cfg(feature = "toolkits")]
15type CompiledSchema = Arc<jsonschema::Validator>;
16
17/// Type-erased tool trait for dynamic dispatch
18#[async_trait]
19pub trait DynTool: Send + Sync {
20    /// Get the tool's metadata
21    fn metadata(&self) -> &ToolMetadata;
22
23    /// Execute with JSON input/output
24    async fn execute_json(&self, input: serde_json::Value) -> ToolResult<serde_json::Value>;
25
26    /// Get input schema
27    fn input_schema(&self) -> serde_json::Value;
28
29    /// Get the tool name
30    fn name(&self) -> &str {
31        self.metadata().name()
32    }
33}
34
35/// Global schema cache for compiled JSON schemas.
36#[cfg(feature = "toolkits")]
37static SCHEMA_CACHE: LazyLock<std::sync::RwLock<HashMap<String, Arc<jsonschema::Validator>>>> =
38    LazyLock::new(|| std::sync::RwLock::new(HashMap::new()));
39
40/// Maximum number of compiled schemas to cache.
41#[cfg(feature = "toolkits")]
42const SCHEMA_CACHE_MAX_SIZE: usize = 256;
43
44/// Metadata used to identify, describe, and categorize a tool.
45#[derive(Debug, Clone, Serialize)]
46pub struct ToolMetadata {
47    /// Tool name (must be unique)
48    name: Cow<'static, str>,
49
50    /// Tool description
51    description: Cow<'static, str>,
52
53    /// Tool version
54    version: Cow<'static, str>,
55
56    /// Tool author
57    author: Option<Cow<'static, str>>,
58
59    /// Tool tags for categorization
60    tags: Vec<Cow<'static, str>>,
61
62    /// Whether the tool is enabled
63    enabled: bool,
64
65    /// Additional metadata
66    metadata: HashMap<Cow<'static, str>, serde_json::Value>,
67}
68
69impl ToolMetadata {
70    /// Create new metadata with validation
71    pub fn new(name: impl Into<String>, description: impl Into<String>) -> ToolResult<Self> {
72        let name = name.into();
73        let description = description.into();
74
75        validate_tool_name(&name)?;
76
77        Ok(Self {
78            name: Cow::Owned(name),
79            description: Cow::Owned(description),
80            version: Cow::Borrowed("1.0.0"),
81            author: None,
82            tags: Vec::new(),
83            enabled: true,
84            metadata: HashMap::new(),
85        })
86    }
87
88    /// Validated tool name.
89    pub fn name(&self) -> &str {
90        &self.name
91    }
92
93    /// Human-readable description presented to the model.
94    pub fn description(&self) -> &str {
95        &self.description
96    }
97
98    /// Tool implementation version.
99    pub fn version(&self) -> &str {
100        &self.version
101    }
102
103    /// Optional tool author.
104    pub fn author(&self) -> Option<&str> {
105        self.author.as_deref()
106    }
107
108    /// Category tags attached to the tool.
109    pub fn tags(&self) -> &[Cow<'static, str>] {
110        &self.tags
111    }
112
113    /// Whether the executor may run and export this tool.
114    pub fn is_enabled(&self) -> bool {
115        self.enabled
116    }
117
118    /// Additional application-defined metadata.
119    pub fn additional_metadata(&self) -> &HashMap<Cow<'static, str>, serde_json::Value> {
120        &self.metadata
121    }
122
123    /// Set the tool version.
124    pub fn with_version(mut self, version: impl Into<Cow<'static, str>>) -> Self {
125        self.version = version.into();
126        self
127    }
128
129    /// Set the tool author.
130    pub fn with_author(mut self, author: impl Into<Cow<'static, str>>) -> Self {
131        self.author = Some(author.into());
132        self
133    }
134
135    /// Set the tool's category tags.
136    pub fn with_tags<T: Into<Cow<'static, str>>>(
137        mut self,
138        tags: impl IntoIterator<Item = T>,
139    ) -> Self {
140        self.tags = tags.into_iter().map(Into::into).collect();
141        self
142    }
143
144    /// Enable or disable the tool.
145    pub fn with_enabled(mut self, enabled: bool) -> Self {
146        self.enabled = enabled;
147        self
148    }
149
150    /// Attach an arbitrary key/value metadata entry.
151    pub fn with_metadata(
152        mut self,
153        key: impl Into<Cow<'static, str>>,
154        value: serde_json::Value,
155    ) -> Self {
156        self.metadata.insert(key.into(), value);
157        self
158    }
159}
160
161pub(crate) fn validate_tool_name(name: &str) -> ToolResult<()> {
162    if name.trim().is_empty() {
163        return Err(error_context().invalid_parameters("Tool name cannot be empty"));
164    }
165    if name.len() > 64 {
166        return Err(error_context().invalid_parameters("Tool name cannot exceed 64 characters"));
167    }
168    if !name
169        .bytes()
170        .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
171    {
172        return Err(error_context().invalid_parameters(
173            "Tool name must contain only ASCII letters, digits, underscores, and hyphens",
174        ));
175    }
176    Ok(())
177}
178
179/// Helper functions for type conversions (avoiding orphan rule issues)
180pub mod conversions {
181    use crate::toolkits::error::{ToolResult, error_context};
182
183    /// Convert a value to JSON
184    pub fn to_json<T: serde::Serialize>(value: T) -> ToolResult<serde_json::Value> {
185        serde_json::to_value(value).map_err(|e| error_context().serialization_error(e))
186    }
187
188    /// Extract string from JSON value
189    pub fn from_json_string(value: serde_json::Value) -> ToolResult<String> {
190        match value {
191            serde_json::Value::String(s) => Ok(s),
192            _ => Err(error_context().invalid_parameters("Expected string value")),
193        }
194    }
195
196    /// Extract i32 from JSON value
197    pub fn from_json_i32(value: serde_json::Value) -> ToolResult<i32> {
198        match value {
199            serde_json::Value::Number(n) => n
200                .as_i64()
201                .and_then(|i| i.try_into().ok())
202                .ok_or_else(|| error_context().invalid_parameters("Expected i32 value")),
203            _ => Err(error_context().invalid_parameters("Expected number value")),
204        }
205    }
206
207    /// Extract f64 from JSON value
208    pub fn from_json_f64(value: serde_json::Value) -> ToolResult<f64> {
209        match value {
210            serde_json::Value::Number(n) => n
211                .as_f64()
212                .ok_or_else(|| error_context().invalid_parameters("Expected f64 value")),
213            _ => Err(error_context().invalid_parameters("Expected number value")),
214        }
215    }
216
217    /// Extract bool from JSON value
218    pub fn from_json_bool(value: serde_json::Value) -> ToolResult<bool> {
219        match value {
220            serde_json::Value::Bool(b) => Ok(b),
221            _ => Err(error_context().invalid_parameters("Expected boolean value")),
222        }
223    }
224}
225
226// -----------------------------
227// Single-struct dynamic FunctionTool
228// -----------------------------
229
230/// Shared, type-erased asynchronous handler used by registry-based tool
231/// loading.
232pub type ToolHandler = std::sync::Arc<
233    dyn Fn(
234            serde_json::Value,
235        ) -> std::pin::Pin<
236            Box<dyn std::future::Future<Output = ToolResult<serde_json::Value>> + Send>,
237        > + Send
238        + Sync,
239>;
240
241/// A single-struct tool that carries metadata, JSON schema, and an async
242/// handler
243pub struct FunctionTool {
244    metadata: ToolMetadata,
245    input_schema: serde_json::Value,
246    #[cfg(feature = "toolkits")]
247    compiled_schema: CompiledSchema,
248    handler: ToolHandler,
249}
250
251impl Clone for FunctionTool {
252    fn clone(&self) -> Self {
253        Self {
254            metadata: self.metadata.clone(),
255            input_schema: self.input_schema.clone(),
256            #[cfg(feature = "toolkits")]
257            compiled_schema: Arc::clone(&self.compiled_schema),
258            handler: self.handler.clone(),
259        }
260    }
261}
262
263impl FunctionTool {
264    /// Start building a [`FunctionTool`] with the given name and description.
265    pub fn builder(name: impl Into<String>, description: impl Into<String>) -> FunctionToolBuilder {
266        FunctionToolBuilder::new(name, description)
267    }
268    /// Convenience: build a FunctionTool directly from a full JSON schema and a
269    /// handler
270    pub fn from_schema<F, Fut>(
271        name: impl Into<String>,
272        description: impl Into<String>,
273        schema: serde_json::Value,
274        f: F,
275    ) -> ToolResult<FunctionTool>
276    where
277        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
278        Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
279    {
280        Self::builder(name, description)
281            .schema(schema)
282            .handler(f)
283            .build()
284    }
285    /// Build a FunctionTool from a full JSON spec (supports two shapes):
286    /// 1) {"name":..., "description":..., "parameters": {...}}
287    /// 2) {"type":"function", "function": {"name":..., "description":...,
288    ///    "parameters": {...}}}
289    pub fn from_function_spec<F, Fut>(spec: serde_json::Value, f: F) -> ToolResult<FunctionTool>
290    where
291        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
292        Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
293    {
294        let (name, description, parameters) = parse_function_spec_details(&spec)?;
295        let mut builder = Self::builder(name, description);
296        if let Some(p) = parameters {
297            builder = builder.schema(p);
298        }
299        builder.handler(f).build()
300    }
301
302    /// Read a JSON function spec from a file and build a FunctionTool.
303    pub fn from_function_spec_file<F, Fut>(
304        path: impl AsRef<std::path::Path>,
305        f: F,
306    ) -> ToolResult<FunctionTool>
307    where
308        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
309        Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
310    {
311        let content = std::fs::read_to_string(path).map_err(|e| {
312            error_context().invalid_parameters(format!("Failed to read spec file: {e}"))
313        })?;
314        let spec: serde_json::Value = serde_json::from_str(&content)
315            .map_err(|e| error_context().invalid_parameters(format!("Invalid JSON: {e}")))?;
316        Self::from_function_spec(spec, f)
317    }
318}
319
320#[cfg(feature = "toolkits")]
321/// Compile JSON schema with caching for better performance
322fn compile_schema_cached(
323    schema: &serde_json::Value,
324    tool_name: &str,
325) -> ToolResult<Arc<jsonschema::Validator>> {
326    // The canonical schema itself is the cache key. A short hash could collide
327    // and reuse the wrong validator, which would change validation semantics.
328    let cache_key = schema.to_string();
329
330    // Check cache first
331    {
332        // Recover from a poisoned lock (a prior panic while holding it) by
333        // taking the inner guard rather than panicking here.
334        let cache = SCHEMA_CACHE
335            .read()
336            .unwrap_or_else(std::sync::PoisonError::into_inner);
337        if let Some(cached) = cache.get(&cache_key) {
338            return Ok(Arc::clone(cached));
339        }
340    }
341
342    // Compile and cache
343    let validator = jsonschema::validator_for(schema).map_err(|e| {
344        error_context()
345            .with_tool(tool_name)
346            .schema_validation(format!("Failed to compile schema: {e}"))
347    })?;
348
349    let validator = Arc::new(validator);
350
351    {
352        let mut cache = SCHEMA_CACHE
353            .write()
354            .unwrap_or_else(std::sync::PoisonError::into_inner);
355        // Evict a slice of entries if cache is full
356        if cache.len() >= SCHEMA_CACHE_MAX_SIZE {
357            // Remove ~10% of entries to make room. `HashMap` iteration order is
358            // unspecified, so the evicted slice is arbitrary rather than the
359            // oldest — acceptable for a bounded, registration-time cache.
360            let remove_count = (SCHEMA_CACHE_MAX_SIZE / 10).max(1);
361            let keys: Vec<String> = cache.keys().take(remove_count).cloned().collect();
362            for k in keys {
363                cache.remove(&k);
364            }
365        }
366        cache.insert(cache_key, Arc::clone(&validator));
367    }
368
369    Ok(validator)
370}
371
372/// (internal) Parses the name, description, and parameters from a JSON function
373/// spec.
374pub(crate) fn parse_function_spec_details(
375    spec: &serde_json::Value,
376) -> ToolResult<(String, String, Option<serde_json::Value>)> {
377    use serde_json::Value;
378    let obj = match spec {
379        Value::Object(map) => map,
380        _ => return Err(error_context().invalid_parameters("Function spec must be a JSON object")),
381    };
382    // Shape 2 with outer {type:function, function:{...}}
383    let (name, desc, params) = if obj.get("type").and_then(|v| v.as_str()) == Some("function") {
384        let f = obj
385            .get("function")
386            .and_then(|v| v.as_object())
387            .ok_or_else(|| error_context().invalid_parameters("Missing 'function' object"))?;
388        let name = f
389            .get("name")
390            .and_then(|v| v.as_str())
391            .ok_or_else(|| error_context().invalid_parameters("Missing function.name"))?
392            .to_string();
393        let desc = f
394            .get("description")
395            .and_then(|v| v.as_str())
396            .unwrap_or("")
397            .to_string();
398        let params = f.get("parameters").cloned();
399        (name, desc, params)
400    } else {
401        // Shape 1 inner {name, description, parameters}
402        let name = obj
403            .get("name")
404            .and_then(|v| v.as_str())
405            .ok_or_else(|| error_context().invalid_parameters("Missing name"))?
406            .to_string();
407        let desc = obj
408            .get("description")
409            .and_then(|v| v.as_str())
410            .unwrap_or("")
411            .to_string();
412        let params = obj.get("parameters").cloned();
413        (name, desc, params)
414    };
415    Ok((name, desc, params))
416}
417
418/// Builder for FunctionTool
419pub struct FunctionToolBuilder {
420    metadata: ToolMetadata,
421    input_schema: Option<serde_json::Value>,
422    /// Schema fragments accumulated by the fluent property API.
423    staged_properties: Option<serde_json::Map<String, serde_json::Value>>,
424    staged_required: Vec<String>,
425    handler: Option<ToolHandler>,
426}
427
428impl FunctionToolBuilder {
429    /// Create a new builder for a tool with the given name and description.
430    pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
431        // Preserve invalid input until `build`: silently renaming an invalid
432        // tool to `unknown` hides configuration errors and creates collisions.
433        let metadata = ToolMetadata {
434            name: Cow::Owned(name.into()),
435            description: Cow::Owned(description.into()),
436            version: Cow::Borrowed("1.0.0"),
437            author: None,
438            tags: Vec::new(),
439            enabled: true,
440            metadata: HashMap::new(),
441        };
442        Self {
443            metadata,
444            input_schema: None,
445            staged_properties: None,
446            staged_required: Vec::new(),
447            handler: None,
448        }
449    }
450
451    /// Provide the full input JSON schema directly.
452    pub fn schema(mut self, schema: serde_json::Value) -> Self {
453        self.input_schema = Some(schema);
454        self
455    }
456
457    /// Mutate the tool's metadata (e.g. version, tags) via a closure.
458    pub fn metadata(mut self, f: impl FnOnce(ToolMetadata) -> ToolMetadata) -> Self {
459        self.metadata = f(self.metadata);
460        self
461    }
462
463    /// Set the async handler invoked on each (validated) call.
464    pub fn handler<F, Fut>(mut self, f: F) -> Self
465    where
466        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
467        Fut: std::future::Future<Output = ToolResult<serde_json::Value>> + Send + 'static,
468    {
469        let wrapped = move |args: serde_json::Value| -> std::pin::Pin<
470            Box<dyn std::future::Future<Output = ToolResult<serde_json::Value>> + Send>,
471        > { Box::pin(f(args)) };
472        self.handler = Some(std::sync::Arc::new(wrapped));
473        self
474    }
475
476    /// Chain API: add one property to the schema. If `schema(json!(...))` is
477    /// also provided, the property will be merged into its `properties`
478    /// object.
479    pub fn property(mut self, name: impl Into<String>, schema: serde_json::Value) -> Self {
480        let name = name.into();
481        let entry = self
482            .staged_properties
483            .get_or_insert_with(serde_json::Map::new);
484        entry.insert(name, schema);
485        self
486    }
487
488    /// Chain API: mark a property as required. Will be merged with any provided
489    /// schema's `required`.
490    pub fn required(mut self, name: impl Into<String>) -> Self {
491        self.staged_required.push(name.into());
492        self
493    }
494
495    /// Finalize the tool: validates the handler is set, compiles the schema,
496    /// and returns the built [`FunctionTool`].
497    pub fn build(mut self) -> ToolResult<FunctionTool> {
498        validate_tool_name(&self.metadata.name)?;
499        let handler = self
500            .handler
501            .ok_or_else(|| error_context().invalid_parameters("FunctionTool handler not set"))?;
502        let mut schema = self
503            .input_schema
504            .take()
505            .unwrap_or_else(|| serde_json::json!({}));
506
507        if let serde_json::Value::Object(ref mut obj) = schema {
508            if obj
509                .get("type")
510                .is_some_and(|schema_type| schema_type.as_str() != Some("object"))
511            {
512                return Err(error_context()
513                    .with_tool(self.metadata.name.clone())
514                    .invalid_parameters("tool input schema type must be 'object'"));
515            }
516            obj.entry("type")
517                .or_insert(serde_json::Value::String("object".to_string()));
518            obj.entry("additionalProperties")
519                .or_insert(serde_json::Value::Bool(false));
520
521            if let Some(staged) = self.staged_properties.take() {
522                let props = obj
523                    .entry("properties")
524                    .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
525                let props = props.as_object_mut().ok_or_else(|| {
526                    error_context()
527                        .with_tool(self.metadata.name.clone())
528                        .invalid_parameters("schema.properties must be an object")
529                })?;
530                for (name, property_schema) in staged {
531                    props.insert(name, property_schema);
532                }
533            }
534
535            if !self.staged_required.is_empty() {
536                use std::collections::BTreeSet;
537                let mut required = BTreeSet::new();
538                if let Some(existing) = obj.get("required") {
539                    let entries = existing.as_array().ok_or_else(|| {
540                        error_context()
541                            .with_tool(self.metadata.name.clone())
542                            .invalid_parameters("schema.required must be an array of strings")
543                    })?;
544                    for entry in entries {
545                        let name = entry.as_str().ok_or_else(|| {
546                            error_context()
547                                .with_tool(self.metadata.name.clone())
548                                .invalid_parameters("schema.required must contain only strings")
549                        })?;
550                        required.insert(name.to_string());
551                    }
552                }
553                required.extend(self.staged_required);
554                obj.insert(
555                    "required".to_string(),
556                    serde_json::Value::Array(
557                        required
558                            .into_iter()
559                            .map(serde_json::Value::String)
560                            .collect(),
561                    ),
562                );
563            }
564            obj.entry("properties")
565                .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
566        } else {
567            return Err(error_context()
568                .with_tool(self.metadata.name.clone())
569                .invalid_parameters("tool input schema must be a JSON object"));
570        }
571
572        #[cfg(feature = "toolkits")]
573        let compiled_schema: CompiledSchema = compile_schema_cached(&schema, &self.metadata.name)?;
574        Ok(FunctionTool {
575            metadata: self.metadata,
576            input_schema: schema,
577            #[cfg(feature = "toolkits")]
578            compiled_schema,
579            handler,
580        })
581    }
582}
583
584#[async_trait]
585impl DynTool for FunctionTool {
586    fn metadata(&self) -> &ToolMetadata {
587        &self.metadata
588    }
589
590    async fn execute_json(&self, input: serde_json::Value) -> ToolResult<serde_json::Value> {
591        // Validate the input against the compiled schema (only when enabled).
592        #[cfg(feature = "toolkits")]
593        if let Err(validation_error) = self.compiled_schema.validate(&input) {
594            return Err(error_context()
595                .with_tool(self.name())
596                .invalid_parameters(format!("Input validation failed: {validation_error}")));
597        }
598
599        // If validation passes (or is disabled), execute the handler
600        (self.handler)(input).await
601    }
602
603    fn input_schema(&self) -> serde_json::Value {
604        self.input_schema.clone()
605    }
606}
607
608#[cfg(test)]
609mod tests {
610    use super::*;
611    use crate::toolkits::ToolError;
612
613    #[test]
614    fn test_tool_metadata_new() {
615        let metadata = ToolMetadata::new("test_tool", "A test tool").unwrap();
616        assert_eq!(metadata.name(), "test_tool");
617        assert_eq!(metadata.description(), "A test tool");
618        assert_eq!(metadata.version(), "1.0.0");
619        assert!(metadata.is_enabled());
620
621        let hyphenated = ToolMetadata::new("test-tool", "A test tool").unwrap();
622        assert_eq!(hyphenated.name(), "test-tool");
623    }
624
625    #[test]
626    fn test_tool_metadata_invalid_name_empty() {
627        let result = ToolMetadata::new("", "A test tool");
628        assert!(result.is_err());
629        match result.unwrap_err() {
630            ToolError::InvalidParameters { .. } => {},
631            _ => panic!("Expected InvalidParameters error"),
632        }
633    }
634
635    #[test]
636    fn test_tool_metadata_invalid_name_special_chars() {
637        let result = ToolMetadata::new("test-tool!", "A test tool");
638        assert!(result.is_err());
639        match result.unwrap_err() {
640            ToolError::InvalidParameters { .. } => {},
641            _ => panic!("Expected InvalidParameters error"),
642        }
643    }
644
645    #[test]
646    fn test_tool_metadata_builder() {
647        let metadata = ToolMetadata::new("test_tool", "A test tool")
648            .unwrap()
649            .with_version("2.0.0")
650            .with_author("Test Author")
651            .with_tags(["tag1", "tag2"])
652            .with_enabled(false);
653
654        assert_eq!(metadata.version(), "2.0.0");
655        assert_eq!(metadata.author(), Some("Test Author"));
656        assert_eq!(metadata.tags().len(), 2);
657        assert!(!metadata.is_enabled());
658    }
659
660    #[test]
661    fn test_conversions_to_json() {
662        let value = conversions::to_json(42).unwrap();
663        assert_eq!(value, 42);
664    }
665
666    #[test]
667    fn test_conversions_from_json_string() {
668        let value = serde_json::Value::String("hello".to_string());
669        let result = conversions::from_json_string(value).unwrap();
670        assert_eq!(result, "hello");
671    }
672
673    #[test]
674    fn test_conversions_from_json_string_invalid() {
675        let value = serde_json::Value::Number(42.into());
676        let result = conversions::from_json_string(value);
677        assert!(result.is_err());
678    }
679
680    #[test]
681    fn test_conversions_from_json_i32() {
682        let value = serde_json::Value::Number(42.into());
683        let result = conversions::from_json_i32(value).unwrap();
684        assert_eq!(result, 42);
685    }
686
687    #[test]
688    fn test_conversions_from_json_f64() {
689        let value = serde_json::json!(3.5);
690        let result = conversions::from_json_f64(value).unwrap();
691        assert_eq!(result, 3.5);
692    }
693
694    #[test]
695    fn test_conversions_from_json_bool() {
696        let value = serde_json::Value::Bool(true);
697        let result = conversions::from_json_bool(value).unwrap();
698        assert!(result);
699    }
700
701    #[test]
702    fn test_function_tool_builder() {
703        let tool = FunctionTool::builder("test_tool", "A test tool")
704            .property("param1", serde_json::json!({"type": "string"}))
705            .property("param2", serde_json::json!({"type": "number"}))
706            .required("param1")
707            .handler(|_args| async move { Ok(serde_json::json!({"result": "ok"})) })
708            .build();
709
710        assert!(tool.is_ok());
711        let tool = tool.unwrap();
712        assert_eq!(tool.name(), "test_tool");
713    }
714
715    #[test]
716    fn test_function_tool_clone() {
717        let tool1 = FunctionTool::builder("test_tool", "A test tool")
718            .property("param1", serde_json::json!({"type": "string"}))
719            .required("param1")
720            .handler(|_args| async move { Ok(serde_json::json!({"result": "ok"})) })
721            .build()
722            .unwrap();
723
724        let tool2 = tool1.clone();
725        assert_eq!(tool1.name(), tool2.name());
726        assert_eq!(tool1.input_schema(), tool2.input_schema());
727    }
728
729    #[test]
730    fn function_tool_rejects_non_object_input_schemas() {
731        for schema in [
732            serde_json::json!(true),
733            serde_json::json!({"type": "string"}),
734            serde_json::json!([{"type": "object"}]),
735        ] {
736            let result = FunctionTool::builder("invalid_schema", "test fixture")
737                .schema(schema)
738                .handler(|input| async move { Ok(input) })
739                .build();
740            assert!(matches!(result, Err(ToolError::InvalidParameters { .. })));
741        }
742    }
743
744    #[test]
745    fn test_parse_function_spec_shape1() {
746        let spec = serde_json::json!({
747            "name": "test_tool",
748            "description": "A test tool",
749            "parameters": {
750                "type": "object",
751                "properties": {
752                    "param1": {"type": "string"}
753                }
754            }
755        });
756
757        let (name, description, parameters) = parse_function_spec_details(&spec).unwrap();
758        assert_eq!(name, "test_tool");
759        assert_eq!(description, "A test tool");
760        assert!(parameters.is_some());
761    }
762
763    #[test]
764    fn test_parse_function_spec_shape2() {
765        let spec = serde_json::json!({
766            "type": "function",
767            "function": {
768                "name": "test_tool",
769                "description": "A test tool",
770                "parameters": {
771                    "type": "object",
772                    "properties": {
773                        "param1": {"type": "string"}
774                    }
775                }
776            }
777        });
778
779        let (name, description, parameters) = parse_function_spec_details(&spec).unwrap();
780        assert_eq!(name, "test_tool");
781        assert_eq!(description, "A test tool");
782        assert!(parameters.is_some());
783    }
784
785    #[test]
786    fn test_parse_function_spec_invalid() {
787        let spec = serde_json::Value::String("invalid".to_string());
788        let result = parse_function_spec_details(&spec);
789        assert!(result.is_err());
790    }
791}