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}