1use std::collections::{HashMap, HashSet};
4use std::path::PathBuf;
5
6use crate::cli::GenerateArgs;
7use crate::config::{CONFIG_FILE_NAME, Config, SCHEMA_FILE_PATH};
8use crate::error::{CliError, CliResult};
9use crate::output::{self, success};
10
11pub async fn run(args: GenerateArgs) -> CliResult<()> {
13 output::header("Generate Prax Client");
14
15 let cwd = std::env::current_dir()?;
16
17 let config_path = cwd.join(CONFIG_FILE_NAME);
19 let config = if config_path.exists() {
20 Config::load(&config_path)?
21 } else {
22 Config::default()
23 };
24
25 let schema_path = args
27 .schema
28 .clone()
29 .unwrap_or_else(|| cwd.join(SCHEMA_FILE_PATH));
30 if !schema_path.exists() {
31 return Err(
32 CliError::Config(format!("Schema file not found: {}", schema_path.display())).into(),
33 );
34 }
35
36 let output_dir = args
38 .output
39 .clone()
40 .unwrap_or_else(|| PathBuf::from(&config.generator.output));
41
42 output::kv("Schema", &schema_path.display().to_string());
43 output::kv("Output", &output_dir.display().to_string());
44 output::newline();
45
46 output::step(1, 4, "Reading schema...");
47
48 let schema_content = std::fs::read_to_string(&schema_path)?;
50 let schema = parse_schema(&schema_content)?;
51
52 output::step(2, 4, "Validating schema...");
53
54 validate_schema(&schema)?;
56
57 output::step(3, 4, "Generating code...");
58
59 std::fs::create_dir_all(&output_dir)?;
61
62 let generated_files = generate_code(&schema, &output_dir, &args, &config)?;
64
65 output::step(4, 4, "Writing files...");
66
67 output::newline();
69 output::section("Generated files");
70
71 for file in &generated_files {
72 let relative_path = file
73 .strip_prefix(&cwd)
74 .unwrap_or(file)
75 .display()
76 .to_string();
77 output::list_item(&relative_path);
78 }
79
80 output::newline();
81 success(&format!(
82 "Generated {} files in {:.2}s",
83 generated_files.len(),
84 0.0 ));
86
87 Ok(())
88}
89
90fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
92 prax_schema::validate_schema(content)
95 .map_err(|e| CliError::Schema(format!("Failed to parse/validate schema: {}", e)))
96}
97
98fn validate_schema(_schema: &prax_schema::Schema) -> CliResult<()> {
100 Ok(())
102}
103
104fn generate_code(
106 schema: &prax_schema::ast::Schema,
107 output_dir: &PathBuf,
108 args: &GenerateArgs,
109 config: &Config,
110) -> CliResult<Vec<PathBuf>> {
111 let mut generated_files = Vec::new();
112
113 let features = if !args.features.is_empty() {
115 args.features.clone()
116 } else {
117 config
118 .generator
119 .features
120 .clone()
121 .unwrap_or_else(|| vec!["client".to_string()])
122 };
123
124 let relation_graph = build_relation_graph(schema);
126
127 let client_path = output_dir.join("mod.rs");
129 let client_code = generate_client_module(schema, &features)?;
130 std::fs::write(&client_path, client_code)?;
131 generated_files.push(client_path);
132
133 for model in schema.models.values() {
135 let model_path = output_dir.join(format!("{}.rs", to_snake_case(model.name())));
136 let model_code = generate_model_module(model, &features, &relation_graph)?;
137 std::fs::write(&model_path, model_code)?;
138 generated_files.push(model_path);
139 }
140
141 for enum_def in schema.enums.values() {
143 let enum_path = output_dir.join(format!("{}.rs", to_snake_case(enum_def.name())));
144 let enum_code = generate_enum_module(enum_def)?;
145 std::fs::write(&enum_path, enum_code)?;
146 generated_files.push(enum_path);
147 }
148
149 let types_path = output_dir.join("types.rs");
151 let types_code = generate_types_module(schema)?;
152 std::fs::write(&types_path, types_code)?;
153 generated_files.push(types_path);
154
155 let filters_path = output_dir.join("filters.rs");
157 let filters_code = generate_filters_module(schema)?;
158 std::fs::write(&filters_path, filters_code)?;
159 generated_files.push(filters_path);
160
161 Ok(generated_files)
162}
163
164fn build_relation_graph(schema: &prax_schema::ast::Schema) -> HashMap<String, HashSet<String>> {
168 let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
169
170 for model in schema.models.values() {
171 let entry = graph.entry(model.name().to_string()).or_default();
172 for field in model.fields.values() {
173 if let prax_schema::ast::FieldType::Model(ref target) = field.field_type {
174 if !field.is_list() {
175 entry.insert(target.to_string());
176 }
177 }
178 }
179 }
180
181 graph
182}
183
184fn needs_boxing(
188 source_model: &str,
189 target_model: &str,
190 graph: &HashMap<String, HashSet<String>>,
191) -> bool {
192 let mut visited = HashSet::new();
193 let mut stack = vec![target_model.to_string()];
194
195 while let Some(current) = stack.pop() {
196 if current == source_model {
197 return true;
198 }
199 if !visited.insert(current.clone()) {
200 continue;
201 }
202 if let Some(neighbors) = graph.get(¤t) {
203 for neighbor in neighbors {
204 stack.push(neighbor.clone());
205 }
206 }
207 }
208
209 false
210}
211
212fn generate_client_module(
214 schema: &prax_schema::ast::Schema,
215 _features: &[String],
216) -> CliResult<String> {
217 let mut code = String::new();
218
219 code.push_str("//! Auto-generated by Prax - DO NOT EDIT\n");
220 code.push_str("//!\n");
221 code.push_str("//! This module contains the generated Prax client.\n\n");
222
223 code.push_str("pub mod types;\n");
225 code.push_str("pub mod filters;\n\n");
226
227 for model in schema.models.values() {
228 code.push_str(&format!("pub mod {};\n", to_snake_case(model.name())));
229 }
230
231 for enum_def in schema.enums.values() {
232 code.push_str(&format!("pub mod {};\n", to_snake_case(enum_def.name())));
233 }
234
235 code.push_str("\n");
236
237 code.push_str("#[allow(unused_imports)]\npub use types::*;\n");
239 code.push_str("#[allow(unused_imports)]\npub use filters::*;\n\n");
240
241 for model in schema.models.values() {
242 code.push_str(&format!(
243 "#[allow(unused_imports)]\npub use {}::{};\n",
244 to_snake_case(model.name()),
245 model.name()
246 ));
247 }
248
249 for enum_def in schema.enums.values() {
250 code.push_str(&format!(
251 "#[allow(unused_imports)]\npub use {}::{};\n",
252 to_snake_case(enum_def.name()),
253 enum_def.name()
254 ));
255 }
256
257 code.push_str("\n");
258
259 code.push_str("#[allow(dead_code)]\n");
261 code.push_str("/// The Prax database client\n");
262 code.push_str("#[derive(Clone)]\n");
263 code.push_str("pub struct PraxClient<E: prax_query::QueryEngine> {\n");
264 code.push_str(" engine: E,\n");
265 code.push_str("}\n\n");
266
267 code.push_str("impl<E: prax_query::QueryEngine> PraxClient<E> {\n");
268 code.push_str(" /// Create a new Prax client with the given query engine\n");
269 code.push_str(" pub fn new(engine: E) -> Self {\n");
270 code.push_str(" Self { engine }\n");
271 code.push_str(" }\n\n");
272
273 for model in schema.models.values() {
274 let snake_name = to_snake_case(model.name());
275 code.push_str(&format!(" /// Access {} operations\n", model.name()));
276 code.push_str(&format!(
277 " pub fn {}(&self) -> {}::{}Operations<E> {{\n",
278 snake_name,
279 snake_name,
280 model.name()
281 ));
282 code.push_str(&format!(
283 " {}::{}Operations::new(self.engine.clone())\n",
284 snake_name,
285 model.name()
286 ));
287 code.push_str(" }\n\n");
288 }
289
290 code.push_str("}\n");
291
292 Ok(code)
293}
294
295fn generate_model_module(
297 model: &prax_schema::ast::Model,
298 features: &[String],
299 relation_graph: &HashMap<String, HashSet<String>>,
300) -> CliResult<String> {
301 let mut code = String::new();
302
303 code.push_str(&format!(
304 "//! Auto-generated module for {} model\n\n",
305 model.name()
306 ));
307
308 code.push_str("#[allow(unused_imports)]\n");
310 code.push_str("use super::*;\n");
311 code.push_str("#[allow(unused_imports)]\n");
312 code.push_str("use prax_query::traits::Model;\n\n");
313
314 let mut derives = vec!["Debug", "Clone"];
316 if features.contains(&"serde".to_string()) {
317 derives.push("serde::Serialize");
318 derives.push("serde::Deserialize");
319 }
320
321 code.push_str("#[allow(dead_code)]\n");
323 code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
324 code.push_str(&format!("pub struct {} {{\n", model.name()));
325
326 for field in model.fields.values() {
327 let field_name = to_snake_case(field.name());
328
329 if let Some(attr) = field.get_attribute("map") {
331 if features.contains(&"serde".to_string()) {
332 if let Some(value) = attr.first_arg().and_then(|v| v.as_string()) {
333 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
334 }
335 }
336 }
337
338 let rust_type = field_type_to_rust_with_boxing(
339 &field.field_type,
340 field.modifier,
341 model.name(),
342 relation_graph,
343 );
344 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
345 }
346
347 code.push_str("}\n\n");
348
349 let table_name = model.table_name();
351 let id_fields: Vec<&str> = model.id_fields().iter().map(|f| f.name()).collect();
352 let scalar_columns: Vec<String> = model
353 .scalar_fields()
354 .iter()
355 .map(|f| {
356 f.get_attribute("map")
358 .and_then(|a| a.first_arg())
359 .and_then(|v| v.as_string())
360 .map(|s| s.to_string())
361 .unwrap_or_else(|| to_snake_case(f.name()))
362 })
363 .collect();
364
365 code.push_str(&format!("impl Model for {} {{\n", model.name()));
366 code.push_str(&format!(
367 " const MODEL_NAME: &'static str = \"{}\";\n",
368 model.name()
369 ));
370 code.push_str(&format!(
371 " const TABLE_NAME: &'static str = \"{}\";\n",
372 table_name
373 ));
374 code.push_str(&format!(
375 " const PRIMARY_KEY: &'static [&'static str] = &[{}];\n",
376 id_fields
377 .iter()
378 .map(|f| format!("\"{}\"", to_snake_case(f)))
379 .collect::<Vec<_>>()
380 .join(", ")
381 ));
382 code.push_str(&format!(
383 " const COLUMNS: &'static [&'static str] = &[{}];\n",
384 scalar_columns
385 .iter()
386 .map(|c| format!("\"{}\"", c))
387 .collect::<Vec<_>>()
388 .join(", ")
389 ));
390 code.push_str("}\n\n");
391
392 code.push_str("#[allow(dead_code)]\n");
394 code.push_str(&format!("/// Operations for the {} model\n", model.name()));
395 code.push_str(&format!(
396 "pub struct {}Operations<E: prax_query::QueryEngine> {{\n",
397 model.name()
398 ));
399 code.push_str(" engine: E,\n");
400 code.push_str("}\n\n");
401
402 code.push_str(&format!(
403 "impl<E: prax_query::QueryEngine> {}Operations<E> {{\n",
404 model.name()
405 ));
406 code.push_str(" pub fn new(engine: E) -> Self {\n");
407 code.push_str(" Self { engine }\n");
408 code.push_str(" }\n\n");
409
410 code.push_str(" /// Find many records\n");
412 code.push_str(&format!(
413 " pub fn find_many(&self) -> prax_query::FindManyOperation<E, {}> {{\n",
414 model.name()
415 ));
416 code.push_str(" prax_query::FindManyOperation::new(self.engine.clone())\n");
417 code.push_str(" }\n\n");
418
419 code.push_str(" /// Find a unique record\n");
420 code.push_str(&format!(
421 " pub fn find_unique(&self) -> prax_query::FindUniqueOperation<E, {}> {{\n",
422 model.name()
423 ));
424 code.push_str(" prax_query::FindUniqueOperation::new(self.engine.clone())\n");
425 code.push_str(" }\n\n");
426
427 code.push_str(" /// Find the first matching record\n");
428 code.push_str(&format!(
429 " pub fn find_first(&self) -> prax_query::FindFirstOperation<E, {}> {{\n",
430 model.name()
431 ));
432 code.push_str(" prax_query::FindFirstOperation::new(self.engine.clone())\n");
433 code.push_str(" }\n\n");
434
435 code.push_str(" /// Create a new record\n");
436 code.push_str(&format!(
437 " pub fn create(&self) -> prax_query::CreateOperation<E, {}> {{\n",
438 model.name()
439 ));
440 code.push_str(" prax_query::CreateOperation::new(self.engine.clone())\n");
441 code.push_str(" }\n\n");
442
443 code.push_str(" /// Update a record\n");
444 code.push_str(&format!(
445 " pub fn update(&self) -> prax_query::UpdateOperation<E, {}> {{\n",
446 model.name()
447 ));
448 code.push_str(" prax_query::UpdateOperation::new(self.engine.clone())\n");
449 code.push_str(" }\n\n");
450
451 code.push_str(" /// Delete a record\n");
452 code.push_str(&format!(
453 " pub fn delete(&self) -> prax_query::DeleteOperation<E, {}> {{\n",
454 model.name()
455 ));
456 code.push_str(" prax_query::DeleteOperation::new(self.engine.clone())\n");
457 code.push_str(" }\n\n");
458
459 code.push_str(" /// Count records\n");
460 code.push_str(&format!(
461 " pub fn count(&self) -> prax_query::CountOperation<E, {}> {{\n",
462 model.name()
463 ));
464 code.push_str(" prax_query::CountOperation::new(self.engine.clone())\n");
465 code.push_str(" }\n");
466
467 code.push_str("}\n");
468
469 Ok(code)
470}
471
472fn generate_enum_module(enum_def: &prax_schema::ast::Enum) -> CliResult<String> {
474 let mut code = String::new();
475
476 code.push_str(&format!(
477 "//! Auto-generated module for {} enum\n\n",
478 enum_def.name()
479 ));
480
481 code.push_str("#[allow(dead_code)]\n");
482 code.push_str(
483 "#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n",
484 );
485 code.push_str(&format!("pub enum {} {{\n", enum_def.name()));
486
487 for variant in &enum_def.variants {
488 let raw_name = variant.name();
489 let pascal_name = to_pascal_case(raw_name);
490
491 if let Some(attr) = variant.attributes.iter().find(|a| a.is("map")) {
493 if let Some(value) = attr.first_arg().and_then(|v| v.as_string()) {
494 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
495 code.push_str(&format!(" {},\n", pascal_name));
496 continue;
497 }
498 }
499
500 if raw_name != pascal_name {
502 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", raw_name));
503 }
504 code.push_str(&format!(" {},\n", pascal_name));
505 }
506
507 code.push_str("}\n\n");
508
509 code.push_str(&format!(
511 "impl std::fmt::Display for {} {{\n",
512 enum_def.name()
513 ));
514 code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
515 code.push_str(" match self {\n");
516 for variant in &enum_def.variants {
517 let raw_name = variant.name();
518 let pascal_name = to_pascal_case(raw_name);
519 let db_value = variant.db_value();
520 code.push_str(&format!(
521 " Self::{} => write!(f, \"{}\"),\n",
522 pascal_name, db_value
523 ));
524 }
525 code.push_str(" }\n");
526 code.push_str(" }\n");
527 code.push_str("}\n\n");
528
529 if let Some(default_variant) = enum_def.variants.first() {
531 let pascal_name = to_pascal_case(default_variant.name());
532 code.push_str(&format!("impl Default for {} {{\n", enum_def.name()));
533 code.push_str(&format!(
534 " fn default() -> Self {{\n Self::{}\n }}\n",
535 pascal_name
536 ));
537 code.push_str("}\n");
538 }
539
540 Ok(code)
541}
542
543fn generate_types_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
545 let mut code = String::new();
546
547 code.push_str("//! Common type definitions\n\n");
548 code.push_str("#[allow(unused_imports)]\npub use chrono::{DateTime, Utc};\n");
549 code.push_str("#[allow(unused_imports)]\npub use uuid::Uuid;\n");
550 code.push_str("#[allow(unused_imports)]\npub use serde_json::Value as Json;\n");
551 code.push_str("\n");
552
553 for composite in schema.types.values() {
555 code.push_str("#[allow(dead_code)]\n");
556 code.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
557 code.push_str(&format!("pub struct {} {{\n", composite.name()));
558 for field in composite.fields.values() {
559 let rust_type = field_type_to_rust(&field.field_type, field.modifier);
560 let field_name = to_snake_case(field.name());
561 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
562 }
563 code.push_str("}\n\n");
564 }
565
566 Ok(code)
567}
568
569fn generate_filters_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
571 let mut code = String::new();
572
573 code.push_str("//! Filter types for queries\n\n");
574 code.push_str("#[allow(unused_imports)]\n");
575 code.push_str("use prax_query::filter::{Filter, ScalarFilter};\n");
576
577 let mut referenced_enums = HashSet::new();
579 for model in schema.models.values() {
580 for field in model.fields.values() {
581 if !field.is_relation() {
582 if let prax_schema::ast::FieldType::Enum(ref name) = field.field_type {
583 referenced_enums.insert(name.to_string());
584 }
585 }
586 }
587 }
588
589 for enum_name in &referenced_enums {
591 code.push_str(&format!(
592 "#[allow(unused_imports)]\nuse super::{}::{};\n",
593 to_snake_case(enum_name),
594 enum_name
595 ));
596 }
597
598 code.push_str("\n");
599
600 for model in schema.models.values() {
601 code.push_str("#[allow(dead_code)]\n");
603 code.push_str(&format!("/// Filter input for {} queries\n", model.name()));
604 code.push_str("#[derive(Debug, Default, Clone)]\n");
605 code.push_str(&format!("pub struct {}WhereInput {{\n", model.name()));
606
607 for field in model.fields.values() {
608 if !field.is_relation() {
609 let filter_type = field_to_filter_type(&field.field_type);
610 let field_name = to_snake_case(field.name());
611 code.push_str(&format!(
612 " pub {}: Option<{}>,\n",
613 field_name, filter_type
614 ));
615 }
616 }
617
618 code.push_str(" pub and: Option<Vec<Self>>,\n");
619 code.push_str(" pub or: Option<Vec<Self>>,\n");
620 code.push_str(" pub not: Option<Box<Self>>,\n");
621 code.push_str("}\n\n");
622
623 code.push_str("#[allow(dead_code)]\n");
625 code.push_str(&format!(
626 "/// Order by input for {} queries\n",
627 model.name()
628 ));
629 code.push_str("#[derive(Debug, Default, Clone)]\n");
630 code.push_str(&format!("pub struct {}OrderByInput {{\n", model.name()));
631
632 for field in model.fields.values() {
633 if !field.is_relation() {
634 let field_name = to_snake_case(field.name());
635 code.push_str(&format!(
636 " pub {}: Option<prax_query::SortOrder>,\n",
637 field_name
638 ));
639 }
640 }
641
642 code.push_str("}\n\n");
643 }
644
645 Ok(code)
646}
647
648fn field_type_to_rust(
650 field_type: &prax_schema::ast::FieldType,
651 modifier: prax_schema::ast::TypeModifier,
652) -> String {
653 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
654
655 let base_type = match field_type {
656 FieldType::Scalar(scalar) => match scalar {
657 ScalarType::Int => "i32".to_string(),
658 ScalarType::BigInt => "i64".to_string(),
659 ScalarType::Float => "f64".to_string(),
660 ScalarType::String => "String".to_string(),
661 ScalarType::Boolean => "bool".to_string(),
662 ScalarType::DateTime => "chrono::DateTime<chrono::Utc>".to_string(),
663 ScalarType::Date => "chrono::NaiveDate".to_string(),
664 ScalarType::Time => "chrono::NaiveTime".to_string(),
665 ScalarType::Json => "serde_json::Value".to_string(),
666 ScalarType::Bytes => "Vec<u8>".to_string(),
667 ScalarType::Decimal => "rust_decimal::Decimal".to_string(),
668 ScalarType::Uuid => "uuid::Uuid".to_string(),
669 ScalarType::Cuid => "String".to_string(),
670 ScalarType::Cuid2 => "String".to_string(),
671 ScalarType::NanoId => "String".to_string(),
672 ScalarType::Ulid => "String".to_string(),
673 ScalarType::Vector(_) | ScalarType::HalfVector(_) => "Vec<f32>".to_string(),
674 ScalarType::SparseVector(_) => "Vec<(u32, f32)>".to_string(),
675 ScalarType::Bit(_) => "Vec<u8>".to_string(),
676 },
677 FieldType::Model(name) => name.to_string(),
678 FieldType::Enum(name) => name.to_string(),
679 FieldType::Composite(name) => name.to_string(),
680 FieldType::Unsupported(_) => "serde_json::Value".to_string(),
681 };
682
683 match modifier {
684 TypeModifier::Optional | TypeModifier::OptionalList => format!("Option<{}>", base_type),
685 TypeModifier::List => format!("Vec<{}>", base_type),
686 TypeModifier::Required => base_type,
687 }
688}
689
690fn field_type_to_rust_with_boxing(
692 field_type: &prax_schema::ast::FieldType,
693 modifier: prax_schema::ast::TypeModifier,
694 source_model: &str,
695 relation_graph: &HashMap<String, HashSet<String>>,
696) -> String {
697 use prax_schema::ast::{FieldType, TypeModifier};
698
699 if let FieldType::Model(target) = field_type {
701 if !matches!(modifier, TypeModifier::List) {
702 let should_box = needs_boxing(source_model, target, relation_graph);
703 let base = target.to_string();
704 return match modifier {
705 TypeModifier::Optional | TypeModifier::OptionalList => {
706 if should_box {
707 format!("Option<Box<{}>>", base)
708 } else {
709 format!("Option<{}>", base)
710 }
711 }
712 TypeModifier::Required => {
713 if should_box {
714 format!("Box<{}>", base)
715 } else {
716 base
717 }
718 }
719 TypeModifier::List => unreachable!(),
720 };
721 }
722 }
723
724 field_type_to_rust(field_type, modifier)
726}
727
728fn field_to_filter_type(field_type: &prax_schema::ast::FieldType) -> String {
730 use prax_schema::ast::{FieldType, ScalarType};
731
732 match field_type {
733 FieldType::Scalar(scalar) => match scalar {
734 ScalarType::Int | ScalarType::BigInt => "ScalarFilter<i64>".to_string(),
735 ScalarType::Float | ScalarType::Decimal => "ScalarFilter<f64>".to_string(),
736 ScalarType::String
737 | ScalarType::Uuid
738 | ScalarType::Cuid
739 | ScalarType::Cuid2
740 | ScalarType::NanoId
741 | ScalarType::Ulid => "ScalarFilter<String>".to_string(),
742 ScalarType::Boolean => "ScalarFilter<bool>".to_string(),
743 ScalarType::DateTime => "ScalarFilter<chrono::DateTime<chrono::Utc>>".to_string(),
744 ScalarType::Date => "ScalarFilter<chrono::NaiveDate>".to_string(),
745 ScalarType::Time => "ScalarFilter<chrono::NaiveTime>".to_string(),
746 ScalarType::Json => "ScalarFilter<serde_json::Value>".to_string(),
747 ScalarType::Bytes => "ScalarFilter<Vec<u8>>".to_string(),
748 ScalarType::Vector(_) | ScalarType::HalfVector(_) => "VectorFilter".to_string(),
750 ScalarType::SparseVector(_) => "SparseVectorFilter".to_string(),
751 ScalarType::Bit(_) => "BitFilter".to_string(),
752 },
753 FieldType::Enum(name) => format!("ScalarFilter<{}>", name),
754 _ => "Filter".to_string(),
755 }
756}
757
758fn to_snake_case(name: &str) -> String {
760 let mut result = String::new();
761 for (i, c) in name.chars().enumerate() {
762 if c.is_uppercase() {
763 if i > 0 {
764 result.push('_');
765 }
766 result.push(c.to_lowercase().next().unwrap());
767 } else {
768 result.push(c);
769 }
770 }
771 result
772}
773
774fn to_pascal_case(name: &str) -> String {
776 if name.is_empty() {
777 return String::new();
778 }
779
780 let first = name.chars().next().unwrap();
782 if first.is_uppercase() && name.chars().any(|c| c.is_lowercase()) && !name.contains('_') {
783 return name.to_string();
784 }
785
786 name.split('_')
788 .filter(|s| !s.is_empty())
789 .map(|segment| {
790 let mut chars = segment.chars();
791 match chars.next() {
792 None => String::new(),
793 Some(first) => {
794 let rest: String = chars.collect();
795 format!("{}{}", first.to_uppercase(), rest.to_lowercase())
796 }
797 }
798 })
799 .collect()
800}
801
802#[cfg(test)]
803mod tests {
804 use super::*;
805
806 #[test]
807 fn test_to_snake_case() {
808 assert_eq!(to_snake_case("BoardMember"), "board_member");
809 assert_eq!(to_snake_case("User"), "user");
810 assert_eq!(to_snake_case("JiraImportConfig"), "jira_import_config");
811 }
812
813 #[test]
814 fn test_to_pascal_case_from_snake() {
815 assert_eq!(to_pascal_case("card_created"), "CardCreated");
816 assert_eq!(to_pascal_case("branch_deleted"), "BranchDeleted");
817 assert_eq!(to_pascal_case("pr_merged"), "PrMerged");
818 }
819
820 #[test]
821 fn test_to_pascal_case_from_screaming() {
822 assert_eq!(to_pascal_case("CARD_CREATED"), "CardCreated");
823 assert_eq!(to_pascal_case("PR_MERGED"), "PrMerged");
824 }
825
826 #[test]
827 fn test_to_pascal_case_already_pascal() {
828 assert_eq!(to_pascal_case("Admin"), "Admin");
829 assert_eq!(to_pascal_case("SuperAdmin"), "SuperAdmin");
830 assert_eq!(to_pascal_case("Low"), "Low");
831 }
832
833 #[test]
834 fn test_to_pascal_case_single_word() {
835 assert_eq!(to_pascal_case("active"), "Active");
836 assert_eq!(to_pascal_case("ACTIVE"), "Active");
837 }
838
839 #[test]
840 fn test_needs_boxing_direct_cycle() {
841 let mut graph = HashMap::new();
842 graph.insert(
843 "Board".to_string(),
844 HashSet::from(["JiraConfig".to_string()]),
845 );
846 graph.insert(
847 "JiraConfig".to_string(),
848 HashSet::from(["Board".to_string()]),
849 );
850
851 assert!(needs_boxing("Board", "JiraConfig", &graph));
852 assert!(needs_boxing("JiraConfig", "Board", &graph));
853 }
854
855 #[test]
856 fn test_needs_boxing_no_cycle() {
857 let mut graph = HashMap::new();
858 graph.insert("Post".to_string(), HashSet::from(["User".to_string()]));
859 graph.insert("User".to_string(), HashSet::new());
860
861 assert!(!needs_boxing("Post", "User", &graph));
862 }
863
864 #[test]
865 fn test_needs_boxing_indirect_cycle() {
866 let mut graph = HashMap::new();
867 graph.insert("A".to_string(), HashSet::from(["B".to_string()]));
868 graph.insert("B".to_string(), HashSet::from(["C".to_string()]));
869 graph.insert("C".to_string(), HashSet::from(["A".to_string()]));
870
871 assert!(needs_boxing("A", "B", &graph));
872 assert!(needs_boxing("B", "C", &graph));
873 assert!(needs_boxing("C", "A", &graph));
874 }
875}