1use ryo_analysis::context::AnalysisContext;
7use ryo_analysis::{SymbolId, SymbolPath};
8
9use crate::suggest::{
10 MutationSpec, OpportunityContext, OpportunityId, ParamDef, SafetyLevel, Suggest,
11 SuggestCategory, SuggestLocation, SuggestOpportunity, SuggestParams, SuggestResult,
12};
13
14fn create_generation_location(name: &str) -> SuggestLocation {
20 let symbol_id = SymbolId::parse("0v1").expect("valid dummy SymbolId");
22
23 let symbol_path = SymbolPath::builder("generated")
24 .push(name)
25 .build()
26 .unwrap_or_else(|_| SymbolPath::builder("generated").build().expect("path"));
27
28 SuggestLocation::new(symbol_id, symbol_path, "(generated)")
29}
30
31pub struct DomainStructSuggest {
50 default_derives: Vec<String>,
51}
52
53impl DomainStructSuggest {
54 pub fn new() -> Self {
55 Self {
56 default_derives: vec!["Debug".into(), "Clone".into()],
57 }
58 }
59
60 pub fn with_derives(mut self, derives: Vec<String>) -> Self {
61 self.default_derives = derives;
62 self
63 }
64
65 fn parse_fields(&self, fields_str: &str) -> Vec<(String, String)> {
68 if fields_str.is_empty() {
69 return vec![];
70 }
71
72 fields_str
73 .split(',')
74 .filter_map(|field| {
75 let parts: Vec<&str> = field.trim().split(':').collect();
76 if parts.len() == 2 {
77 Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
78 } else {
79 None
80 }
81 })
82 .collect()
83 }
84}
85
86impl Default for DomainStructSuggest {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl Suggest for DomainStructSuggest {
93 fn name(&self) -> &'static str {
94 "domain-struct"
95 }
96
97 fn description(&self) -> &str {
98 "Generate a domain struct with derives and fields"
99 }
100
101 fn category(&self) -> SuggestCategory {
102 SuggestCategory::Pattern
103 }
104
105 fn safety_level(&self) -> SafetyLevel {
106 SafetyLevel::Confirm
107 }
108
109 fn rule_id(&self) -> Option<&str> {
110 Some("RG001")
111 }
112
113 fn accepts_params(&self) -> bool {
114 true
115 }
116
117 fn param_schema(&self) -> Vec<ParamDef> {
118 vec![
119 ParamDef::required("name", "Struct name (e.g., Order, User, Product)"),
120 ParamDef::optional(
121 "fields",
122 "Comma-separated fields (e.g., id:u64,name:String)",
123 ),
124 ParamDef::optional("derives", "Comma-separated derives (default: Debug,Clone)"),
125 ]
126 }
127
128 fn detect_with_params(
129 &self,
130 _ctx: &AnalysisContext,
131 _symbols: &[SymbolId],
132 params: &SuggestParams,
133 ) -> Vec<SuggestOpportunity> {
134 let Some(name) = params.get("name") else {
135 return vec![];
136 };
137
138 let fields = params
139 .get("fields")
140 .map(|s| self.parse_fields(s))
141 .unwrap_or_default();
142
143 let derives = params
144 .get("derives")
145 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
146 .unwrap_or_else(|| self.default_derives.clone());
147
148 let message = format!(
149 "Generate struct `{}` with {} fields and derives {:?}",
150 name,
151 fields.len(),
152 derives
153 );
154
155 let location = create_generation_location(name);
157
158 vec![SuggestOpportunity::new(
159 OpportunityId::new(0),
160 vec![],
161 location,
162 message,
163 1.0,
164 OpportunityContext::Generation {
165 pattern: "domain-struct".to_string(),
166 params: params.clone(),
167 },
168 )]
169 }
170
171 fn detect(&self, _ctx: &AnalysisContext, _symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
172 vec![]
174 }
175
176 fn to_mutation_specs(
177 &self,
178 _ctx: &AnalysisContext,
179 opportunity: &SuggestOpportunity,
180 ) -> SuggestResult<Vec<MutationSpec>> {
181 let OpportunityContext::Generation { params, .. } = &opportunity.context else {
182 return Ok(vec![]);
183 };
184
185 let Some(name) = params.get("name") else {
186 return Ok(vec![]);
187 };
188
189 let fields = params
190 .get("fields")
191 .map(|s| self.parse_fields(s))
192 .unwrap_or_default();
193
194 let derives = params
195 .get("derives")
196 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
197 .unwrap_or_else(|| self.default_derives.clone());
198
199 let mut code = String::new();
201
202 if !derives.is_empty() {
204 code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
205 }
206
207 code.push_str(&format!("pub struct {} {{\n", name));
209 for (field_name, field_type) in &fields {
210 code.push_str(&format!(" pub {}: {},\n", field_name, field_type));
211 }
212 code.push('}');
213
214 let target = SymbolPath::parse("crate")
216 .unwrap_or_else(|_| SymbolPath::builder("crate").build().expect("crate path"));
217
218 Ok(vec![MutationSpec::AddItem {
219 target: ryo_executor::MutationTargetSymbol::ByPath(Box::new(target)),
220 content: code,
221 position: ryo_executor::InsertPosition::Bottom,
222 }])
223 }
224}
225
226pub struct ApiPatternSuggest {
252 methods: Vec<ApiMethod>,
253}
254
255#[derive(Clone)]
256struct ApiMethod {
257 name: &'static str,
258 has_id_param: bool,
259 has_entity_param: bool,
260 returns_entity: bool,
261}
262
263impl ApiPatternSuggest {
264 pub fn new() -> Self {
265 Self {
266 methods: vec![
267 ApiMethod {
268 name: "get",
269 has_id_param: true,
270 has_entity_param: false,
271 returns_entity: true,
272 },
273 ApiMethod {
274 name: "list",
275 has_id_param: false,
276 has_entity_param: false,
277 returns_entity: true,
278 },
279 ApiMethod {
280 name: "create",
281 has_id_param: false,
282 has_entity_param: true,
283 returns_entity: true,
284 },
285 ApiMethod {
286 name: "update",
287 has_id_param: true,
288 has_entity_param: true,
289 returns_entity: true,
290 },
291 ApiMethod {
292 name: "delete",
293 has_id_param: true,
294 has_entity_param: false,
295 returns_entity: false,
296 },
297 ],
298 }
299 }
300}
301
302impl Default for ApiPatternSuggest {
303 fn default() -> Self {
304 Self::new()
305 }
306}
307
308impl Suggest for ApiPatternSuggest {
309 fn name(&self) -> &'static str {
310 "api-pattern"
311 }
312
313 fn description(&self) -> &str {
314 "Generate API struct with CRUD methods (get, list, create, update, delete)"
315 }
316
317 fn category(&self) -> SuggestCategory {
318 SuggestCategory::Pattern
319 }
320
321 fn safety_level(&self) -> SafetyLevel {
322 SafetyLevel::Confirm
323 }
324
325 fn rule_id(&self) -> Option<&str> {
326 Some("RG002")
327 }
328
329 fn accepts_params(&self) -> bool {
330 true
331 }
332
333 fn param_schema(&self) -> Vec<ParamDef> {
334 vec![
335 ParamDef::required("name", "API name prefix (e.g., Order -> OrderAPI)"),
336 ParamDef::optional("entity", "Entity type name (default: same as name)"),
337 ParamDef::optional(
338 "methods",
339 "Comma-separated methods to generate (default: get,list,create,update,delete)",
340 ),
341 ]
342 }
343
344 fn detect_with_params(
345 &self,
346 _ctx: &AnalysisContext,
347 _symbols: &[SymbolId],
348 params: &SuggestParams,
349 ) -> Vec<SuggestOpportunity> {
350 let Some(name) = params.get("name") else {
351 return vec![];
352 };
353
354 let api_name = format!("{}API", name);
355 let entity = params
356 .get("entity")
357 .cloned()
358 .unwrap_or_else(|| name.clone());
359
360 let method_names: Vec<&str> = params
361 .get("methods")
362 .map(|s| s.split(',').map(|m| m.trim()).collect())
363 .unwrap_or_else(|| vec!["get", "list", "create", "update", "delete"]);
364
365 let message = format!(
366 "Generate `{}` with methods: {} for entity `{}`",
367 api_name,
368 method_names.join(", "),
369 entity
370 );
371
372 let location = create_generation_location(&api_name);
373
374 vec![SuggestOpportunity::new(
375 OpportunityId::new(0),
376 vec![],
377 location,
378 message,
379 1.0,
380 OpportunityContext::Generation {
381 pattern: "api-pattern".to_string(),
382 params: params.clone(),
383 },
384 )]
385 }
386
387 fn detect(&self, _ctx: &AnalysisContext, _symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
388 vec![]
389 }
390
391 fn to_mutation_specs(
392 &self,
393 _ctx: &AnalysisContext,
394 opportunity: &SuggestOpportunity,
395 ) -> SuggestResult<Vec<MutationSpec>> {
396 let OpportunityContext::Generation { params, .. } = &opportunity.context else {
397 return Ok(vec![]);
398 };
399
400 let Some(name) = params.get("name") else {
401 return Ok(vec![]);
402 };
403
404 let api_name = format!("{}API", name);
405 let entity = params
406 .get("entity")
407 .cloned()
408 .unwrap_or_else(|| name.clone());
409 let id_type = format!("{}Id", entity);
410
411 let method_filter: Option<Vec<&str>> = params
412 .get("methods")
413 .map(|s| s.split(',').map(|m| m.trim()).collect());
414
415 let mut specs = Vec::new();
416
417 let struct_code = format!("pub struct {} {{}}", api_name);
419 let target = SymbolPath::parse("crate")
420 .unwrap_or_else(|_| SymbolPath::builder("crate").build().expect("crate path"));
421
422 specs.push(MutationSpec::AddItem {
423 target: ryo_executor::MutationTargetSymbol::ByPath(Box::new(target)),
424 content: struct_code,
425 position: ryo_executor::InsertPosition::Bottom,
426 });
427
428 for method in &self.methods {
430 if let Some(ref filter) = method_filter {
431 if !filter.contains(&method.name) {
432 continue;
433 }
434 }
435
436 let mut method_params: Vec<(String, String)> = vec![];
437 if method.has_id_param {
438 method_params.push(("id".to_string(), id_type.clone()));
439 }
440 if method.has_entity_param {
441 method_params.push(("entity".to_string(), entity.clone()));
442 }
443
444 let return_type = if method.returns_entity {
445 if method.name == "list" {
446 format!("Result<Vec<{}>, Error>", entity)
447 } else {
448 format!("Result<{}, Error>", entity)
449 }
450 } else {
451 "Result<(), Error>".to_string()
452 };
453
454 let body = "todo!()".to_string();
455
456 specs.push(MutationSpec::AddMethod {
457 target: ryo_executor::MutationTargetSymbol::ByKindAndName(
458 ryo_executor::ItemKind::Struct,
459 api_name.clone(),
460 ),
461 method_name: method.name.to_string(),
462 params: method_params,
463 return_type: Some(return_type),
464 body,
465 is_pub: true,
466 self_param: Some(ryo_executor::SelfParam::Ref),
467 });
468 }
469
470 Ok(specs)
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_domain_struct_param_schema() {
480 let suggest = DomainStructSuggest::new();
481 assert!(suggest.accepts_params());
482 assert_eq!(suggest.param_schema().len(), 3);
483 }
484
485 #[test]
486 fn test_domain_struct_parse_fields() {
487 let suggest = DomainStructSuggest::new();
488 let fields = suggest.parse_fields("id:u64,name:String,active:bool");
489 assert_eq!(fields.len(), 3);
490 assert_eq!(fields[0], ("id".to_string(), "u64".to_string()));
491 assert_eq!(fields[1], ("name".to_string(), "String".to_string()));
492 assert_eq!(fields[2], ("active".to_string(), "bool".to_string()));
493 }
494
495 #[test]
496 fn test_api_pattern_param_schema() {
497 let suggest = ApiPatternSuggest::new();
498 assert!(suggest.accepts_params());
499 assert_eq!(suggest.param_schema().len(), 3);
500 }
501
502 #[test]
503 fn test_api_pattern_name() {
504 let suggest = ApiPatternSuggest::new();
505 assert_eq!(suggest.name(), "api-pattern");
506 assert_eq!(suggest.rule_id(), Some("RG002"));
507 }
508}