Skip to main content

ryo_app/discover/
response.rs

1//! Discover Response - Application Layer response DTOs
2//!
3//! # Role in Architecture
4//!
5//! This module defines **Response DTOs** (Data Transfer Objects) for the Discover feature.
6//! These are **not** Domain Models - they are Application Layer constructs designed for
7//! external communication (CLI output, JSON API responses, etc.).
8//!
9//! ```text
10//! ┌─────────────────────────────────────────────────────────────────┐
11//! │ Domain Layer (ryo-analysis)                                     │
12//! │   CascadeSpec ← Domain Model (canonical type)                   │
13//! └───────────────────────────┬─────────────────────────────────────┘
14//!                             ↓
15//! ┌─────────────────────────────────────────────────────────────────┐
16//! │ Application Layer (ryo-app)                                     │
17//! │   DiscoverService.find_cascade_effects()                        │
18//! │       ↓ Compose & Construct                                     │
19//! │   CascadeResult ← Response DTO (this module)                    │
20//! └───────────────────────────┬─────────────────────────────────────┘
21//!                             ↓
22//! ┌─────────────────────────────────────────────────────────────────┐
23//! │ Presenter Layer (ryo-cli)                                       │
24//! │   print_cascade_summary() ← Render to CLI                       │
25//! │   serde_json::to_string() ← Render to JSON                      │
26//! └─────────────────────────────────────────────────────────────────┘
27//! ```
28//!
29//! # Design Principles
30//!
31//! - **Serializable**: All types implement or support `Serialize` for JSON output
32//! - **Headless**: No rendering logic - that's the Presenter's job
33//! - **Composable**: Wraps Domain types with additional context for consumers
34//! - **Stable API**: Changes here affect external consumers (CLI, API clients)
35//!
36//! # Relationship to Domain Types
37//!
38//! Response DTOs may wrap or reference Domain types:
39//! - `CascadeResult.specs: Vec<CascadeSpec>` - wraps domain type from ryo-analysis
40//! - Use `Intent::from(CascadeSpec)` to convert domain → intent for execution
41
42use ryo_analysis::cascade::CascadeSpec;
43use serde::Serialize;
44
45// ============================================================================
46// Error Types
47// ============================================================================
48
49/// Error type for Discover operations.
50#[derive(Debug, thiserror::Error)]
51pub enum DiscoverError {
52    /// Project loading error
53    #[error("Project error: {0}")]
54    Project(String),
55
56    /// Query execution error
57    #[error("Query error: {0}")]
58    Query(String),
59}
60
61// ============================================================================
62// Response DTOs
63// ============================================================================
64
65/// Response DTO for cascade analysis.
66///
67/// Wraps `Vec<CascadeSpec>` from ryo-analysis with metadata for consumers.
68/// This is **not** a Domain Model - it's an Application Layer response type.
69///
70/// # Example
71///
72/// ```ignore
73/// let result: CascadeResult = service.find_cascade_effects("Status", Some("Cancelled"));
74///
75/// // JSON output
76/// println!("{}", serde_json::to_string_pretty(&result)?);
77///
78/// // Convert to Intents for execution
79/// let intents: Vec<Intent> = result.specs.into_iter().map(Into::into).collect();
80/// ```
81#[derive(Debug, Clone, Serialize)]
82pub struct CascadeResult {
83    /// The enum being analyzed (query pattern)
84    pub symbol: String,
85
86    /// Cascade specs from domain layer (ryo-analysis)
87    #[serde(serialize_with = "serialize_cascade_specs")]
88    pub specs: Vec<CascadeSpec>,
89}
90
91impl CascadeResult {
92    /// Create a new CascadeResult.
93    pub fn new(symbol: String) -> Self {
94        Self {
95            symbol,
96            specs: Vec::new(),
97        }
98    }
99
100    /// Check if there are any cascade specs.
101    pub fn is_empty(&self) -> bool {
102        self.specs.is_empty()
103    }
104
105    /// Get the number of cascade specs.
106    pub fn len(&self) -> usize {
107        self.specs.len()
108    }
109}
110
111// ============================================================================
112// Serialization Helpers
113// ============================================================================
114
115/// Custom serializer for CascadeSpec (which doesn't derive Serialize).
116///
117/// This keeps serde dependency out of the Domain layer (ryo-analysis).
118fn serialize_cascade_specs<S>(specs: &[CascadeSpec], serializer: S) -> Result<S::Ok, S::Error>
119where
120    S: serde::Serializer,
121{
122    use serde::ser::SerializeSeq;
123
124    let mut seq = serializer.serialize_seq(Some(specs.len()))?;
125    for spec in specs {
126        let value = match spec {
127            CascadeSpec::AddMatchArm {
128                target,
129                function_name,
130                enum_name,
131                pattern,
132                body,
133            } => serde_json::json!({
134                "type": "AddMatchArm",
135                "target": target.to_string(),
136                "function_name": function_name,
137                "enum_name": enum_name,
138                "pattern": pattern,
139                "body": body,
140            }),
141            CascadeSpec::AddDerive { symbol_id, derives } => serde_json::json!({
142                "type": "AddDerive",
143                "symbol_id": format!("{:?}", symbol_id),
144                "derives": derives,
145            }),
146            CascadeSpec::GenerateImpl {
147                target,
148                trait_name,
149                call_new,
150            } => serde_json::json!({
151                "type": "GenerateImpl",
152                "target": target.to_string(),
153                "trait_name": trait_name,
154                "call_new": call_new,
155            }),
156            CascadeSpec::ChangeVisibility {
157                symbol_id,
158                visibility,
159            } => serde_json::json!({
160                "type": "ChangeVisibility",
161                "symbol_id": format!("{:?}", symbol_id),
162                "visibility": format!("{:?}", visibility),
163            }),
164            CascadeSpec::AddUse {
165                target_module,
166                path,
167            } => serde_json::json!({
168                "type": "AddUse",
169                "target_module": target_module.to_string(),
170                "path": path,
171            }),
172            CascadeSpec::RemoveMatchArm {
173                target,
174                function_name,
175                enum_name,
176                pattern,
177            } => serde_json::json!({
178                "type": "RemoveMatchArm",
179                "target": target.to_string(),
180                "function_name": function_name,
181                "enum_name": enum_name,
182                "pattern": pattern,
183            }),
184        };
185        seq.serialize_element(&value)?;
186    }
187    seq.end()
188}