use ryo_analysis::context::AnalysisContext;
use ryo_analysis::query::SpecAliasData;
use ryo_analysis::{SymbolId, SymbolKind};
use ryo_executor::{SelfParam, Visibility};
use crate::MutationSpec;
pub trait SpecGenerator: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &str;
fn matches(&self, spec: &SpecAliasData) -> bool;
fn generate(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<MutationSpec>;
}
#[derive(Debug, Clone, Default)]
pub struct GeneratorOptions {
pub generate_accessors: bool,
pub generate_id_fields: bool,
pub generate_collection_fields: bool,
pub default_derives: Vec<String>,
}
impl GeneratorOptions {
pub fn new() -> Self {
Self {
generate_accessors: true,
generate_id_fields: true,
generate_collection_fields: false,
default_derives: vec!["Debug".into(), "Clone".into()],
}
}
pub fn with_accessors(mut self, enabled: bool) -> Self {
self.generate_accessors = enabled;
self
}
pub fn with_id_fields(mut self, enabled: bool) -> Self {
self.generate_id_fields = enabled;
self
}
pub fn with_collection_fields(mut self, enabled: bool) -> Self {
self.generate_collection_fields = enabled;
self
}
pub fn with_derives(mut self, derives: Vec<String>) -> Self {
self.default_derives = derives;
self
}
}
pub struct DomainSpecGenerator {
options: GeneratorOptions,
target_groups: Vec<String>,
}
impl DomainSpecGenerator {
pub fn new() -> Self {
Self {
options: GeneratorOptions::new(),
target_groups: vec!["DomainGroup".into()],
}
}
pub fn with_options(mut self, options: GeneratorOptions) -> Self {
self.options = options;
self
}
pub fn with_groups(mut self, groups: Vec<String>) -> Self {
self.target_groups = groups;
self
}
fn extract_relations(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<String> {
use super::is_framework_type;
let typeflow = ctx.typeflow_graph();
let mut relations = Vec::new();
let base_type = spec.wrapped_type_name.as_deref().unwrap_or("");
for used_id in typeflow.types_used_by(spec.alias_id) {
if let Some(path) = ctx.registry.path(used_id) {
let kind = ctx.registry.kind(used_id);
if !matches!(kind, Some(SymbolKind::Struct) | Some(SymbolKind::Enum)) {
continue;
}
let name = path.name();
if is_framework_type(name) || name == base_type {
continue;
}
relations.push(name.to_string());
}
}
relations
}
fn has_field_for(&self, ctx: &AnalysisContext, struct_id: SymbolId, target: &str) -> bool {
let graph = ctx.code_graph();
let target_lower = target.to_lowercase();
for child_id in graph.children_of(struct_id) {
if let Some(SymbolKind::Field) = ctx.registry.kind(child_id) {
if let Some(path) = ctx.registry.path(child_id) {
if path.name().to_lowercase().contains(&target_lower) {
return true;
}
}
}
}
false
}
fn has_method(&self, ctx: &AnalysisContext, struct_id: SymbolId, method_name: &str) -> bool {
let graph = ctx.code_graph();
for child_id in graph.children_of(struct_id) {
if let Some(SymbolKind::Method) = ctx.registry.kind(child_id) {
if let Some(path) = ctx.registry.path(child_id) {
if path.name() == method_name {
return true;
}
}
}
}
false
}
fn to_snake_case(&self, s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
}
result
}
}
impl Default for DomainSpecGenerator {
fn default() -> Self {
Self::new()
}
}
impl SpecGenerator for DomainSpecGenerator {
fn name(&self) -> &'static str {
"domain-spec-generator"
}
fn description(&self) -> &str {
"Generates struct fields and methods from domain Spec relations"
}
fn matches(&self, spec: &SpecAliasData) -> bool {
spec.wrapped_type_id.is_some()
}
fn generate(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<MutationSpec> {
let mut mutations = Vec::new();
let struct_id = match spec.wrapped_type_id {
Some(id) => id,
None => return mutations,
};
let _struct_name = match &spec.wrapped_type_name {
Some(name) => name.clone(),
None => return mutations,
};
let relations = self.extract_relations(ctx, spec);
for relation in &relations {
let field_name = format!("{}_id", self.to_snake_case(relation));
let field_type = format!("{}Id", relation);
if !self.has_field_for(ctx, struct_id, relation) && self.options.generate_id_fields {
mutations.push(MutationSpec::AddField {
target: ryo_executor::MutationTargetSymbol::ById(struct_id),
field_name: field_name.clone(),
field_type: field_type.clone(),
visibility: Visibility::Pub,
});
}
if self.options.generate_accessors && !self.has_method(ctx, struct_id, &field_name) {
mutations.push(MutationSpec::AddMethod {
target: ryo_executor::MutationTargetSymbol::ById(struct_id),
method_name: field_name.clone(),
params: vec![],
return_type: Some(format!("&{}", field_type)),
body: format!("&self.{}", field_name),
is_pub: true,
self_param: Some(SelfParam::Ref),
});
}
}
if !self.options.default_derives.is_empty() {
mutations.push(MutationSpec::AddDerive {
target: ryo_executor::MutationTargetSymbol::ById(struct_id),
derives: self.options.default_derives.clone(),
});
}
mutations
}
}
#[derive(Default)]
pub struct SpecGeneratorRegistry {
generators: Vec<Box<dyn SpecGenerator>>,
}
impl SpecGeneratorRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register<G: SpecGenerator + 'static>(&mut self, generator: G) {
self.generators.push(Box::new(generator));
}
pub fn generate_for(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<MutationSpec> {
let mut all_mutations = Vec::new();
for generator in &self.generators {
if generator.matches(spec) {
let mutations = generator.generate(ctx, spec);
all_mutations.extend(mutations);
}
}
all_mutations
}
pub fn generate_all(&self, ctx: &AnalysisContext) -> Vec<(String, Vec<MutationSpec>)> {
let mut results = Vec::new();
for symbol_id in ctx.registry.iter_by_kind(SymbolKind::TypeAlias) {
let path = match ctx.registry.path(symbol_id) {
Some(p) => p,
None => continue,
};
let alias_name = path.name();
if !alias_name.ends_with("Spec") || alias_name == "Spec" {
continue;
}
let base_type = &alias_name[..alias_name.len() - 4]; let wrapped_type_id = self.find_struct_by_name(ctx, base_type);
let spec_data = SpecAliasData {
alias_id: symbol_id,
alias_name: alias_name.to_string(),
wrapped_type_id,
wrapped_type_name: Some(base_type.to_string()),
group_idx: 0, source: ryo_analysis::query::SpecSource::TypeAlias,
};
let mutations = self.generate_for(ctx, &spec_data);
if !mutations.is_empty() {
results.push((alias_name.to_string(), mutations));
}
}
results
}
fn find_struct_by_name(&self, ctx: &AnalysisContext, name: &str) -> Option<SymbolId> {
for symbol_id in ctx.registry.iter_by_kind(SymbolKind::Struct) {
if let Some(path) = ctx.registry.path(symbol_id) {
if path.name() == name {
return Some(symbol_id);
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_to_snake_case() {
let gen = DomainSpecGenerator::new();
assert_eq!(gen.to_snake_case("Order"), "order");
assert_eq!(gen.to_snake_case("OrderItem"), "order_item");
assert_eq!(gen.to_snake_case("HTTPRequest"), "h_t_t_p_request");
}
#[test]
fn test_generator_options() {
let opts = GeneratorOptions::new()
.with_accessors(false)
.with_id_fields(true)
.with_derives(vec!["Serialize".into()]);
assert!(!opts.generate_accessors);
assert!(opts.generate_id_fields);
assert_eq!(opts.default_derives, vec!["Serialize"]);
}
#[test]
fn test_domain_spec_generator_new() {
let gen = DomainSpecGenerator::new();
assert_eq!(gen.name(), "domain-spec-generator");
assert!(gen.options.generate_accessors);
assert!(gen.options.generate_id_fields);
}
}