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