1use ryo_analysis::context::AnalysisContext;
27use ryo_analysis::query::SpecAliasData;
28use ryo_analysis::{SymbolId, SymbolKind};
29use ryo_executor::{SelfParam, Visibility};
30
31use crate::MutationSpec;
32
33pub trait SpecGenerator: Send + Sync {
38 fn name(&self) -> &'static str;
40
41 fn description(&self) -> &str;
43
44 fn matches(&self, spec: &SpecAliasData) -> bool;
46
47 fn generate(&self, ctx: &AnalysisContext, spec: &SpecAliasData) -> Vec<MutationSpec>;
52}
53
54#[derive(Debug, Clone, Default)]
56pub struct GeneratorOptions {
57 pub generate_accessors: bool,
59 pub generate_id_fields: bool,
61 pub generate_collection_fields: bool,
63 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
98pub struct DomainSpecGenerator {
110 options: GeneratorOptions,
111 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 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 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 fn has_method(&self, ctx: &AnalysisContext, struct_id: SymbolId, method_name: &str) -> bool {
182 let graph = ctx.code_graph();
183
184 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 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 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 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 let relations = self.extract_relations(ctx, spec);
249
250 for relation in &relations {
252 let field_name = format!("{}_id", self.to_snake_case(relation));
253 let field_type = format!("{}Id", relation);
254
255 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 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 if !self.options.default_derives.is_empty() {
281 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#[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 pub fn register<G: SpecGenerator + 'static>(&mut self, generator: G) {
305 self.generators.push(Box::new(generator));
306 }
307
308 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 pub fn generate_all(&self, ctx: &AnalysisContext) -> Vec<(String, Vec<MutationSpec>)> {
326 let mut results = Vec::new();
327
328 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 let base_type = &alias_name[..alias_name.len() - 4]; 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, 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 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}