Skip to main content

ryo_suggest/spec/
generator.rs

1//! SpecGenerator - Generate code from Spec definitions
2//!
3//! This module provides Spec-first code generation, complementing the
4//! problem-detection approach of Suggest.
5//!
6//! # Architecture
7//!
8//! ```text
9//! ┌─────────────────────────────────────────────────────────────────┐
10//! │  Suggest (Problem Detection)    vs    SpecGenerator (Creation)  │
11//! │  ────────────────────────────────────────────────────────────── │
12//! │  Code → detect() → Problems       Spec → generate() → New Code  │
13//! └─────────────────────────────────────────────────────────────────┘
14//! ```
15//!
16//! # Usage
17//!
18//! ```ignore
19//! use ryo_suggest::spec::{SpecGenerator, DomainSpecGenerator};
20//!
21//! let generator = DomainSpecGenerator::new();
22//! let specs = generator.generate(&ctx, &spec_alias_data);
23//! // specs: Vec<MutationSpec> containing AddField, AddMethod, etc.
24//! ```
25
26use ryo_analysis::context::AnalysisContext;
27use ryo_analysis::query::SpecAliasData;
28use ryo_analysis::{SymbolId, SymbolKind};
29use ryo_executor::{SelfParam, Visibility};
30
31use crate::MutationSpec;
32
33/// Trait for generating code from Spec definitions
34///
35/// Unlike `Suggest` which detects problems in existing code,
36/// `SpecGenerator` creates new code based on Spec definitions.
37pub trait SpecGenerator: Send + Sync {
38    /// Generator name for identification
39    fn name(&self) -> &'static str;
40
41    /// Description of what this generator creates
42    fn description(&self) -> &str;
43
44    /// Check if this generator applies to the given Spec
45    fn matches(&self, spec: &SpecAliasData) -> bool;
46
47    /// Generate MutationSpecs from a Spec definition
48    ///
49    /// Returns structured MutationSpecs (AddField, AddMethod, etc.)
50    /// rather than raw code strings when possible.
51    fn generate(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<MutationSpec>;
52}
53
54/// Options for code generation
55#[derive(Debug, Clone, Default)]
56pub struct GeneratorOptions {
57    /// Generate accessor methods for relations
58    pub generate_accessors: bool,
59    /// Generate ID field for relations (e.g., order_id: OrderId)
60    pub generate_id_fields: bool,
61    /// Generate collection fields for relations (e.g., orders: Vec<Order>)
62    pub generate_collection_fields: bool,
63    /// Default derives to add
64    pub default_derives: Vec<String>,
65}
66
67impl GeneratorOptions {
68    pub fn new() -> Self {
69        Self {
70            generate_accessors: true,
71            generate_id_fields: true,
72            generate_collection_fields: false,
73            default_derives: vec!["Debug".into(), "Clone".into()],
74        }
75    }
76
77    pub fn with_accessors(mut self, enabled: bool) -> Self {
78        self.generate_accessors = enabled;
79        self
80    }
81
82    pub fn with_id_fields(mut self, enabled: bool) -> Self {
83        self.generate_id_fields = enabled;
84        self
85    }
86
87    pub fn with_collection_fields(mut self, enabled: bool) -> Self {
88        self.generate_collection_fields = enabled;
89        self
90    }
91
92    pub fn with_derives(mut self, derives: Vec<String>) -> Self {
93        self.default_derives = derives;
94        self
95    }
96}
97
98/// Generator for domain model Specs
99///
100/// Generates struct fields and methods based on Spec relations.
101///
102/// # Generated Code
103///
104/// For `type UserSpec = Spec<DomainGroup, User, [DependsOn<Order>]>`:
105///
106/// - `AddField { field_name: "order_id", field_type: "OrderId" }`
107/// - `AddMethod { method_name: "order_id", return_type: "&OrderId" }`
108/// - `AddDerive { derives: ["Debug", "Clone"] }` (if not present)
109pub struct DomainSpecGenerator {
110    options: GeneratorOptions,
111    /// Groups this generator applies to
112    target_groups: Vec<String>,
113}
114
115impl DomainSpecGenerator {
116    pub fn new() -> Self {
117        Self {
118            options: GeneratorOptions::new(),
119            target_groups: vec!["DomainGroup".into()],
120        }
121    }
122
123    pub fn with_options(mut self, options: GeneratorOptions) -> Self {
124        self.options = options;
125        self
126    }
127
128    pub fn with_groups(mut self, groups: Vec<String>) -> Self {
129        self.target_groups = groups;
130        self
131    }
132
133    /// Extract relation targets from a Spec (types it depends on)
134    fn extract_relations(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<String> {
135        use super::is_framework_type;
136
137        let typeflow = ctx.typeflow_graph();
138        let mut relations = Vec::new();
139
140        let base_type = spec.wrapped_type_name.as_deref().unwrap_or("");
141
142        for used_id in typeflow.types_used_by(spec.alias_id) {
143            if let Some(path) = ctx.registry.path(used_id) {
144                let kind = ctx.registry.kind(used_id);
145
146                if !matches!(kind, Some(SymbolKind::Struct) | Some(SymbolKind::Enum)) {
147                    continue;
148                }
149
150                let name = path.name();
151                if is_framework_type(name) || name == base_type {
152                    continue;
153                }
154
155                relations.push(name.to_string());
156            }
157        }
158
159        relations
160    }
161
162    /// Check if struct has a field for the relation
163    fn has_field_for(&self, ctx: &AnalysisContext, struct_id: SymbolId, target: &str) -> bool {
164        let graph = ctx.code_graph();
165        let target_lower = target.to_lowercase();
166
167        for child_id in graph.children_of(struct_id) {
168            if let Some(SymbolKind::Field) = ctx.registry.kind(child_id) {
169                if let Some(path) = ctx.registry.path(child_id) {
170                    if path.name().to_lowercase().contains(&target_lower) {
171                        return true;
172                    }
173                }
174            }
175        }
176
177        false
178    }
179
180    /// Check if struct has a method (simplified - checks children)
181    fn has_method(&self, ctx: &AnalysisContext, struct_id: SymbolId, method_name: &str) -> bool {
182        let graph = ctx.code_graph();
183
184        // Check children (methods in impl blocks are children of the type)
185        for child_id in graph.children_of(struct_id) {
186            if let Some(SymbolKind::Method) = ctx.registry.kind(child_id) {
187                if let Some(path) = ctx.registry.path(child_id) {
188                    if path.name() == method_name {
189                        return true;
190                    }
191                }
192            }
193        }
194
195        false
196    }
197
198    /// Convert PascalCase to snake_case
199    fn to_snake_case(&self, s: &str) -> String {
200        let mut result = String::new();
201        for (i, c) in s.chars().enumerate() {
202            if c.is_uppercase() && i > 0 {
203                result.push('_');
204            }
205            result.push(c.to_ascii_lowercase());
206        }
207        result
208    }
209}
210
211impl Default for DomainSpecGenerator {
212    fn default() -> Self {
213        Self::new()
214    }
215}
216
217impl SpecGenerator for DomainSpecGenerator {
218    fn name(&self) -> &'static str {
219        "domain-spec-generator"
220    }
221
222    fn description(&self) -> &str {
223        "Generates struct fields and methods from domain Spec relations"
224    }
225
226    fn matches(&self, spec: &SpecAliasData) -> bool {
227        // Check if spec belongs to one of our target groups
228        // We need to get the group name, but SpecAliasData only has group_idx
229        // For now, match all specs with wrapped types
230        spec.wrapped_type_id.is_some()
231    }
232
233    fn generate(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<MutationSpec> {
234        let mut mutations = Vec::new();
235
236        // Get the wrapped struct
237        let struct_id = match spec.wrapped_type_id {
238            Some(id) => id,
239            None => return mutations,
240        };
241
242        let _struct_name = match &spec.wrapped_type_name {
243            Some(name) => name.clone(),
244            None => return mutations,
245        };
246
247        // Extract relations
248        let relations = self.extract_relations(ctx, spec);
249
250        // Generate fields for each relation
251        for relation in &relations {
252            let field_name = format!("{}_id", self.to_snake_case(relation));
253            let field_type = format!("{}Id", relation);
254
255            // Check if field already exists
256            if !self.has_field_for(ctx, struct_id, relation) && self.options.generate_id_fields {
257                mutations.push(MutationSpec::AddField {
258                    target: ryo_executor::MutationTargetSymbol::ById(struct_id),
259                    field_name: field_name.clone(),
260                    field_type: field_type.clone(),
261                    visibility: Visibility::Pub,
262                });
263            }
264
265            // Generate accessor method
266            if self.options.generate_accessors && !self.has_method(ctx, struct_id, &field_name) {
267                mutations.push(MutationSpec::AddMethod {
268                    target: ryo_executor::MutationTargetSymbol::ById(struct_id),
269                    method_name: field_name.clone(),
270                    params: vec![],
271                    return_type: Some(format!("&{}", field_type)),
272                    body: format!("&self.{}", field_name),
273                    is_pub: true,
274                    self_param: Some(SelfParam::Ref),
275                });
276            }
277        }
278
279        // Add default derives if configured
280        if !self.options.default_derives.is_empty() {
281            // Check which derives are missing (simplified - always add for now)
282            mutations.push(MutationSpec::AddDerive {
283                target: ryo_executor::MutationTargetSymbol::ById(struct_id),
284                derives: self.options.default_derives.clone(),
285            });
286        }
287
288        mutations
289    }
290}
291
292/// Registry for SpecGenerators
293#[derive(Default)]
294pub struct SpecGeneratorRegistry {
295    generators: Vec<Box<dyn SpecGenerator>>,
296}
297
298impl SpecGeneratorRegistry {
299    pub fn new() -> Self {
300        Self::default()
301    }
302
303    /// Register a generator
304    pub fn register<G: SpecGenerator + 'static>(&mut self, generator: G) {
305        self.generators.push(Box::new(generator));
306    }
307
308    /// Generate MutationSpecs for a Spec using all matching generators
309    pub fn generate_for(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<MutationSpec> {
310        let mut all_mutations = Vec::new();
311
312        for generator in &self.generators {
313            if generator.matches(spec) {
314                let mutations = generator.generate(ctx, spec);
315                all_mutations.extend(mutations);
316            }
317        }
318
319        all_mutations
320    }
321
322    /// Generate MutationSpecs for all Specs in the context
323    ///
324    /// Iterates all TypeAliases matching Spec pattern and generates code.
325    pub fn generate_all(&self, ctx: &AnalysisContext) -> Vec<(String, Vec<MutationSpec>)> {
326        let mut results = Vec::new();
327
328        // Find all TypeAliases that look like Specs
329        for symbol_id in ctx.registry.iter_by_kind(SymbolKind::TypeAlias) {
330            let path = match ctx.registry.path(symbol_id) {
331                Some(p) => p,
332                None => continue,
333            };
334
335            let alias_name = path.name();
336            if !alias_name.ends_with("Spec") || alias_name == "Spec" {
337                continue;
338            }
339
340            // Build SpecAliasData manually
341            let base_type = &alias_name[..alias_name.len() - 4]; // Remove "Spec"
342            let wrapped_type_id = self.find_struct_by_name(ctx, base_type);
343
344            let spec_data = SpecAliasData {
345                alias_id: symbol_id,
346                alias_name: alias_name.to_string(),
347                wrapped_type_id,
348                wrapped_type_name: Some(base_type.to_string()),
349                group_idx: 0, // Unknown
350                source: ryo_analysis::query::SpecSource::TypeAlias,
351            };
352
353            let mutations = self.generate_for(ctx, &spec_data);
354            if !mutations.is_empty() {
355                results.push((alias_name.to_string(), mutations));
356            }
357        }
358
359        results
360    }
361
362    /// Find struct by name
363    fn find_struct_by_name(&self, ctx: &AnalysisContext, name: &str) -> Option<SymbolId> {
364        for symbol_id in ctx.registry.iter_by_kind(SymbolKind::Struct) {
365            if let Some(path) = ctx.registry.path(symbol_id) {
366                if path.name() == name {
367                    return Some(symbol_id);
368                }
369            }
370        }
371        None
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use super::*;
378
379    #[test]
380    fn test_to_snake_case() {
381        let gen = DomainSpecGenerator::new();
382        assert_eq!(gen.to_snake_case("Order"), "order");
383        assert_eq!(gen.to_snake_case("OrderItem"), "order_item");
384        assert_eq!(gen.to_snake_case("HTTPRequest"), "h_t_t_p_request");
385    }
386
387    #[test]
388    fn test_generator_options() {
389        let opts = GeneratorOptions::new()
390            .with_accessors(false)
391            .with_id_fields(true)
392            .with_derives(vec!["Serialize".into()]);
393
394        assert!(!opts.generate_accessors);
395        assert!(opts.generate_id_fields);
396        assert_eq!(opts.default_derives, vec!["Serialize"]);
397    }
398
399    #[test]
400    fn test_domain_spec_generator_new() {
401        let gen = DomainSpecGenerator::new();
402        assert_eq!(gen.name(), "domain-spec-generator");
403        assert!(gen.options.generate_accessors);
404        assert!(gen.options.generate_id_fields);
405    }
406}