Skip to main content

ryo_suggest/spec/
parameterized.rs

1//! Parameterized Suggest implementations for code generation.
2//!
3//! These suggestions accept external parameters to generate code from patterns.
4//! LLMs can discover available patterns via `SuggestService::list_parameterized()`.
5
6use 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
14// =============================================================================
15// Helper Functions
16// =============================================================================
17
18/// Create a SuggestLocation for generated code (not from existing source).
19fn create_generation_location(name: &str) -> SuggestLocation {
20    // Use a dummy SymbolId for generated code
21    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
31// =============================================================================
32// DomainStructSuggest - Generate domain struct from pattern
33// =============================================================================
34
35/// Generates a domain struct with common patterns.
36///
37/// # Parameters
38/// - `name` (required): Struct name (e.g., "Order")
39/// - `fields` (optional): Comma-separated field definitions (e.g., "id:u64,name:String")
40///
41/// # Generated Code
42/// ```ignore
43/// #[derive(Debug, Clone)]
44/// pub struct Order {
45///     pub id: u64,
46///     pub name: String,
47/// }
48/// ```
49pub 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    /// Parse fields from comma-separated string.
66    /// Format: "name:Type,other:OtherType"
67    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        // Create a dummy location since this is generation, not detection
156        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        // This is a generation-only suggest, no detection
173        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        // Build struct code
200        let mut code = String::new();
201
202        // Add derives
203        if !derives.is_empty() {
204            code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
205        }
206
207        // Add struct definition
208        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        // Use AddItem to add the struct
215        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
226// =============================================================================
227// ApiPatternSuggest - Generate API struct with CRUD methods
228// =============================================================================
229
230/// Generates an API struct with common CRUD methods.
231///
232/// # Parameters
233/// - `name` (required): API name prefix (e.g., "Order" -> OrderAPI)
234/// - `entity` (optional): Entity type (default: same as name)
235///
236/// # Generated Code
237/// ```ignore
238/// pub struct OrderAPI {
239///     // ...
240/// }
241///
242/// impl OrderAPI {
243///     pub fn new() -> Self { ... }
244///     pub fn get(&self, id: OrderId) -> Result<Order, Error> { ... }
245///     pub fn list(&self) -> Result<Vec<Order>, Error> { ... }
246///     pub fn create(&self, entity: Order) -> Result<Order, Error> { ... }
247///     pub fn update(&self, id: OrderId, entity: Order) -> Result<Order, Error> { ... }
248///     pub fn delete(&self, id: OrderId) -> Result<(), Error> { ... }
249/// }
250/// ```
251pub 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        // Generate API struct using AddItem
418        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        // Generate methods
429        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}