agenterra 0.2.2

Generate production-ready MCP (Model Context Protocol) servers and clients from OpenAPI specs
//! Use case for generating server implementations

use crate::application::{
    ApplicationError, GenerateServerRequest, GenerateServerResponse, OutputService,
};
use crate::generation::{GenerationOrchestrator, OpenApiLoader};
use crate::infrastructure::shell::CommandExecutor;
use crate::protocols::{ProtocolConfig, ProtocolInput, ProtocolRegistry, Role};
use std::sync::Arc;

/// Use case for generating server implementations
pub struct GenerateServerUseCase {
    protocol_registry: Arc<ProtocolRegistry>,
    openapi_loader: Arc<dyn OpenApiLoader>,
    generation_orchestrator: Arc<GenerationOrchestrator>,
    output_service: Arc<dyn OutputService>,
}

impl GenerateServerUseCase {
    pub fn new(
        protocol_registry: Arc<ProtocolRegistry>,
        openapi_loader: Arc<dyn OpenApiLoader>,
        generation_orchestrator: Arc<GenerationOrchestrator>,
        output_service: Arc<dyn OutputService>,
    ) -> Self {
        Self {
            protocol_registry,
            openapi_loader,
            generation_orchestrator,
            output_service,
        }
    }

    pub async fn execute(
        &self,
        request: GenerateServerRequest,
    ) -> Result<GenerateServerResponse, ApplicationError> {
        // 1. Validate request
        request.validate()?;

        // 2. Get protocol handler
        let handler =
            self.protocol_registry
                .get(request.protocol)
                .ok_or(ApplicationError::ProtocolError(
                    crate::protocols::ProtocolError::NotImplemented(request.protocol),
                ))?;

        // 3. Load OpenAPI if needed
        let capabilities = handler.protocol().capabilities();
        let openapi_spec = if capabilities.requires_openapi {
            match &request.schema_path {
                Some(path) => {
                    tracing::debug!("GenerateServerUseCase: Loading OpenAPI from path: {}", path);
                    Some(self.openapi_loader.load(path).await?)
                }
                None => {
                    return Err(ApplicationError::ValidationError(
                        crate::application::ValidationError::MissingField(
                            "MCP server requires OpenAPI schema path".to_string(),
                        ),
                    ));
                }
            }
        } else {
            None
        };

        // 4. Prepare protocol input
        let input = ProtocolInput {
            role: Role::Server,
            language: request.language,
            config: ProtocolConfig {
                project_name: request.project_name.clone(),
                version: None,
                options: request.options.clone(),
            },
            openapi_spec,
        };

        // 5. Build generation context
        let mut context = handler.prepare_context(input).await?;

        // Set the output directory in context for post-processors to use
        context.output_dir = Some(request.output_dir.clone());

        // 6. Generate code
        let result = self.generation_orchestrator.generate(context).await?;

        // 7. Ensure output directory exists
        self.output_service
            .ensure_directory(&request.output_dir)
            .await?;

        // 8. Prepend output directory to artifact paths and write
        let mut output_artifacts = result.artifacts;
        for artifact in &mut output_artifacts {
            artifact.path = request.output_dir.join(&artifact.path);
        }

        let artifacts_count = output_artifacts.len();

        self.output_service
            .write_artifacts(&output_artifacts)
            .await?;

        // Execute post-generation commands after files are written
        if !result.post_generation_commands.is_empty() {
            let command_executor = crate::infrastructure::ShellCommandExecutor::new();
            for command in &result.post_generation_commands {
                tracing::info!(
                    project_name = %result.metadata.project_name,
                    command = %command,
                    working_dir = ?request.output_dir,
                    "Executing post-generation command after file write"
                );

                match command_executor.execute(command, &request.output_dir).await {
                    Ok(cmd_result) => {
                        if cmd_result.is_success() {
                            tracing::debug!(
                                project_name = %result.metadata.project_name,
                                command = %command,
                                "Post-generation command completed successfully"
                            );
                        } else {
                            tracing::error!(
                                project_name = %result.metadata.project_name,
                                command = %command,
                                exit_code = cmd_result.exit_code,
                                stderr = %cmd_result.stderr,
                                "Post-generation command failed"
                            );
                        }
                    }
                    Err(e) => {
                        tracing::warn!(
                            project_name = %result.metadata.project_name,
                            command = %command,
                            error = %e,
                            "Post-generation command could not be executed (this is optional and non-fatal)"
                        );
                    }
                }
            }
        }

        Ok(GenerateServerResponse {
            artifacts_count,
            output_path: request.output_dir,
            metadata: result.metadata,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::generation::{self, GenerationError, Language};
    use crate::infrastructure::{Template, TemplateManifest, TemplateSource};
    use crate::protocols::{self, Protocol, Role};
    use std::collections::HashMap;
    use std::path::PathBuf;

    #[tokio::test]
    async fn test_execute_success() {
        let protocol_registry = Arc::new(create_mock_registry());
        let openapi_loader = Arc::new(MockOpenApiLoader);
        let generation_orchestrator = Arc::new(create_mock_orchestrator());
        let output_service = Arc::new(MockOutputService::new());

        let use_case = GenerateServerUseCase::new(
            protocol_registry,
            openapi_loader,
            generation_orchestrator,
            output_service.clone(),
        );

        let request = GenerateServerRequest {
            protocol: Protocol::Mcp,
            language: Language::Rust,
            project_name: "test-server".to_string(),
            schema_path: Some("/path/to/openapi.yaml".to_string()),
            output_dir: PathBuf::from("/output"),
            options: HashMap::new(),
        };

        let response = use_case.execute(request).await.unwrap();
        assert_eq!(response.artifacts_count, 5);
        assert_eq!(response.output_path, PathBuf::from("/output"));

        // Verify the output service was called with artifacts
        let written = output_service.get_written_artifacts();
        assert_eq!(written.len(), 5);

        // Verify directory was ensured
        let dirs = output_service.get_ensured_directories();
        assert!(!dirs.is_empty());
        assert!(dirs.contains(&PathBuf::from("/output")));
    }

    // Helper functions to create mocks
    fn create_mock_registry() -> ProtocolRegistry {
        let registry = ProtocolRegistry::new();
        let _ = registry.register(
            Protocol::Mcp,
            Arc::new(protocols::handlers::mcp::McpProtocolHandler::new()),
        );
        registry
    }

    fn create_mock_orchestrator() -> GenerationOrchestrator {
        GenerationOrchestrator::new(
            Arc::new(MockTemplateDiscovery),
            Arc::new(MockContextBuilder),
            Arc::new(MockTemplateRenderer),
            Arc::new(MockPostProcessor),
        )
    }

    // Mock implementations
    struct MockOpenApiLoader;

    #[async_trait::async_trait]
    impl generation::OpenApiLoader for MockOpenApiLoader {
        async fn load(&self, _source: &str) -> Result<generation::OpenApiContext, GenerationError> {
            Ok(generation::OpenApiContext {
                version: "3.0.0".to_string(),
                info: generation::ApiInfo {
                    title: "Test API".to_string(),
                    version: "1.0.0".to_string(),
                    description: None,
                },
                servers: vec![],
                operations: vec![],
                components: None,
            })
        }
    }

    struct MockOutputService {
        written_artifacts: std::sync::Mutex<Vec<generation::Artifact>>,
        ensured_directories: std::sync::Mutex<Vec<std::path::PathBuf>>,
    }

    impl MockOutputService {
        fn new() -> Self {
            Self {
                written_artifacts: std::sync::Mutex::new(Vec::new()),
                ensured_directories: std::sync::Mutex::new(Vec::new()),
            }
        }

        fn get_written_artifacts(&self) -> Vec<generation::Artifact> {
            self.written_artifacts.lock().unwrap().clone()
        }

        fn get_ensured_directories(&self) -> Vec<std::path::PathBuf> {
            self.ensured_directories.lock().unwrap().clone()
        }
    }

    #[async_trait::async_trait]
    impl OutputService for MockOutputService {
        async fn write_artifacts(
            &self,
            artifacts: &[generation::Artifact],
        ) -> Result<(), ApplicationError> {
            // Track artifacts being written
            let mut written = self.written_artifacts.lock().unwrap();
            written.extend(artifacts.iter().cloned());

            // Validate artifacts have content
            for artifact in artifacts {
                if artifact.path.to_string_lossy().is_empty() {
                    return Err(ApplicationError::GenerationError(
                        GenerationError::InvalidConfiguration(
                            "Artifact path cannot be empty".to_string(),
                        ),
                    ));
                }
                if artifact.content.is_empty() {
                    return Err(ApplicationError::GenerationError(
                        GenerationError::InvalidConfiguration(
                            "Artifact content cannot be empty".to_string(),
                        ),
                    ));
                }
            }

            Ok(())
        }

        async fn ensure_directory(&self, path: &std::path::Path) -> Result<(), ApplicationError> {
            // Track directories being ensured
            self.ensured_directories
                .lock()
                .unwrap()
                .push(path.to_path_buf());

            // Validate path
            if path.to_string_lossy().is_empty() {
                return Err(ApplicationError::GenerationError(
                    GenerationError::InvalidConfiguration(
                        "Directory path cannot be empty".to_string(),
                    ),
                ));
            }

            Ok(())
        }
    }

    struct MockTemplateDiscovery;

    #[async_trait::async_trait]
    impl generation::TemplateDiscovery for MockTemplateDiscovery {
        async fn discover(
            &self,
            protocol: Protocol,
            role: Role,
            language: Language,
        ) -> Result<Template, GenerationError> {
            Ok(Template {
                manifest: TemplateManifest {
                    name: "test-template".to_string(),
                    version: "1.0.0".to_string(),
                    description: Some("Test template".to_string()),
                    path: "test-template".to_string(),
                    protocol,
                    role,
                    language,
                    files: vec![],
                    variables: HashMap::new(),
                    post_generate_hooks: vec![],
                },
                files: vec![],
                source: TemplateSource::Embedded,
            })
        }
    }

    struct MockContextBuilder;

    #[async_trait::async_trait]
    impl generation::ContextBuilder for MockContextBuilder {
        async fn build(
            &self,
            _context: &generation::GenerationContext,
            _template: &Template,
        ) -> Result<generation::RenderContext, GenerationError> {
            Ok(generation::RenderContext::default())
        }
    }

    struct MockTemplateRenderer;

    #[async_trait::async_trait]
    impl generation::TemplateRenderingStrategy for MockTemplateRenderer {
        async fn render(
            &self,
            _template: &Template,
            _context: &generation::RenderContext,
            _generation_context: &generation::GenerationContext,
        ) -> Result<Vec<generation::Artifact>, GenerationError> {
            Ok(vec![
                crate::generation::Artifact {
                    path: PathBuf::from("src/main.rs"),
                    content: "fn main() {}".to_string(),
                    permissions: None,
                };
                5
            ])
        }
    }

    struct MockPostProcessor;

    #[async_trait::async_trait]
    impl generation::PostProcessor for MockPostProcessor {
        async fn process(
            &self,
            artifacts: Vec<generation::Artifact>,
            _context: &generation::GenerationContext,
            _post_generation_commands: &[String],
        ) -> Result<Vec<generation::Artifact>, GenerationError> {
            Ok(artifacts)
        }
    }
}