use ryo_analysis::context::AnalysisContext;
use ryo_analysis::{SymbolId, SymbolPath};
use crate::suggest::{
MutationSpec, OpportunityContext, OpportunityId, ParamDef, SafetyLevel, Suggest,
SuggestCategory, SuggestLocation, SuggestOpportunity, SuggestParams, SuggestResult,
};
fn create_generation_location(name: &str) -> SuggestLocation {
let symbol_id = SymbolId::parse("0v1").expect("valid dummy SymbolId");
let symbol_path = SymbolPath::builder("generated")
.push(name)
.build()
.unwrap_or_else(|_| SymbolPath::builder("generated").build().expect("path"));
SuggestLocation::new(symbol_id, symbol_path, "(generated)")
}
pub struct DomainStructSuggest {
default_derives: Vec<String>,
}
impl DomainStructSuggest {
pub fn new() -> Self {
Self {
default_derives: vec!["Debug".into(), "Clone".into()],
}
}
pub fn with_derives(mut self, derives: Vec<String>) -> Self {
self.default_derives = derives;
self
}
fn parse_fields(&self, fields_str: &str) -> Vec<(String, String)> {
if fields_str.is_empty() {
return vec![];
}
fields_str
.split(',')
.filter_map(|field| {
let parts: Vec<&str> = field.trim().split(':').collect();
if parts.len() == 2 {
Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
} else {
None
}
})
.collect()
}
}
impl Default for DomainStructSuggest {
fn default() -> Self {
Self::new()
}
}
impl Suggest for DomainStructSuggest {
fn name(&self) -> &'static str {
"domain-struct"
}
fn description(&self) -> &str {
"Generate a domain struct with derives and fields"
}
fn category(&self) -> SuggestCategory {
SuggestCategory::Pattern
}
fn safety_level(&self) -> SafetyLevel {
SafetyLevel::Confirm
}
fn rule_id(&self) -> Option<&str> {
Some("RG001")
}
fn accepts_params(&self) -> bool {
true
}
fn param_schema(&self) -> Vec<ParamDef> {
vec![
ParamDef::required("name", "Struct name (e.g., Order, User, Product)"),
ParamDef::optional(
"fields",
"Comma-separated fields (e.g., id:u64,name:String)",
),
ParamDef::optional("derives", "Comma-separated derives (default: Debug,Clone)"),
]
}
fn detect_with_params(
&self,
_ctx: &AnalysisContext,
_symbols: &[SymbolId],
params: &SuggestParams,
) -> Vec<SuggestOpportunity> {
let Some(name) = params.get("name") else {
return vec![];
};
let fields = params
.get("fields")
.map(|s| self.parse_fields(s))
.unwrap_or_default();
let derives = params
.get("derives")
.map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
.unwrap_or_else(|| self.default_derives.clone());
let message = format!(
"Generate struct `{}` with {} fields and derives {:?}",
name,
fields.len(),
derives
);
let location = create_generation_location(name);
vec![SuggestOpportunity::new(
OpportunityId::new(0),
vec![],
location,
message,
1.0,
OpportunityContext::Generation {
pattern: "domain-struct".to_string(),
params: params.clone(),
},
)]
}
fn detect(&self, _ctx: &AnalysisContext, _symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
vec![]
}
fn to_mutation_specs(
&self,
_ctx: &AnalysisContext,
opportunity: &SuggestOpportunity,
) -> SuggestResult<Vec<MutationSpec>> {
let OpportunityContext::Generation { params, .. } = &opportunity.context else {
return Ok(vec![]);
};
let Some(name) = params.get("name") else {
return Ok(vec![]);
};
let fields = params
.get("fields")
.map(|s| self.parse_fields(s))
.unwrap_or_default();
let derives = params
.get("derives")
.map(|s| s.split(',').map(|d| d.trim().to_string()).collect())
.unwrap_or_else(|| self.default_derives.clone());
let mut code = String::new();
if !derives.is_empty() {
code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
}
code.push_str(&format!("pub struct {} {{\n", name));
for (field_name, field_type) in &fields {
code.push_str(&format!(" pub {}: {},\n", field_name, field_type));
}
code.push('}');
let target = SymbolPath::parse("crate")
.unwrap_or_else(|_| SymbolPath::builder("crate").build().expect("crate path"));
Ok(vec![MutationSpec::AddItem {
target: ryo_executor::MutationTargetSymbol::ByPath(Box::new(target)),
content: code,
position: ryo_executor::InsertPosition::Bottom,
}])
}
}
pub struct ApiPatternSuggest {
methods: Vec<ApiMethod>,
}
#[derive(Clone)]
struct ApiMethod {
name: &'static str,
has_id_param: bool,
has_entity_param: bool,
returns_entity: bool,
}
impl ApiPatternSuggest {
pub fn new() -> Self {
Self {
methods: vec![
ApiMethod {
name: "get",
has_id_param: true,
has_entity_param: false,
returns_entity: true,
},
ApiMethod {
name: "list",
has_id_param: false,
has_entity_param: false,
returns_entity: true,
},
ApiMethod {
name: "create",
has_id_param: false,
has_entity_param: true,
returns_entity: true,
},
ApiMethod {
name: "update",
has_id_param: true,
has_entity_param: true,
returns_entity: true,
},
ApiMethod {
name: "delete",
has_id_param: true,
has_entity_param: false,
returns_entity: false,
},
],
}
}
}
impl Default for ApiPatternSuggest {
fn default() -> Self {
Self::new()
}
}
impl Suggest for ApiPatternSuggest {
fn name(&self) -> &'static str {
"api-pattern"
}
fn description(&self) -> &str {
"Generate API struct with CRUD methods (get, list, create, update, delete)"
}
fn category(&self) -> SuggestCategory {
SuggestCategory::Pattern
}
fn safety_level(&self) -> SafetyLevel {
SafetyLevel::Confirm
}
fn rule_id(&self) -> Option<&str> {
Some("RG002")
}
fn accepts_params(&self) -> bool {
true
}
fn param_schema(&self) -> Vec<ParamDef> {
vec![
ParamDef::required("name", "API name prefix (e.g., Order -> OrderAPI)"),
ParamDef::optional("entity", "Entity type name (default: same as name)"),
ParamDef::optional(
"methods",
"Comma-separated methods to generate (default: get,list,create,update,delete)",
),
]
}
fn detect_with_params(
&self,
_ctx: &AnalysisContext,
_symbols: &[SymbolId],
params: &SuggestParams,
) -> Vec<SuggestOpportunity> {
let Some(name) = params.get("name") else {
return vec![];
};
let api_name = format!("{}API", name);
let entity = params
.get("entity")
.cloned()
.unwrap_or_else(|| name.clone());
let method_names: Vec<&str> = params
.get("methods")
.map(|s| s.split(',').map(|m| m.trim()).collect())
.unwrap_or_else(|| vec!["get", "list", "create", "update", "delete"]);
let message = format!(
"Generate `{}` with methods: {} for entity `{}`",
api_name,
method_names.join(", "),
entity
);
let location = create_generation_location(&api_name);
vec![SuggestOpportunity::new(
OpportunityId::new(0),
vec![],
location,
message,
1.0,
OpportunityContext::Generation {
pattern: "api-pattern".to_string(),
params: params.clone(),
},
)]
}
fn detect(&self, _ctx: &AnalysisContext, _symbols: &[SymbolId]) -> Vec<SuggestOpportunity> {
vec![]
}
fn to_mutation_specs(
&self,
_ctx: &AnalysisContext,
opportunity: &SuggestOpportunity,
) -> SuggestResult<Vec<MutationSpec>> {
let OpportunityContext::Generation { params, .. } = &opportunity.context else {
return Ok(vec![]);
};
let Some(name) = params.get("name") else {
return Ok(vec![]);
};
let api_name = format!("{}API", name);
let entity = params
.get("entity")
.cloned()
.unwrap_or_else(|| name.clone());
let id_type = format!("{}Id", entity);
let method_filter: Option<Vec<&str>> = params
.get("methods")
.map(|s| s.split(',').map(|m| m.trim()).collect());
let mut specs = Vec::new();
let struct_code = format!("pub struct {} {{}}", api_name);
let target = SymbolPath::parse("crate")
.unwrap_or_else(|_| SymbolPath::builder("crate").build().expect("crate path"));
specs.push(MutationSpec::AddItem {
target: ryo_executor::MutationTargetSymbol::ByPath(Box::new(target)),
content: struct_code,
position: ryo_executor::InsertPosition::Bottom,
});
for method in &self.methods {
if let Some(ref filter) = method_filter {
if !filter.contains(&method.name) {
continue;
}
}
let mut method_params: Vec<(String, String)> = vec![];
if method.has_id_param {
method_params.push(("id".to_string(), id_type.clone()));
}
if method.has_entity_param {
method_params.push(("entity".to_string(), entity.clone()));
}
let return_type = if method.returns_entity {
if method.name == "list" {
format!("Result<Vec<{}>, Error>", entity)
} else {
format!("Result<{}, Error>", entity)
}
} else {
"Result<(), Error>".to_string()
};
let body = "todo!()".to_string();
specs.push(MutationSpec::AddMethod {
target: ryo_executor::MutationTargetSymbol::ByKindAndName(
ryo_executor::ItemKind::Struct,
api_name.clone(),
),
method_name: method.name.to_string(),
params: method_params,
return_type: Some(return_type),
body,
is_pub: true,
self_param: Some(ryo_executor::SelfParam::Ref),
});
}
Ok(specs)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_domain_struct_param_schema() {
let suggest = DomainStructSuggest::new();
assert!(suggest.accepts_params());
assert_eq!(suggest.param_schema().len(), 3);
}
#[test]
fn test_domain_struct_parse_fields() {
let suggest = DomainStructSuggest::new();
let fields = suggest.parse_fields("id:u64,name:String,active:bool");
assert_eq!(fields.len(), 3);
assert_eq!(fields[0], ("id".to_string(), "u64".to_string()));
assert_eq!(fields[1], ("name".to_string(), "String".to_string()));
assert_eq!(fields[2], ("active".to_string(), "bool".to_string()));
}
#[test]
fn test_api_pattern_param_schema() {
let suggest = ApiPatternSuggest::new();
assert!(suggest.accepts_params());
assert_eq!(suggest.param_schema().len(), 3);
}
#[test]
fn test_api_pattern_name() {
let suggest = ApiPatternSuggest::new();
assert_eq!(suggest.name(), "api-pattern");
assert_eq!(suggest.rule_id(), Some("RG002"));
}
}