ryo-app 0.1.0

[preview] Application layer for RYO - Project management, Intent handling, API
Documentation
//! Discover Response - Application Layer response DTOs
//!
//! # Role in Architecture
//!
//! This module defines **Response DTOs** (Data Transfer Objects) for the Discover feature.
//! These are **not** Domain Models - they are Application Layer constructs designed for
//! external communication (CLI output, JSON API responses, etc.).
//!
//! ```text
//! ┌─────────────────────────────────────────────────────────────────┐
//! │ Domain Layer (ryo-analysis)                                     │
//! │   CascadeSpec ← Domain Model (canonical type)                   │
//! └───────────────────────────┬─────────────────────────────────────┘
//!//! ┌─────────────────────────────────────────────────────────────────┐
//! │ Application Layer (ryo-app)                                     │
//! │   DiscoverService.find_cascade_effects()                        │
//! │       ↓ Compose & Construct                                     │
//! │   CascadeResult ← Response DTO (this module)                    │
//! └───────────────────────────┬─────────────────────────────────────┘
//!//! ┌─────────────────────────────────────────────────────────────────┐
//! │ Presenter Layer (ryo-cli)                                       │
//! │   print_cascade_summary() ← Render to CLI                       │
//! │   serde_json::to_string() ← Render to JSON                      │
//! └─────────────────────────────────────────────────────────────────┘
//! ```
//!
//! # Design Principles
//!
//! - **Serializable**: All types implement or support `Serialize` for JSON output
//! - **Headless**: No rendering logic - that's the Presenter's job
//! - **Composable**: Wraps Domain types with additional context for consumers
//! - **Stable API**: Changes here affect external consumers (CLI, API clients)
//!
//! # Relationship to Domain Types
//!
//! Response DTOs may wrap or reference Domain types:
//! - `CascadeResult.specs: Vec<CascadeSpec>` - wraps domain type from ryo-analysis
//! - Use `Intent::from(CascadeSpec)` to convert domain → intent for execution

use ryo_analysis::cascade::CascadeSpec;
use serde::Serialize;

// ============================================================================
// Error Types
// ============================================================================

/// Error type for Discover operations.
#[derive(Debug, thiserror::Error)]
pub enum DiscoverError {
    /// Project loading error
    #[error("Project error: {0}")]
    Project(String),

    /// Query execution error
    #[error("Query error: {0}")]
    Query(String),
}

// ============================================================================
// Response DTOs
// ============================================================================

/// Response DTO for cascade analysis.
///
/// Wraps `Vec<CascadeSpec>` from ryo-analysis with metadata for consumers.
/// This is **not** a Domain Model - it's an Application Layer response type.
///
/// # Example
///
/// ```ignore
/// let result: CascadeResult = service.find_cascade_effects("Status", Some("Cancelled"));
///
/// // JSON output
/// println!("{}", serde_json::to_string_pretty(&result)?);
///
/// // Convert to Intents for execution
/// let intents: Vec<Intent> = result.specs.into_iter().map(Into::into).collect();
/// ```
#[derive(Debug, Clone, Serialize)]
pub struct CascadeResult {
    /// The enum being analyzed (query pattern)
    pub symbol: String,

    /// Cascade specs from domain layer (ryo-analysis)
    #[serde(serialize_with = "serialize_cascade_specs")]
    pub specs: Vec<CascadeSpec>,
}

impl CascadeResult {
    /// Create a new CascadeResult.
    pub fn new(symbol: String) -> Self {
        Self {
            symbol,
            specs: Vec::new(),
        }
    }

    /// Check if there are any cascade specs.
    pub fn is_empty(&self) -> bool {
        self.specs.is_empty()
    }

    /// Get the number of cascade specs.
    pub fn len(&self) -> usize {
        self.specs.len()
    }
}

// ============================================================================
// Serialization Helpers
// ============================================================================

/// Custom serializer for CascadeSpec (which doesn't derive Serialize).
///
/// This keeps serde dependency out of the Domain layer (ryo-analysis).
fn serialize_cascade_specs<S>(specs: &[CascadeSpec], serializer: S) -> Result<S::Ok, S::Error>
where
    S: serde::Serializer,
{
    use serde::ser::SerializeSeq;

    let mut seq = serializer.serialize_seq(Some(specs.len()))?;
    for spec in specs {
        let value = match spec {
            CascadeSpec::AddMatchArm {
                target,
                function_name,
                enum_name,
                pattern,
                body,
            } => serde_json::json!({
                "type": "AddMatchArm",
                "target": target.to_string(),
                "function_name": function_name,
                "enum_name": enum_name,
                "pattern": pattern,
                "body": body,
            }),
            CascadeSpec::AddDerive { symbol_id, derives } => serde_json::json!({
                "type": "AddDerive",
                "symbol_id": format!("{:?}", symbol_id),
                "derives": derives,
            }),
            CascadeSpec::GenerateImpl {
                target,
                trait_name,
                call_new,
            } => serde_json::json!({
                "type": "GenerateImpl",
                "target": target.to_string(),
                "trait_name": trait_name,
                "call_new": call_new,
            }),
            CascadeSpec::ChangeVisibility {
                symbol_id,
                visibility,
            } => serde_json::json!({
                "type": "ChangeVisibility",
                "symbol_id": format!("{:?}", symbol_id),
                "visibility": format!("{:?}", visibility),
            }),
            CascadeSpec::AddUse {
                target_module,
                path,
            } => serde_json::json!({
                "type": "AddUse",
                "target_module": target_module.to_string(),
                "path": path,
            }),
            CascadeSpec::RemoveMatchArm {
                target,
                function_name,
                enum_name,
                pattern,
            } => serde_json::json!({
                "type": "RemoveMatchArm",
                "target": target.to_string(),
                "function_name": function_name,
                "enum_name": enum_name,
                "pattern": pattern,
            }),
        };
        seq.serialize_element(&value)?;
    }
    seq.end()
}