descry-tool-core 0.3.1

Core traits and types for descry-tool framework
Documentation
//! Compile-time tool registry using inventory
//!
//! Provides zero-cost tool registration at compile time.

use inventory::collect;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;

use crate::context::ToolContext;
use crate::error::ToolError;

/// Tool metadata (type-erased)
///
/// This struct is submitted to inventory by the `#[tool]` macro.
pub struct ToolMeta {
    /// Tool name
    pub name: &'static str,

    /// Tool description
    pub description: &'static str,

    /// Type-erased call function
    ///
    /// Takes `Arc<ToolContext>` and JSON value, returns future.
    pub call: fn(Arc<ToolContext>, serde_json::Value) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, ToolError>> + Send>>,

    /// Schema generator function (returns JSON Value for flexibility)
    pub schema: fn() -> &'static serde_json::Value,

    /// Examples accessor function
    pub examples: fn() -> &'static [(&'static str, &'static str)],
}

// Collect ToolMeta using inventory
collect!(ToolMeta);

/// Get all registered tools
///
/// Returns an iterator over all tools submitted via inventory.
///
/// # Examples
///
/// ```
/// let tools: Vec<_> = descry_tool_core::all_tools().collect();
/// println!("Available tools: {}", tools.len());
/// ```
pub fn all_tools() -> impl Iterator<Item = &'static ToolMeta> {
    inventory::iter::<ToolMeta>.into_iter()
}

/// Find tool by name
///
/// # Examples
///
/// ```ignore
/// let tool = descry_tool_core::find_tool("add");
/// assert!(tool.is_some());
/// ```
pub fn find_tool(name: &str) -> Option<&'static ToolMeta> {
    all_tools().find(|meta| meta.name == name)
}

/// Call tool by name
///
/// # Examples
///
/// ```ignore
/// use std::sync::Arc;
/// use descry_tool_core::{call_tool, ToolContext};
///
/// #[tokio::main]
/// async fn main() {
///     let ctx = Arc::new(ToolContext::new());
///     let result = call_tool("add", json!({"a": 1, "b": 2}), ctx).await.unwrap();
/// }
/// ```
pub async fn call_tool(
    name: &str,
    params: serde_json::Value,
    ctx: Arc<ToolContext>,
) -> Result<serde_json::Value, ToolError> {
    let meta = find_tool(name).ok_or_else(|| ToolError::not_found(name))?;
    (meta.call)(ctx, params).await
}

/// Get tool schema by name
pub fn get_tool_schema(name: &str) -> Option<&'static serde_json::Value> {
    find_tool(name).map(|meta| (meta.schema)())
}

/// Get tool examples by name
pub fn get_tool_examples(name: &str) -> Option<&'static [(&'static str, &'static str)]> {
    find_tool(name).map(|meta| (meta.examples)())
}

/// Check if tool exists
pub fn tool_exists(name: &str) -> bool {
    find_tool(name).is_some()
}

/// Get tool count
pub fn tool_count() -> usize {
    all_tools().count()
}

/// Get all tool names
pub fn tool_names() -> Vec<&'static str> {
    all_tools().map(|meta| meta.name).collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::Tool;
    use serde::{Deserialize, Serialize};
    use schemars::JsonSchema;

    #[derive(Deserialize, JsonSchema)]
    struct TestParams {
        value: i32,
    }

    #[derive(Serialize, JsonSchema)]
    struct TestOutput {
        result: i32,
    }

    struct TestTool;

    impl Tool for TestTool {
        type Params = TestParams;
        type Output = TestOutput;

        const NAME: &'static str = "test_tool_v2";
        const DESCRIPTION: &'static str = "Test tool for v2";

        async fn call(
            _ctx: Arc<ToolContext>,
            params: Self::Params,
        ) -> Result<Self::Output, ToolError> {
            Ok(TestOutput {
                result: params.value * 2,
            })
        }
    }

    // Manual inventory submission for testing
    inventory::submit! {
        ToolMeta {
            name: TestTool::NAME,
            description: TestTool::DESCRIPTION,
            call: |ctx, params| {
                Box::pin(async move {
                    let params: TestParams = serde_json::from_value(params)?;
                    let result = <TestTool as Tool>::call(ctx, params).await?;
                    Ok(serde_json::to_value(result)?)
                })
            },
            schema: || <TestTool as Tool>::schema(),
            examples: || <TestTool as Tool>::EXAMPLES,
        }
    }

    #[test]
    fn test_find_tool() {
        let tool = find_tool("test_tool_v2");
        assert!(tool.is_some());
        assert_eq!(tool.unwrap().name, "test_tool_v2");
    }

    #[test]
    fn test_tool_exists() {
        assert!(tool_exists("test_tool_v2"));
        assert!(!tool_exists("nonexistent"));
    }

    #[test]
    fn test_tool_count() {
        assert!(tool_count() > 0);
    }

    #[test]
    fn test_tool_names() {
        let names = tool_names();
        assert!(names.contains(&"test_tool_v2"));
    }

    #[tokio::test]
    async fn test_call_tool() {
        let ctx = Arc::new(ToolContext::new());
        let params = serde_json::json!({"value": 5});
        let result = call_tool("test_tool_v2", params, ctx).await.unwrap();
        assert_eq!(result["result"], 10);
    }

    #[tokio::test]
    async fn test_call_nonexistent_tool() {
        let ctx = Arc::new(ToolContext::new());
        let result = call_tool("nonexistent", serde_json::json!({}), ctx).await;
        assert!(result.is_err());
    }
}