Skip to main content

ryo_app/spec_dsl/
converter.rs

1//! SpecToIntentConverter - Converts domain specs to executable Intents
2//!
3//! This is the core integration between Spec DSL and the execution pipeline.
4
5use thiserror::Error;
6
7use super::types::*;
8use crate::intent::{EstimatedScope, Goal, Intent, ScopeHint, SelfParam};
9
10/// Conversion error
11#[derive(Debug, Error)]
12pub enum ConversionError {
13    #[error("Invalid module path: {0}")]
14    InvalidModulePath(String),
15
16    #[error("Missing required field: {0}")]
17    MissingField(String),
18
19    #[error("Invalid variant type format: {0}")]
20    InvalidVariantType(String),
21
22    #[error("Unsupported refactor kind: {0}")]
23    UnsupportedRefactor(String),
24}
25
26/// Conversion result with phase information
27#[derive(Debug, Clone)]
28pub struct ConversionResult {
29    /// All generated intents organized by phase
30    pub phases: Vec<PhaseIntents>,
31
32    /// Total intent count
33    pub total_intents: usize,
34
35    /// Estimated lines of code
36    pub estimated_loc: usize,
37}
38
39/// Intents for a single phase
40#[derive(Debug, Clone)]
41pub struct PhaseIntents {
42    /// Phase name
43    pub name: String,
44
45    /// Phase number (1-based)
46    pub phase: usize,
47
48    /// Generated Goal
49    pub goal: Goal,
50
51    /// Description
52    pub description: String,
53}
54
55/// Converter from DomainSpec to Vec<Intent>
56pub struct SpecToIntentConverter {
57    /// Project name for symbol paths
58    _crate_name: String,
59}
60
61impl SpecToIntentConverter {
62    /// Create a new converter
63    pub fn new(crate_name: impl Into<String>) -> Self {
64        Self {
65            _crate_name: crate_name.into(),
66        }
67    }
68
69    /// Convert entire spec to ConversionResult
70    pub fn convert(&self, spec: &DomainSpec) -> Result<ConversionResult, ConversionError> {
71        let mut phases = Vec::new();
72        let mut total_intents = 0;
73
74        // Phase 1: Module Structure
75        let phase1_intents = self.convert_modules(spec)?;
76        if !phase1_intents.is_empty() {
77            total_intents += phase1_intents.len();
78            phases.push(PhaseIntents {
79                name: "Module Structure".to_string(),
80                phase: 1,
81                goal: self.create_goal("Phase 1: Create module structure", phase1_intents),
82                description: "Create module hierarchy".to_string(),
83            });
84        }
85
86        // Phase 2: Common Types
87        let phase2_intents = self.convert_common_types(spec)?;
88        if !phase2_intents.is_empty() {
89            total_intents += phase2_intents.len();
90            phases.push(PhaseIntents {
91                name: "Common Types".to_string(),
92                phase: 2,
93                goal: self.create_goal("Phase 2: Create common types", phase2_intents),
94                description: "Create newtypes, value objects".to_string(),
95            });
96        }
97
98        // Phase 3: Domain Entities
99        let phase3_intents = self.convert_entities(spec)?;
100        if !phase3_intents.is_empty() {
101            total_intents += phase3_intents.len();
102            phases.push(PhaseIntents {
103                name: "Domain Entities".to_string(),
104                phase: 3,
105                goal: self.create_goal("Phase 3: Create domain entities", phase3_intents),
106                description: "Create structs, enums".to_string(),
107            });
108        }
109
110        // Phase 4: Error Types
111        let phase4_intents = self.convert_errors(spec)?;
112        if !phase4_intents.is_empty() {
113            total_intents += phase4_intents.len();
114            phases.push(PhaseIntents {
115                name: "Error Types".to_string(),
116                phase: 4,
117                goal: self.create_goal("Phase 4: Create error types", phase4_intents),
118                description: "Create error enums".to_string(),
119            });
120        }
121
122        // Phase 5: Implementations
123        let phase5_intents = self.convert_implementations(spec)?;
124        if !phase5_intents.is_empty() {
125            total_intents += phase5_intents.len();
126            phases.push(PhaseIntents {
127                name: "Implementations".to_string(),
128                phase: 5,
129                goal: self.create_goal("Phase 5: Add methods", phase5_intents),
130                description: "Add impl blocks and methods".to_string(),
131            });
132        }
133
134        // Phase 6: Refactoring
135        let phase6_intents = self.convert_refactors(spec)?;
136        if !phase6_intents.is_empty() {
137            total_intents += phase6_intents.len();
138            phases.push(PhaseIntents {
139                name: "Refactoring".to_string(),
140                phase: 6,
141                goal: self.create_goal("Phase 6: Apply refactorings", phase6_intents),
142                description: "Builder patterns, From/Into, etc.".to_string(),
143            });
144        }
145
146        // Estimate LOC: ~50 lines per intent on average
147        let estimated_loc = total_intents * 50;
148
149        Ok(ConversionResult {
150            phases,
151            total_intents,
152            estimated_loc,
153        })
154    }
155
156    /// Convert modules to CreateMod intents
157    fn convert_modules(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
158        let mut intents = Vec::new();
159
160        for module in &spec.modules {
161            // Skip "lib" as root - it's already the crate root
162            if module.name == "lib" {
163                // Process children directly
164                for child in &module.children {
165                    self.collect_module_intents(child, &[], &mut intents);
166                }
167            } else {
168                self.collect_module_intents(module, &[], &mut intents);
169            }
170        }
171
172        Ok(intents)
173    }
174
175    /// Recursively collect CreateMod intents
176    fn collect_module_intents(
177        &self,
178        module: &ModuleSpec,
179        parent_path: &[String],
180        intents: &mut Vec<Intent>,
181    ) {
182        intents.push(Intent::CreateMod {
183            parent_mod: parent_path.to_vec(),
184            mod_name: module.name.clone(),
185            content: String::new(),
186            is_pub: module.is_pub,
187        });
188
189        let mut new_path = parent_path.to_vec();
190        new_path.push(module.name.clone());
191
192        for child in &module.children {
193            self.collect_module_intents(child, &new_path, intents);
194        }
195    }
196
197    /// Convert common types (newtypes, value objects)
198    fn convert_common_types(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
199        let mut intents = Vec::new();
200
201        if let Some(common) = &spec.common_types {
202            // Newtypes
203            for newtype in &common.newtypes {
204                // Generate newtype struct code
205                let derives_str = if newtype.derives.is_empty() {
206                    String::new()
207                } else {
208                    format!("#[derive({})]\n", newtype.derives.join(", "))
209                };
210
211                let content = format!(
212                    "{}pub struct {}(pub {});",
213                    derives_str, newtype.name, newtype.inner
214                );
215
216                intents.push(Intent::AddItem {
217                    symbol_id: None,
218                    symbol_path: None,
219                    target_mod: Some(newtype.module.clone()),
220                    content,
221                    item_kind: ryo_source::ItemKind::Struct,
222                });
223            }
224
225            // Value objects (structs/enums)
226            for vo in &common.value_objects {
227                intents.extend(self.convert_entity(vo)?);
228            }
229        }
230
231        Ok(intents)
232    }
233
234    /// Convert entities to AddItem/AddEnum intents
235    fn convert_entities(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
236        let mut intents = Vec::new();
237
238        for entity in &spec.entities {
239            intents.extend(self.convert_entity(entity)?);
240        }
241
242        Ok(intents)
243    }
244
245    /// Convert a single entity
246    fn convert_entity(&self, entity: &EntitySpec) -> Result<Vec<Intent>, ConversionError> {
247        let mut intents = Vec::new();
248
249        match entity.kind {
250            EntityKind::Struct | EntityKind::Newtype => {
251                // Generate struct code
252                let derives_str = if entity.derives.is_empty() {
253                    String::new()
254                } else {
255                    format!("#[derive({})]\n", entity.derives.join(", "))
256                };
257
258                let fields_str: String = entity
259                    .fields
260                    .iter()
261                    .map(|f| {
262                        let vis = if f.is_pub { "pub " } else { "" };
263                        format!("    {}{}: {},", vis, f.name, f.ty)
264                    })
265                    .collect::<Vec<_>>()
266                    .join("\n");
267
268                let content = format!(
269                    "{}pub struct {} {{\n{}\n}}",
270                    derives_str, entity.name, fields_str
271                );
272
273                intents.push(Intent::AddItem {
274                    symbol_id: None,
275                    symbol_path: None,
276                    target_mod: Some(entity.module.clone()),
277                    content,
278                    item_kind: ryo_source::ItemKind::Struct,
279                });
280            }
281            EntityKind::Enum => {
282                // Use AddEnum intent
283                let variants: Vec<String> = entity
284                    .variants
285                    .iter()
286                    .map(|v| v.name().to_string())
287                    .collect();
288
289                intents.push(Intent::AddEnum {
290                    symbol_path: entity.module.clone(),
291                    name: entity.name.clone(),
292                    variants,
293                    is_pub: true,
294                    derives: entity.derives.clone(),
295                });
296            }
297        }
298
299        Ok(intents)
300    }
301
302    /// Convert error types
303    fn convert_errors(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
304        let mut intents = Vec::new();
305
306        for error in &spec.errors {
307            // Generate error enum with complex variants
308            let derives_str = if error.derives.is_empty() {
309                String::new()
310            } else {
311                format!("#[derive({})]\n", error.derives.join(", "))
312            };
313
314            let variants_str: String = error
315                .variants
316                .iter()
317                .map(|v| {
318                    let name = v.name();
319                    match v.variant_type() {
320                        None => format!("    {},", name),
321                        Some(vt) => {
322                            // Parse variant type: "struct:field1:Type1,field2:Type2"
323                            if let Some(fields) = vt.strip_prefix("struct:") {
324                                let field_parts: Vec<&str> = fields.split(',').collect();
325                                let fields_str: String = field_parts
326                                    .iter()
327                                    .map(|fp| {
328                                        let parts: Vec<&str> = fp.split(':').collect();
329                                        if parts.len() == 2 {
330                                            format!("{}: {}", parts[0], parts[1])
331                                        } else {
332                                            fp.to_string()
333                                        }
334                                    })
335                                    .collect::<Vec<_>>()
336                                    .join(", ");
337                                format!("    {} {{ {} }},", name, fields_str)
338                            } else {
339                                format!("    {}({}),", name, vt)
340                            }
341                        }
342                    }
343                })
344                .collect::<Vec<_>>()
345                .join("\n");
346
347            let content = format!(
348                "{}pub enum {} {{\n{}\n}}",
349                derives_str, error.name, variants_str
350            );
351
352            intents.push(Intent::AddItem {
353                symbol_id: None,
354                symbol_path: None,
355                target_mod: Some(error.module.clone()),
356                content,
357                item_kind: ryo_source::ItemKind::Enum,
358            });
359        }
360
361        Ok(intents)
362    }
363
364    /// Convert implementations to AddMethod intents
365    fn convert_implementations(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
366        let mut intents = Vec::new();
367
368        for impl_spec in &spec.implementations {
369            for method in &impl_spec.methods {
370                let self_param = method.self_param.map(|sp| match sp {
371                    SelfParamSpec::Ref => SelfParam::Ref,
372                    SelfParamSpec::Mut => SelfParam::Mut,
373                    SelfParamSpec::Owned => SelfParam::Owned,
374                });
375
376                intents.push(Intent::AddMethod {
377                    symbol_id: None,
378                    symbol_path: None,
379                    target_type: Some(impl_spec.target.clone()),
380                    method_name: method.name.clone(),
381                    params: method.params.clone(),
382                    return_type: method.return_type.clone(),
383                    body: method.body.clone(),
384                    is_pub: method.is_pub,
385                    self_param,
386                });
387            }
388        }
389
390        Ok(intents)
391    }
392
393    /// Convert refactoring specs
394    fn convert_refactors(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
395        let mut intents = Vec::new();
396
397        for refactor in &spec.refactors {
398            match refactor {
399                RefactorSpec::AddBuilderPattern { targets } => {
400                    // For each target, add builder-related methods
401                    for target in targets {
402                        // Add builder() method
403                        intents.push(Intent::AddMethod {
404                            symbol_id: None,
405                            symbol_path: None,
406                            target_type: Some(target.clone()),
407                            method_name: "builder".to_string(),
408                            params: vec![],
409                            return_type: Some(format!("{}Builder", target)),
410                            body: format!("{}Builder::default()", target),
411                            is_pub: true,
412                            self_param: None,
413                        });
414
415                        // Note: Full builder struct would require more complex generation
416                        // For now, we generate a stub that can be expanded
417                    }
418                }
419                RefactorSpec::AddFromInto { pairs } => {
420                    for (from, to) in pairs {
421                        // Add From<Inner> for Newtype
422                        let content = format!(
423                            "impl From<{}> for {} {{\n    fn from(v: {}) -> Self {{\n        Self(v)\n    }}\n}}",
424                            to, from, to
425                        );
426
427                        intents.push(Intent::AddItem {
428                            symbol_id: None,
429                            symbol_path: Some(from.clone()),
430                            target_mod: None,
431                            content,
432                            item_kind: ryo_source::ItemKind::Impl,
433                        });
434                    }
435                }
436                RefactorSpec::AddDefault { targets } => {
437                    for target in targets {
438                        intents.push(Intent::AddDerive {
439                            symbol_id: None,
440                            symbol_path: None,
441                            target_type: Some(target.clone()),
442                            derives: vec!["Default".to_string()],
443                        });
444                    }
445                }
446                RefactorSpec::OrganizeImports { target_modules } => match target_modules {
447                    TargetModules::All => {
448                        intents.push(Intent::OrganizeImports {
449                            target_mod: None,
450                            deduplicate: true,
451                            merge_groups: true,
452                        });
453                    }
454                    TargetModules::List(modules) => {
455                        for module in modules {
456                            intents.push(Intent::OrganizeImports {
457                                target_mod: Some(module.clone()),
458                                deduplicate: true,
459                                merge_groups: true,
460                            });
461                        }
462                    }
463                },
464            }
465        }
466
467        Ok(intents)
468    }
469
470    /// Create a Goal from intents
471    fn create_goal(&self, query: &str, intents: Vec<Intent>) -> Goal {
472        Goal::with_intents(query, intents).with_scope(
473            ScopeHint::new()
474                .with_file_patterns(vec!["src/**/*.rs".to_string()])
475                .with_estimated_scope(EstimatedScope::ProjectWide),
476        )
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use crate::spec_dsl::parser::parse_spec;
484
485    #[test]
486    fn test_convert_modules() {
487        let yaml = r#"
488project:
489  name: "test"
490  crate_name: "test_crate"
491
492modules:
493  - name: lib
494    is_pub: true
495    children:
496      - name: user
497        is_pub: true
498      - name: product
499        is_pub: true
500"#;
501
502        let spec = parse_spec(yaml).unwrap();
503        let converter = SpecToIntentConverter::new("test_crate");
504        let result = converter.convert(&spec).unwrap();
505
506        assert!(!result.phases.is_empty());
507        assert_eq!(result.phases[0].name, "Module Structure");
508        assert_eq!(result.phases[0].goal.intents.len(), 2); // user, product
509    }
510
511    #[test]
512    fn test_convert_entities() {
513        let yaml = r#"
514project:
515  name: "test"
516
517entities:
518  - name: User
519    module: user
520    fields:
521      - name: id
522        type: UserId
523      - name: name
524        type: String
525    derives: [Debug, Clone]
526
527  - name: UserStatus
528    module: user
529    kind: enum
530    variants:
531      - Active
532      - Inactive
533    derives: [Debug, Clone, Copy]
534"#;
535
536        let spec = parse_spec(yaml).unwrap();
537        let converter = SpecToIntentConverter::new("test_crate");
538        let result = converter.convert(&spec).unwrap();
539
540        // Should have Domain Entities phase
541        let entity_phase = result.phases.iter().find(|p| p.name == "Domain Entities");
542        assert!(entity_phase.is_some());
543
544        let phase = entity_phase.unwrap();
545        assert_eq!(phase.goal.intents.len(), 2);
546    }
547
548    #[test]
549    fn test_convert_implementations() {
550        let yaml = r#"
551project:
552  name: "test"
553
554implementations:
555  - target: UserId
556    methods:
557      - name: new
558        self_param: null
559        return_type: Self
560        body: "Self(uuid::Uuid::new_v4())"
561        is_pub: true
562      - name: inner
563        self_param: ref
564        return_type: "uuid::Uuid"
565        body: "self.0"
566        is_pub: true
567"#;
568
569        let spec = parse_spec(yaml).unwrap();
570        let converter = SpecToIntentConverter::new("test_crate");
571        let result = converter.convert(&spec).unwrap();
572
573        let impl_phase = result.phases.iter().find(|p| p.name == "Implementations");
574        assert!(impl_phase.is_some());
575
576        let phase = impl_phase.unwrap();
577        assert_eq!(phase.goal.intents.len(), 2);
578
579        // Check first method
580        if let Intent::AddMethod {
581            method_name,
582            self_param,
583            ..
584        } = &phase.goal.intents[0]
585        {
586            assert_eq!(method_name, "new");
587            assert!(self_param.is_none());
588        } else {
589            panic!("Expected AddMethod intent");
590        }
591
592        // Check second method
593        if let Intent::AddMethod {
594            method_name,
595            self_param,
596            ..
597        } = &phase.goal.intents[1]
598        {
599            assert_eq!(method_name, "inner");
600            assert_eq!(*self_param, Some(SelfParam::Ref));
601        } else {
602            panic!("Expected AddMethod intent");
603        }
604    }
605
606    #[test]
607    fn test_total_intent_count() {
608        let yaml = r#"
609project:
610  name: "ecommerce"
611
612modules:
613  - name: lib
614    children:
615      - name: user
616        is_pub: true
617      - name: product
618        is_pub: true
619
620entities:
621  - name: User
622    module: user
623    fields:
624      - name: id
625        type: UserId
626    derives: [Debug, Clone]
627
628implementations:
629  - target: User
630    methods:
631      - name: new
632        return_type: Self
633        body: "todo!()"
634        is_pub: true
635
636refactors:
637  - kind: OrganizeImports
638    target_modules: all
639"#;
640
641        let spec = parse_spec(yaml).unwrap();
642        let converter = SpecToIntentConverter::new("ecommerce");
643        let result = converter.convert(&spec).unwrap();
644
645        // 2 modules + 1 entity + 1 method + 1 organize = 5 intents
646        assert_eq!(result.total_intents, 5);
647        assert!(result.estimated_loc > 0);
648    }
649}