use thiserror::Error;
use super::types::*;
use crate::intent::{EstimatedScope, Goal, Intent, ScopeHint, SelfParam};
#[derive(Debug, Error)]
pub enum ConversionError {
#[error("Invalid module path: {0}")]
InvalidModulePath(String),
#[error("Missing required field: {0}")]
MissingField(String),
#[error("Invalid variant type format: {0}")]
InvalidVariantType(String),
#[error("Unsupported refactor kind: {0}")]
UnsupportedRefactor(String),
}
#[derive(Debug, Clone)]
pub struct ConversionResult {
pub phases: Vec<PhaseIntents>,
pub total_intents: usize,
pub estimated_loc: usize,
}
#[derive(Debug, Clone)]
pub struct PhaseIntents {
pub name: String,
pub phase: usize,
pub goal: Goal,
pub description: String,
}
pub struct SpecToIntentConverter {
_crate_name: String,
}
impl SpecToIntentConverter {
pub fn new(crate_name: impl Into<String>) -> Self {
Self {
_crate_name: crate_name.into(),
}
}
pub fn convert(&self, spec: &DomainSpec) -> Result<ConversionResult, ConversionError> {
let mut phases = Vec::new();
let mut total_intents = 0;
let phase1_intents = self.convert_modules(spec)?;
if !phase1_intents.is_empty() {
total_intents += phase1_intents.len();
phases.push(PhaseIntents {
name: "Module Structure".to_string(),
phase: 1,
goal: self.create_goal("Phase 1: Create module structure", phase1_intents),
description: "Create module hierarchy".to_string(),
});
}
let phase2_intents = self.convert_common_types(spec)?;
if !phase2_intents.is_empty() {
total_intents += phase2_intents.len();
phases.push(PhaseIntents {
name: "Common Types".to_string(),
phase: 2,
goal: self.create_goal("Phase 2: Create common types", phase2_intents),
description: "Create newtypes, value objects".to_string(),
});
}
let phase3_intents = self.convert_entities(spec)?;
if !phase3_intents.is_empty() {
total_intents += phase3_intents.len();
phases.push(PhaseIntents {
name: "Domain Entities".to_string(),
phase: 3,
goal: self.create_goal("Phase 3: Create domain entities", phase3_intents),
description: "Create structs, enums".to_string(),
});
}
let phase4_intents = self.convert_errors(spec)?;
if !phase4_intents.is_empty() {
total_intents += phase4_intents.len();
phases.push(PhaseIntents {
name: "Error Types".to_string(),
phase: 4,
goal: self.create_goal("Phase 4: Create error types", phase4_intents),
description: "Create error enums".to_string(),
});
}
let phase5_intents = self.convert_implementations(spec)?;
if !phase5_intents.is_empty() {
total_intents += phase5_intents.len();
phases.push(PhaseIntents {
name: "Implementations".to_string(),
phase: 5,
goal: self.create_goal("Phase 5: Add methods", phase5_intents),
description: "Add impl blocks and methods".to_string(),
});
}
let phase6_intents = self.convert_refactors(spec)?;
if !phase6_intents.is_empty() {
total_intents += phase6_intents.len();
phases.push(PhaseIntents {
name: "Refactoring".to_string(),
phase: 6,
goal: self.create_goal("Phase 6: Apply refactorings", phase6_intents),
description: "Builder patterns, From/Into, etc.".to_string(),
});
}
let estimated_loc = total_intents * 50;
Ok(ConversionResult {
phases,
total_intents,
estimated_loc,
})
}
fn convert_modules(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
let mut intents = Vec::new();
for module in &spec.modules {
if module.name == "lib" {
for child in &module.children {
self.collect_module_intents(child, &[], &mut intents);
}
} else {
self.collect_module_intents(module, &[], &mut intents);
}
}
Ok(intents)
}
fn collect_module_intents(
&self,
module: &ModuleSpec,
parent_path: &[String],
intents: &mut Vec<Intent>,
) {
intents.push(Intent::CreateMod {
parent_mod: parent_path.to_vec(),
mod_name: module.name.clone(),
content: String::new(),
is_pub: module.is_pub,
});
let mut new_path = parent_path.to_vec();
new_path.push(module.name.clone());
for child in &module.children {
self.collect_module_intents(child, &new_path, intents);
}
}
fn convert_common_types(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
let mut intents = Vec::new();
if let Some(common) = &spec.common_types {
for newtype in &common.newtypes {
let derives_str = if newtype.derives.is_empty() {
String::new()
} else {
format!("#[derive({})]\n", newtype.derives.join(", "))
};
let content = format!(
"{}pub struct {}(pub {});",
derives_str, newtype.name, newtype.inner
);
intents.push(Intent::AddItem {
symbol_id: None,
symbol_path: None,
target_mod: Some(newtype.module.clone()),
content,
item_kind: ryo_source::ItemKind::Struct,
});
}
for vo in &common.value_objects {
intents.extend(self.convert_entity(vo)?);
}
}
Ok(intents)
}
fn convert_entities(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
let mut intents = Vec::new();
for entity in &spec.entities {
intents.extend(self.convert_entity(entity)?);
}
Ok(intents)
}
fn convert_entity(&self, entity: &EntitySpec) -> Result<Vec<Intent>, ConversionError> {
let mut intents = Vec::new();
match entity.kind {
EntityKind::Struct | EntityKind::Newtype => {
let derives_str = if entity.derives.is_empty() {
String::new()
} else {
format!("#[derive({})]\n", entity.derives.join(", "))
};
let fields_str: String = entity
.fields
.iter()
.map(|f| {
let vis = if f.is_pub { "pub " } else { "" };
format!(" {}{}: {},", vis, f.name, f.ty)
})
.collect::<Vec<_>>()
.join("\n");
let content = format!(
"{}pub struct {} {{\n{}\n}}",
derives_str, entity.name, fields_str
);
intents.push(Intent::AddItem {
symbol_id: None,
symbol_path: None,
target_mod: Some(entity.module.clone()),
content,
item_kind: ryo_source::ItemKind::Struct,
});
}
EntityKind::Enum => {
let variants: Vec<String> = entity
.variants
.iter()
.map(|v| v.name().to_string())
.collect();
intents.push(Intent::AddEnum {
symbol_path: entity.module.clone(),
name: entity.name.clone(),
variants,
is_pub: true,
derives: entity.derives.clone(),
});
}
}
Ok(intents)
}
fn convert_errors(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
let mut intents = Vec::new();
for error in &spec.errors {
let derives_str = if error.derives.is_empty() {
String::new()
} else {
format!("#[derive({})]\n", error.derives.join(", "))
};
let variants_str: String = error
.variants
.iter()
.map(|v| {
let name = v.name();
match v.variant_type() {
None => format!(" {},", name),
Some(vt) => {
if let Some(fields) = vt.strip_prefix("struct:") {
let field_parts: Vec<&str> = fields.split(',').collect();
let fields_str: String = field_parts
.iter()
.map(|fp| {
let parts: Vec<&str> = fp.split(':').collect();
if parts.len() == 2 {
format!("{}: {}", parts[0], parts[1])
} else {
fp.to_string()
}
})
.collect::<Vec<_>>()
.join(", ");
format!(" {} {{ {} }},", name, fields_str)
} else {
format!(" {}({}),", name, vt)
}
}
}
})
.collect::<Vec<_>>()
.join("\n");
let content = format!(
"{}pub enum {} {{\n{}\n}}",
derives_str, error.name, variants_str
);
intents.push(Intent::AddItem {
symbol_id: None,
symbol_path: None,
target_mod: Some(error.module.clone()),
content,
item_kind: ryo_source::ItemKind::Enum,
});
}
Ok(intents)
}
fn convert_implementations(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
let mut intents = Vec::new();
for impl_spec in &spec.implementations {
for method in &impl_spec.methods {
let self_param = method.self_param.map(|sp| match sp {
SelfParamSpec::Ref => SelfParam::Ref,
SelfParamSpec::Mut => SelfParam::Mut,
SelfParamSpec::Owned => SelfParam::Owned,
});
intents.push(Intent::AddMethod {
symbol_id: None,
symbol_path: None,
target_type: Some(impl_spec.target.clone()),
method_name: method.name.clone(),
params: method.params.clone(),
return_type: method.return_type.clone(),
body: method.body.clone(),
is_pub: method.is_pub,
self_param,
});
}
}
Ok(intents)
}
fn convert_refactors(&self, spec: &DomainSpec) -> Result<Vec<Intent>, ConversionError> {
let mut intents = Vec::new();
for refactor in &spec.refactors {
match refactor {
RefactorSpec::AddBuilderPattern { targets } => {
for target in targets {
intents.push(Intent::AddMethod {
symbol_id: None,
symbol_path: None,
target_type: Some(target.clone()),
method_name: "builder".to_string(),
params: vec![],
return_type: Some(format!("{}Builder", target)),
body: format!("{}Builder::default()", target),
is_pub: true,
self_param: None,
});
}
}
RefactorSpec::AddFromInto { pairs } => {
for (from, to) in pairs {
let content = format!(
"impl From<{}> for {} {{\n fn from(v: {}) -> Self {{\n Self(v)\n }}\n}}",
to, from, to
);
intents.push(Intent::AddItem {
symbol_id: None,
symbol_path: Some(from.clone()),
target_mod: None,
content,
item_kind: ryo_source::ItemKind::Impl,
});
}
}
RefactorSpec::AddDefault { targets } => {
for target in targets {
intents.push(Intent::AddDerive {
symbol_id: None,
symbol_path: None,
target_type: Some(target.clone()),
derives: vec!["Default".to_string()],
});
}
}
RefactorSpec::OrganizeImports { target_modules } => match target_modules {
TargetModules::All => {
intents.push(Intent::OrganizeImports {
target_mod: None,
deduplicate: true,
merge_groups: true,
});
}
TargetModules::List(modules) => {
for module in modules {
intents.push(Intent::OrganizeImports {
target_mod: Some(module.clone()),
deduplicate: true,
merge_groups: true,
});
}
}
},
}
}
Ok(intents)
}
fn create_goal(&self, query: &str, intents: Vec<Intent>) -> Goal {
Goal::with_intents(query, intents).with_scope(
ScopeHint::new()
.with_file_patterns(vec!["src/**/*.rs".to_string()])
.with_estimated_scope(EstimatedScope::ProjectWide),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::spec_dsl::parser::parse_spec;
#[test]
fn test_convert_modules() {
let yaml = r#"
project:
name: "test"
crate_name: "test_crate"
modules:
- name: lib
is_pub: true
children:
- name: user
is_pub: true
- name: product
is_pub: true
"#;
let spec = parse_spec(yaml).unwrap();
let converter = SpecToIntentConverter::new("test_crate");
let result = converter.convert(&spec).unwrap();
assert!(!result.phases.is_empty());
assert_eq!(result.phases[0].name, "Module Structure");
assert_eq!(result.phases[0].goal.intents.len(), 2); }
#[test]
fn test_convert_entities() {
let yaml = r#"
project:
name: "test"
entities:
- name: User
module: user
fields:
- name: id
type: UserId
- name: name
type: String
derives: [Debug, Clone]
- name: UserStatus
module: user
kind: enum
variants:
- Active
- Inactive
derives: [Debug, Clone, Copy]
"#;
let spec = parse_spec(yaml).unwrap();
let converter = SpecToIntentConverter::new("test_crate");
let result = converter.convert(&spec).unwrap();
let entity_phase = result.phases.iter().find(|p| p.name == "Domain Entities");
assert!(entity_phase.is_some());
let phase = entity_phase.unwrap();
assert_eq!(phase.goal.intents.len(), 2);
}
#[test]
fn test_convert_implementations() {
let yaml = r#"
project:
name: "test"
implementations:
- target: UserId
methods:
- name: new
self_param: null
return_type: Self
body: "Self(uuid::Uuid::new_v4())"
is_pub: true
- name: inner
self_param: ref
return_type: "uuid::Uuid"
body: "self.0"
is_pub: true
"#;
let spec = parse_spec(yaml).unwrap();
let converter = SpecToIntentConverter::new("test_crate");
let result = converter.convert(&spec).unwrap();
let impl_phase = result.phases.iter().find(|p| p.name == "Implementations");
assert!(impl_phase.is_some());
let phase = impl_phase.unwrap();
assert_eq!(phase.goal.intents.len(), 2);
if let Intent::AddMethod {
method_name,
self_param,
..
} = &phase.goal.intents[0]
{
assert_eq!(method_name, "new");
assert!(self_param.is_none());
} else {
panic!("Expected AddMethod intent");
}
if let Intent::AddMethod {
method_name,
self_param,
..
} = &phase.goal.intents[1]
{
assert_eq!(method_name, "inner");
assert_eq!(*self_param, Some(SelfParam::Ref));
} else {
panic!("Expected AddMethod intent");
}
}
#[test]
fn test_total_intent_count() {
let yaml = r#"
project:
name: "ecommerce"
modules:
- name: lib
children:
- name: user
is_pub: true
- name: product
is_pub: true
entities:
- name: User
module: user
fields:
- name: id
type: UserId
derives: [Debug, Clone]
implementations:
- target: User
methods:
- name: new
return_type: Self
body: "todo!()"
is_pub: true
refactors:
- kind: OrganizeImports
target_modules: all
"#;
let spec = parse_spec(yaml).unwrap();
let converter = SpecToIntentConverter::new("ecommerce");
let result = converter.convert(&spec).unwrap();
assert_eq!(result.total_intents, 5);
assert!(result.estimated_loc > 0);
}
}