1use thiserror::Error;
6
7use super::types::*;
8use crate::intent::{EstimatedScope, Goal, Intent, ScopeHint, SelfParam};
9
10#[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#[derive(Debug, Clone)]
28pub struct ConversionResult {
29 pub phases: Vec<PhaseIntents>,
31
32 pub total_intents: usize,
34
35 pub estimated_loc: usize,
37}
38
39#[derive(Debug, Clone)]
41pub struct PhaseIntents {
42 pub name: String,
44
45 pub phase: usize,
47
48 pub goal: Goal,
50
51 pub description: String,
53}
54
55pub struct SpecToIntentConverter {
57 _crate_name: String,
59}
60
61impl SpecToIntentConverter {
62 pub fn new(crate_name: impl Into<String>) -> Self {
64 Self {
65 _crate_name: crate_name.into(),
66 }
67 }
68
69 pub fn convert(&self, spec: &DomainSpec) -> Result<ConversionResult, ConversionError> {
71 let mut phases = Vec::new();
72 let mut total_intents = 0;
73
74 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 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 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 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 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 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 let estimated_loc = total_intents * 50;
148
149 Ok(ConversionResult {
150 phases,
151 total_intents,
152 estimated_loc,
153 })
154 }
155
156 fn convert_modules(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
158 let mut intents = Vec::new();
159
160 for module in &spec.modules {
161 if module.name == "lib" {
163 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 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 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 for newtype in &common.newtypes {
204 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 for vo in &common.value_objects {
227 intents.extend(self.convert_entity(vo)?);
228 }
229 }
230
231 Ok(intents)
232 }
233
234 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 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 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 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 fn convert_errors(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
304 let mut intents = Vec::new();
305
306 for error in &spec.errors {
307 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 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 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 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 target in targets {
402 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 }
418 }
419 RefactorSpec::AddFromInto { pairs } => {
420 for (from, to) in pairs {
421 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 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); }
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 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 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 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 assert_eq!(result.total_intents, 5);
647 assert!(result.estimated_loc > 0);
648 }
649}