descry-tool-core 0.3.1

Core traits and types for descry-tool framework
Documentation
//! Tower Service integration
//!
//! Provides Tower Service implementation for tools, enabling middleware composition.

#[cfg(feature = "tower")]
mod tower_impl {
    use crate::{ToolMeta, ToolError};
    use futures::Future;
    use std::pin::Pin;
    use std::sync::Arc;
    use std::task::{Context, Poll};
    use tower::Service;

    /// Tool request for Tower Service
    #[derive(Debug, Clone)]
    pub struct ToolRequest {
        /// Tool name
        pub name: String,
        /// Tool parameters (JSON value)
        pub params: serde_json::Value,
        /// Execution context
        pub ctx: Arc<crate::ToolContext>,
    }

    /// Tool response from Tower Service
    #[derive(Debug, Clone)]
    pub struct ToolResponse {
        /// Tool output (JSON value)
        pub output: serde_json::Value,
    }

    /// ToolService implements Tower Service trait
    ///
    /// This allows tools to be used with Tower middleware ecosystem.
    ///
    /// # Examples
    ///
    /// ```rust,ignore
    /// use tower::ServiceBuilder;
    /// use tower_http::{timeout::TimeoutLayer, limit::RateLimitLayer};
    /// use std::time::Duration;
    ///
    /// let service = ServiceBuilder::new()
    ///     .layer(TimeoutLayer::new(Duration::from_secs(30)))
    ///     .layer(RateLimitLayer::new(10, Duration::from_secs(1)))
    ///     .service(ToolService::new());
    /// ```
    pub struct ToolService {
        _meta: Option<&'static ToolMeta>,
    }

    impl Clone for ToolService {
        fn clone(&self) -> Self {
            Self { _meta: self._meta }
        }
    }

    impl ToolService {
        /// Create new ToolService
        pub fn new() -> Self {
            Self { _meta: None }
        }

        /// Create ToolService for a specific tool
        pub fn for_tool(meta: &'static ToolMeta) -> Self {
            Self {
                _meta: Some(meta),
            }
        }
    }

    impl Default for ToolService {
        fn default() -> Self {
            Self::new()
        }
    }

    impl Service<ToolRequest> for ToolService {
        type Response = ToolResponse;
        type Error = ToolError;
        type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

        fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
            Poll::Ready(Ok(()))
        }

        fn call(&mut self, req: ToolRequest) -> Self::Future {
            Box::pin(async move {
                let output = crate::call_tool(&req.name, req.params, req.ctx).await?;
                Ok(ToolResponse { output })
            })
        }
    }

    /// Convenience function to create a ToolService
    pub fn tool_service() -> ToolService {
        ToolService::new()
    }

    /// Convenience function to create a ToolService for a specific tool
    pub fn tool_service_for(name: &str) -> Option<ToolService> {
        crate::find_tool(name).map(ToolService::for_tool)
    }
    }

#[cfg(feature = "tower")]
pub use tower_impl::*;

#[cfg(test)]
#[cfg(feature = "tower")]
mod tests {
    use super::*;
    use crate::{Tool, ToolContext};
    use schemars::JsonSchema;
    use serde::{Deserialize, Serialize};
    use std::sync::Arc;
    use tower::Service;

    #[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_tower";
        const DESCRIPTION: &'static str = "Test tool for Tower";

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

    inventory::submit! {
        crate::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,
        }
    }

    #[tokio::test]
    async fn test_tool_service_basic() {
        let mut service = ToolService::new();
        
        let req = ToolRequest {
            name: "test_tower".to_string(),
            params: serde_json::json!({"value": 5}),
            ctx: Arc::new(ToolContext::new()),
        };

        let response = service.call(req).await.unwrap();
        assert_eq!(response.output["result"], 10);
    }

    #[tokio::test]
    async fn test_tool_service_not_found() {
        let mut service = ToolService::new();
        
        let req = ToolRequest {
            name: "nonexistent".to_string(),
            params: serde_json::json!({}),
            ctx: Arc::new(ToolContext::new()),
        };

        let result = service.call(req).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn test_tool_service_for_tool() {
        let meta = crate::find_tool("test_tower").unwrap();
        let mut service = ToolService::for_tool(meta);
        
        let req = ToolRequest {
            name: "test_tower".to_string(),
            params: serde_json::json!({"value": 7}),
            ctx: Arc::new(ToolContext::new()),
        };

        let response = service.call(req).await.unwrap();
        assert_eq!(response.output["result"], 14);
    }

    #[tokio::test]
    async fn test_convenience_functions() {
        let service = tool_service();
        // Just verify we can create a service
        let _ = service;

        let service = tool_service_for("test_tower");
        assert!(service.is_some());

        let service = tool_service_for("nonexistent");
        assert!(service.is_none());
    }
}