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_field_ident(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_field_ident(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_field_ident(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_field_ident(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_field_ident(field.name());
471 let column = field
475 .get_attribute("map")
476 .and_then(|a| a.first_arg())
477 .and_then(|v| v.as_string())
478 .map(|s| s.to_string())
479 .unwrap_or_else(|| to_snake_case(field.name()));
480 let rust_type = field_type_to_rust_with_boxing(
481 &field.field_type,
482 field.modifier,
483 model.name(),
484 relation_graph,
485 );
486 code.push_str(&format!(
487 " \"{}\" => ::core::option::Option::Some(\n <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n ),\n",
488 column, rust_type, field_name
489 ));
490 }
491 code.push_str(" _ => ::core::option::Option::None,\n");
492 code.push_str(" }\n");
493 code.push_str(" }\n");
494 code.push_str("}\n\n");
495
496 code.push_str("#[allow(dead_code)]\n");
501 code.push_str(&format!("/// Operations for the {} model\n", model.name()));
502 code.push_str("pub struct Client<E: prax_query::QueryEngine> {\n");
503 code.push_str(" engine: E,\n");
504 code.push_str("}\n\n");
505
506 code.push_str("impl<E: prax_query::QueryEngine> Client<E> {\n");
507 code.push_str(" pub fn new(engine: E) -> Self {\n");
508 code.push_str(" Self { engine }\n");
509 code.push_str(" }\n\n");
510
511 let model_ty = model.name();
512 let crud_methods: &[(&str, &str, &str)] = &[
513 ("find_many", "FindManyOperation", "Find many records"),
514 ("find_unique", "FindUniqueOperation", "Find a unique record"),
515 (
516 "find_first",
517 "FindFirstOperation",
518 "Find the first matching record",
519 ),
520 ("create", "CreateOperation", "Create a new record"),
521 (
522 "create_many",
523 "CreateManyOperation",
524 "Create many records in one operation",
525 ),
526 ("update", "UpdateOperation", "Update a record"),
527 (
528 "update_many",
529 "UpdateManyOperation",
530 "Update many records matching a filter",
531 ),
532 ("upsert", "UpsertOperation", "Insert or update a record"),
533 ("delete", "DeleteOperation", "Delete a record"),
534 (
535 "delete_many",
536 "DeleteManyOperation",
537 "Delete many records matching a filter",
538 ),
539 ("count", "CountOperation", "Count records"),
540 ];
541 for (method, op_ty, doc) in crud_methods {
542 code.push_str(&format!(" /// {}\n", doc));
543 code.push_str(&format!(
544 " pub fn {}(&self) -> prax_query::operations::{}<E, {}> {{\n",
545 method, op_ty, model_ty,
546 ));
547 code.push_str(&format!(
548 " prax_query::operations::{}::new(self.engine.clone())\n",
549 op_ty,
550 ));
551 code.push_str(" }\n\n");
552 }
553
554 code.push_str("}\n");
555
556 Ok(code)
557}
558
559fn generate_enum_module(enum_def: &prax_schema::ast::Enum) -> CliResult<String> {
561 let mut code = String::new();
562
563 code.push_str(&format!(
564 "//! Auto-generated module for {} enum\n\n",
565 enum_def.name()
566 ));
567
568 code.push_str("#[allow(dead_code)]\n");
569 code.push_str(
570 "#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n",
571 );
572 code.push_str(&format!("pub enum {} {{\n", enum_def.name()));
573
574 for variant in &enum_def.variants {
575 let raw_name = variant.name();
576 let pascal_name = to_pascal_case(raw_name);
577
578 if let Some(attr) = variant.attributes.iter().find(|a| a.is("map"))
580 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
581 {
582 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
583 code.push_str(&format!(" {},\n", pascal_name));
584 continue;
585 }
586
587 if raw_name != pascal_name {
589 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", raw_name));
590 }
591 code.push_str(&format!(" {},\n", pascal_name));
592 }
593
594 code.push_str("}\n\n");
595
596 code.push_str(&format!(
598 "impl std::fmt::Display for {} {{\n",
599 enum_def.name()
600 ));
601 code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
602 code.push_str(" match self {\n");
603 for variant in &enum_def.variants {
604 let raw_name = variant.name();
605 let pascal_name = to_pascal_case(raw_name);
606 let db_value = variant.db_value();
607 code.push_str(&format!(
608 " Self::{} => write!(f, \"{}\"),\n",
609 pascal_name, db_value
610 ));
611 }
612 code.push_str(" }\n");
613 code.push_str(" }\n");
614 code.push_str("}\n\n");
615
616 if let Some(default_variant) = enum_def.variants.first() {
618 let pascal_name = to_pascal_case(default_variant.name());
619 code.push_str(&format!("impl Default for {} {{\n", enum_def.name()));
620 code.push_str(&format!(
621 " fn default() -> Self {{\n Self::{}\n }}\n",
622 pascal_name
623 ));
624 code.push_str("}\n\n");
625 }
626
627 code.push_str(&format!(
631 "impl std::str::FromStr for {} {{\n",
632 enum_def.name()
633 ));
634 code.push_str(" type Err = prax_query::row::RowError;\n");
635 code.push_str(" fn from_str(s: &str) -> Result<Self, Self::Err> {\n");
636 code.push_str(" match s {\n");
637 for variant in &enum_def.variants {
638 let raw_name = variant.name();
639 let pascal_name = to_pascal_case(raw_name);
640 let db_value = variant.db_value();
641 code.push_str(&format!(
642 " \"{}\" => Ok(Self::{}),\n",
643 db_value, pascal_name
644 ));
645 }
646 code.push_str(&format!(
647 " _ => Err(prax_query::row::RowError::TypeConversion {{\n column: String::new(),\n message: format!(\"unknown {} variant: {{}}\", s),\n }}),\n",
648 enum_def.name()
649 ));
650 code.push_str(" }\n");
651 code.push_str(" }\n");
652 code.push_str("}\n\n");
653
654 code.push_str(&format!(
658 "impl prax_query::row::FromColumn for {} {{\n",
659 enum_def.name()
660 ));
661 code.push_str(
662 " fn from_column(row: &impl prax_query::row::RowRef, column: &str)\n -> Result<Self, prax_query::row::RowError>\n {\n",
663 );
664 code.push_str(" let raw = row.get_string(column)?;\n");
665 code.push_str(" <Self as std::str::FromStr>::from_str(&raw).map_err(|e| {\n");
666 code.push_str(" let msg = match &e {\n");
667 code.push_str(
668 " prax_query::row::RowError::TypeConversion { message, .. } => message.clone(),\n",
669 );
670 code.push_str(" other => other.to_string(),\n");
671 code.push_str(" };\n");
672 code.push_str(" prax_query::row::RowError::TypeConversion {\n");
673 code.push_str(" column: column.to_string(),\n");
674 code.push_str(" message: msg,\n");
675 code.push_str(" }\n");
676 code.push_str(" })\n");
677 code.push_str(" }\n");
678 code.push_str("}\n\n");
679
680 code.push_str(&format!(
689 "impl prax_query::filter::ToFilterValue for {} {{\n",
690 enum_def.name()
691 ));
692 code.push_str(" fn to_filter_value(&self) -> prax_query::filter::FilterValue {\n");
693 code.push_str(" prax_query::filter::FilterValue::String(self.to_string())\n");
694 code.push_str(" }\n");
695 code.push_str("}\n");
696
697 Ok(code)
698}
699
700fn generate_types_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
702 let mut code = String::new();
703
704 code.push_str("//! Common type definitions\n\n");
705 code.push_str("#[allow(unused_imports)]\npub use chrono::{DateTime, Utc};\n");
706 code.push_str("#[allow(unused_imports)]\npub use uuid::Uuid;\n");
707 code.push_str("#[allow(unused_imports)]\npub use serde_json::Value as Json;\n");
708 code.push('\n');
709
710 for composite in schema.types.values() {
712 code.push_str("#[allow(dead_code)]\n");
713 code.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
714 code.push_str(&format!("pub struct {} {{\n", composite.name()));
715 for field in composite.fields.values() {
716 let rust_type = field_type_to_rust(&field.field_type, field.modifier);
717 let field_name = to_field_ident(field.name());
718 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
719 }
720 code.push_str("}\n\n");
721 }
722
723 Ok(code)
724}
725
726fn generate_filters_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
728 let mut code = String::new();
729
730 code.push_str("//! Filter types for queries\n\n");
731 code.push_str("#[allow(unused_imports)]\n");
732 code.push_str("use prax_query::filter::{Filter, ScalarFilter};\n");
733
734 let mut referenced_enums = HashSet::new();
736 for model in schema.models.values() {
737 for field in model.fields.values() {
738 if !field.is_relation()
739 && let prax_schema::ast::FieldType::Enum(ref name) = field.field_type
740 {
741 referenced_enums.insert(name.to_string());
742 }
743 }
744 }
745
746 for enum_name in &referenced_enums {
748 code.push_str(&format!(
749 "#[allow(unused_imports)]\nuse super::{}::{};\n",
750 to_snake_case(enum_name),
751 enum_name
752 ));
753 }
754
755 code.push('\n');
756
757 for model in schema.models.values() {
758 code.push_str("#[allow(dead_code)]\n");
760 code.push_str(&format!("/// Filter input for {} queries\n", model.name()));
761 code.push_str("#[derive(Debug, Default, Clone)]\n");
762 code.push_str(&format!("pub struct {}WhereInput {{\n", model.name()));
763
764 for field in model.fields.values() {
765 if !field.is_relation() {
766 let filter_type = field_to_filter_type(&field.field_type);
767 let field_name = to_field_ident(field.name());
768 code.push_str(&format!(
769 " pub {}: Option<{}>,\n",
770 field_name, filter_type
771 ));
772 }
773 }
774
775 code.push_str(" pub and: Option<Vec<Self>>,\n");
776 code.push_str(" pub or: Option<Vec<Self>>,\n");
777 code.push_str(" pub not: Option<Box<Self>>,\n");
778 code.push_str("}\n\n");
779
780 code.push_str("#[allow(dead_code)]\n");
782 code.push_str(&format!(
783 "/// Order by input for {} queries\n",
784 model.name()
785 ));
786 code.push_str("#[derive(Debug, Default, Clone)]\n");
787 code.push_str(&format!("pub struct {}OrderByInput {{\n", model.name()));
788
789 for field in model.fields.values() {
790 if !field.is_relation() {
791 let field_name = to_field_ident(field.name());
792 code.push_str(&format!(
793 " pub {}: Option<prax_query::SortOrder>,\n",
794 field_name
795 ));
796 }
797 }
798
799 code.push_str("}\n\n");
800 }
801
802 Ok(code)
803}
804
805fn field_type_to_rust(
807 field_type: &prax_schema::ast::FieldType,
808 modifier: prax_schema::ast::TypeModifier,
809) -> String {
810 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
811
812 let base_type = match field_type {
813 FieldType::Scalar(scalar) => match scalar {
814 ScalarType::Int => "i32".to_string(),
815 ScalarType::BigInt => "i64".to_string(),
816 ScalarType::Float => "f64".to_string(),
817 ScalarType::String => "String".to_string(),
818 ScalarType::Boolean => "bool".to_string(),
819 ScalarType::DateTime => "chrono::DateTime<chrono::Utc>".to_string(),
820 ScalarType::Date => "chrono::NaiveDate".to_string(),
821 ScalarType::Time => "chrono::NaiveTime".to_string(),
822 ScalarType::Json => "serde_json::Value".to_string(),
823 ScalarType::Bytes => "Vec<u8>".to_string(),
824 ScalarType::Decimal => "rust_decimal::Decimal".to_string(),
825 ScalarType::Uuid => "uuid::Uuid".to_string(),
826 ScalarType::Cuid => "String".to_string(),
827 ScalarType::Cuid2 => "String".to_string(),
828 ScalarType::NanoId => "String".to_string(),
829 ScalarType::Ulid => "String".to_string(),
830 ScalarType::Vector(_) | ScalarType::HalfVector(_) => "Vec<f32>".to_string(),
831 ScalarType::SparseVector(_) => "Vec<(u32, f32)>".to_string(),
832 ScalarType::Bit(_) => "Vec<u8>".to_string(),
833 },
834 FieldType::Model(name) => name.to_string(),
835 FieldType::Enum(name) => name.to_string(),
836 FieldType::Composite(name) => name.to_string(),
837 FieldType::Unsupported(_) => "serde_json::Value".to_string(),
838 };
839
840 match modifier {
841 TypeModifier::Optional | TypeModifier::OptionalList => format!("Option<{}>", base_type),
842 TypeModifier::List => format!("Vec<{}>", base_type),
843 TypeModifier::Required => base_type,
844 }
845}
846
847fn field_type_to_rust_with_boxing(
849 field_type: &prax_schema::ast::FieldType,
850 modifier: prax_schema::ast::TypeModifier,
851 source_model: &str,
852 relation_graph: &HashMap<String, HashSet<String>>,
853) -> String {
854 use prax_schema::ast::{FieldType, TypeModifier};
855
856 if let FieldType::Model(target) = field_type
865 && !matches!(modifier, TypeModifier::List)
866 {
867 let should_box = needs_boxing(source_model, target, relation_graph);
868 let base = target.to_string();
869 return if should_box {
870 format!("Option<Box<{}>>", base)
871 } else {
872 format!("Option<{}>", base)
873 };
874 }
875
876 field_type_to_rust(field_type, modifier)
878}
879
880fn field_to_filter_type(field_type: &prax_schema::ast::FieldType) -> String {
882 use prax_schema::ast::{FieldType, ScalarType};
883
884 match field_type {
885 FieldType::Scalar(scalar) => match scalar {
886 ScalarType::Int | ScalarType::BigInt => "ScalarFilter<i64>".to_string(),
887 ScalarType::Float | ScalarType::Decimal => "ScalarFilter<f64>".to_string(),
888 ScalarType::String
889 | ScalarType::Uuid
890 | ScalarType::Cuid
891 | ScalarType::Cuid2
892 | ScalarType::NanoId
893 | ScalarType::Ulid => "ScalarFilter<String>".to_string(),
894 ScalarType::Boolean => "ScalarFilter<bool>".to_string(),
895 ScalarType::DateTime => "ScalarFilter<chrono::DateTime<chrono::Utc>>".to_string(),
896 ScalarType::Date => "ScalarFilter<chrono::NaiveDate>".to_string(),
897 ScalarType::Time => "ScalarFilter<chrono::NaiveTime>".to_string(),
898 ScalarType::Json => "ScalarFilter<serde_json::Value>".to_string(),
899 ScalarType::Bytes => "ScalarFilter<Vec<u8>>".to_string(),
900 ScalarType::Vector(_) | ScalarType::HalfVector(_) => {
907 "prax_pgvector::filter::VectorFilter".to_string()
908 }
909 ScalarType::SparseVector(_) => "ScalarFilter<Vec<(u32, f32)>>".to_string(),
910 ScalarType::Bit(_) => "ScalarFilter<Vec<u8>>".to_string(),
911 },
912 FieldType::Enum(name) => format!("ScalarFilter<{}>", name),
913 _ => "Filter".to_string(),
914 }
915}
916
917fn to_snake_case(name: &str) -> String {
919 let mut result = String::new();
920 for (i, c) in name.chars().enumerate() {
921 if c.is_uppercase() {
922 if i > 0 {
923 result.push('_');
924 }
925 result.push(c.to_lowercase().next().unwrap());
926 } else {
927 result.push(c);
928 }
929 }
930 result
931}
932
933fn to_field_ident(name: &str) -> String {
948 let snake = to_snake_case(name);
949 if is_rust_keyword(&snake) {
950 format!("r#{}", snake)
951 } else {
952 snake
953 }
954}
955
956fn is_rust_keyword(s: &str) -> bool {
957 matches!(
958 s,
959 "abstract"
960 | "as"
961 | "async"
962 | "await"
963 | "become"
964 | "box"
965 | "break"
966 | "const"
967 | "continue"
968 | "do"
969 | "dyn"
970 | "else"
971 | "enum"
972 | "extern"
973 | "false"
974 | "final"
975 | "fn"
976 | "for"
977 | "gen"
978 | "if"
979 | "impl"
980 | "in"
981 | "let"
982 | "loop"
983 | "macro"
984 | "match"
985 | "mod"
986 | "move"
987 | "mut"
988 | "override"
989 | "priv"
990 | "pub"
991 | "ref"
992 | "return"
993 | "static"
994 | "struct"
995 | "trait"
996 | "true"
997 | "try"
998 | "type"
999 | "typeof"
1000 | "unsafe"
1001 | "unsized"
1002 | "use"
1003 | "virtual"
1004 | "where"
1005 | "while"
1006 | "yield"
1007 )
1008}
1009
1010fn to_pascal_case(name: &str) -> String {
1012 if name.is_empty() {
1013 return String::new();
1014 }
1015
1016 let first = name.chars().next().unwrap();
1018 if first.is_uppercase() && name.chars().any(|c| c.is_lowercase()) && !name.contains('_') {
1019 return name.to_string();
1020 }
1021
1022 name.split('_')
1024 .filter(|s| !s.is_empty())
1025 .map(|segment| {
1026 let mut chars = segment.chars();
1027 match chars.next() {
1028 None => String::new(),
1029 Some(first) => {
1030 let rest: String = chars.collect();
1031 format!("{}{}", first.to_uppercase(), rest.to_lowercase())
1032 }
1033 }
1034 })
1035 .collect()
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 #[test]
1043 fn test_to_snake_case() {
1044 assert_eq!(to_snake_case("BoardMember"), "board_member");
1045 assert_eq!(to_snake_case("User"), "user");
1046 assert_eq!(to_snake_case("JiraImportConfig"), "jira_import_config");
1047 }
1048
1049 #[test]
1050 fn test_to_pascal_case_from_snake() {
1051 assert_eq!(to_pascal_case("card_created"), "CardCreated");
1052 assert_eq!(to_pascal_case("branch_deleted"), "BranchDeleted");
1053 assert_eq!(to_pascal_case("pr_merged"), "PrMerged");
1054 }
1055
1056 #[test]
1057 fn test_to_pascal_case_from_screaming() {
1058 assert_eq!(to_pascal_case("CARD_CREATED"), "CardCreated");
1059 assert_eq!(to_pascal_case("PR_MERGED"), "PrMerged");
1060 }
1061
1062 #[test]
1063 fn test_to_pascal_case_already_pascal() {
1064 assert_eq!(to_pascal_case("Admin"), "Admin");
1065 assert_eq!(to_pascal_case("SuperAdmin"), "SuperAdmin");
1066 assert_eq!(to_pascal_case("Low"), "Low");
1067 }
1068
1069 #[test]
1070 fn test_to_pascal_case_single_word() {
1071 assert_eq!(to_pascal_case("active"), "Active");
1072 assert_eq!(to_pascal_case("ACTIVE"), "Active");
1073 }
1074
1075 #[test]
1076 fn test_needs_boxing_direct_cycle() {
1077 let mut graph = HashMap::new();
1078 graph.insert(
1079 "Board".to_string(),
1080 HashSet::from(["JiraConfig".to_string()]),
1081 );
1082 graph.insert(
1083 "JiraConfig".to_string(),
1084 HashSet::from(["Board".to_string()]),
1085 );
1086
1087 assert!(needs_boxing("Board", "JiraConfig", &graph));
1088 assert!(needs_boxing("JiraConfig", "Board", &graph));
1089 }
1090
1091 #[test]
1092 fn test_needs_boxing_no_cycle() {
1093 let mut graph = HashMap::new();
1094 graph.insert("Post".to_string(), HashSet::from(["User".to_string()]));
1095 graph.insert("User".to_string(), HashSet::new());
1096
1097 assert!(!needs_boxing("Post", "User", &graph));
1098 }
1099
1100 #[test]
1101 fn test_needs_boxing_indirect_cycle() {
1102 let mut graph = HashMap::new();
1103 graph.insert("A".to_string(), HashSet::from(["B".to_string()]));
1104 graph.insert("B".to_string(), HashSet::from(["C".to_string()]));
1105 graph.insert("C".to_string(), HashSet::from(["A".to_string()]));
1106
1107 assert!(needs_boxing("A", "B", &graph));
1108 assert!(needs_boxing("B", "C", &graph));
1109 assert!(needs_boxing("C", "A", &graph));
1110 }
1111}