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 code.push_str(" /// Borrow the underlying engine. Useful when composing\n");
275 code.push_str(" /// per-model operations directly or running raw SQL.\n");
276 code.push_str(" pub fn engine(&self) -> &E {\n");
277 code.push_str(" &self.engine\n");
278 code.push_str(" }\n\n");
279
280 code.push_str(" /// Execute a typed raw SQL query, decoding each returned\n");
281 code.push_str(" /// row as `T`. Mirrors `prax_orm::PraxClient::query_raw`.\n");
282 code.push_str(" ///\n");
283 code.push_str(" /// The typed per-model API covers the common CRUD cases;\n");
284 code.push_str(" /// use this for window functions, vendor-specific\n");
285 code.push_str(" /// extensions, CTEs, JOIN-driven row shapes, and aggregates\n");
286 code.push_str(" /// that the fluent builder doesn't model yet. `T` must\n");
287 code.push_str(" /// implement both `Model` (for the table association) and\n");
288 code.push_str(" /// `FromRow` (for row decoding); the generator emits both\n");
289 code.push_str(" /// impls on every model in this client.\n");
290 code.push_str(" pub async fn query_raw<T>(&self, sql: prax_query::raw::Sql)\n");
291 code.push_str(" -> prax_query::error::QueryResult<Vec<T>>\n");
292 code.push_str(" where\n");
293 code.push_str(
294 " T: prax_query::traits::Model + prax_query::row::FromRow + Send + 'static,\n",
295 );
296 code.push_str(" {\n");
297 code.push_str(" let (s, p) = sql.build();\n");
298 code.push_str(" self.engine.query_many::<T>(&s, p).await\n");
299 code.push_str(" }\n\n");
300
301 code.push_str(" /// Execute a raw statement that doesn't return rows\n");
302 code.push_str(" /// (INSERT / UPDATE / DELETE / DDL). Returns the\n");
303 code.push_str(" /// driver-reported affected-row count. Mirrors\n");
304 code.push_str(" /// `prax_orm::PraxClient::execute_raw`.\n");
305 code.push_str(" pub async fn execute_raw(&self, sql: prax_query::raw::Sql)\n");
306 code.push_str(" -> prax_query::error::QueryResult<u64>\n");
307 code.push_str(" {\n");
308 code.push_str(" let (s, p) = sql.build();\n");
309 code.push_str(" self.engine.execute_raw(&s, p).await\n");
310 code.push_str(" }\n\n");
311
312 for model in schema.models.values() {
313 let snake_name = to_snake_case(model.name());
314 code.push_str(&format!(" /// Access {} operations\n", model.name()));
315 code.push_str(&format!(
316 " pub fn {}(&self) -> {}::Client<E> {{\n",
317 snake_name, snake_name,
318 ));
319 code.push_str(&format!(
320 " {}::Client::new(self.engine.clone())\n",
321 snake_name,
322 ));
323 code.push_str(" }\n\n");
324 }
325
326 code.push_str("}\n");
327
328 Ok(code)
329}
330
331fn generate_model_module(
333 model: &prax_schema::ast::Model,
334 features: &[String],
335 relation_graph: &HashMap<String, HashSet<String>>,
336) -> CliResult<String> {
337 let mut code = String::new();
338
339 code.push_str(&format!(
340 "//! Auto-generated module for {} model\n\n",
341 model.name()
342 ));
343
344 code.push_str("#[allow(unused_imports)]\n");
346 code.push_str("use super::*;\n");
347 code.push_str("#[allow(unused_imports)]\n");
348 code.push_str("use prax_query::traits::Model;\n\n");
349
350 let mut derives = vec!["Debug", "Clone"];
352 if features.contains(&"serde".to_string()) {
353 derives.push("serde::Serialize");
354 derives.push("serde::Deserialize");
355 }
356
357 code.push_str("#[allow(dead_code)]\n");
359 code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
360 code.push_str(&format!("pub struct {} {{\n", model.name()));
361
362 for field in model.fields.values() {
363 let field_name = to_field_ident(field.name());
364
365 if let Some(attr) = field.get_attribute("map")
367 && features.contains(&"serde".to_string())
368 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
369 {
370 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
371 }
372
373 let rust_type = field_type_to_rust_with_boxing(
374 &field.field_type,
375 field.modifier,
376 model.name(),
377 relation_graph,
378 );
379 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
380 }
381
382 code.push_str("}\n\n");
383
384 let table_name = model.table_name();
386 let id_fields: Vec<&str> = model.id_fields().iter().map(|f| f.name()).collect();
387 let scalar_columns: Vec<String> = model
388 .scalar_fields()
389 .iter()
390 .map(|f| {
391 f.get_attribute("map")
393 .and_then(|a| a.first_arg())
394 .and_then(|v| v.as_string())
395 .map(|s| s.to_string())
396 .unwrap_or_else(|| to_snake_case(f.name()))
397 })
398 .collect();
399
400 code.push_str(&format!("impl Model for {} {{\n", model.name()));
401 code.push_str(&format!(
402 " const MODEL_NAME: &'static str = \"{}\";\n",
403 model.name()
404 ));
405 code.push_str(&format!(
406 " const TABLE_NAME: &'static str = \"{}\";\n",
407 table_name
408 ));
409 code.push_str(&format!(
410 " const PRIMARY_KEY: &'static [&'static str] = &[{}];\n",
411 id_fields
412 .iter()
413 .map(|f| format!("\"{}\"", to_snake_case(f)))
414 .collect::<Vec<_>>()
415 .join(", ")
416 ));
417 code.push_str(&format!(
418 " const COLUMNS: &'static [&'static str] = &[{}];\n",
419 scalar_columns
420 .iter()
421 .map(|c| format!("\"{}\"", c))
422 .collect::<Vec<_>>()
423 .join(", ")
424 ));
425 code.push_str("}\n\n");
426
427 code.push_str(&format!(
433 "impl prax_query::row::FromRow for {} {{\n",
434 model.name()
435 ));
436 code.push_str(
437 " fn from_row(row: &impl prax_query::row::RowRef)\n -> Result<Self, prax_query::row::RowError>\n {\n",
438 );
439 code.push_str(" Ok(Self {\n");
440 for field in model.fields.values() {
441 let field_name = to_field_ident(field.name());
442 let rust_type = field_type_to_rust_with_boxing(
443 &field.field_type,
444 field.modifier,
445 model.name(),
446 relation_graph,
447 );
448 if field.is_relation() {
449 code.push_str(&format!(
450 " {}: ::core::default::Default::default(),\n",
451 field_name
452 ));
453 } else {
454 let column = field
455 .get_attribute("map")
456 .and_then(|a| a.first_arg())
457 .and_then(|v| v.as_string())
458 .map(|s| s.to_string())
459 .unwrap_or_else(|| field_name.clone());
460 code.push_str(&format!(
461 " {}: <{} as prax_query::row::FromColumn>::from_column(row, \"{}\")?,\n",
462 field_name, rust_type, column
463 ));
464 }
465 }
466 code.push_str(" })\n");
467 code.push_str(" }\n");
468 code.push_str("}\n\n");
469
470 code.push_str(&format!(
475 "impl prax_query::traits::ModelWithPk for {} {{\n",
476 model.name()
477 ));
478 code.push_str(" fn pk_value(&self) -> prax_query::filter::FilterValue {\n");
479 let id_field_objs: Vec<_> = model.id_fields();
480 if id_field_objs.len() == 1 {
481 let f = id_field_objs[0];
482 code.push_str(&format!(
483 " <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n",
484 field_type_to_rust_with_boxing(&f.field_type, f.modifier, model.name(), relation_graph),
485 to_field_ident(f.name())
486 ));
487 } else if id_field_objs.is_empty() {
488 code.push_str(" prax_query::filter::FilterValue::Null\n");
489 } else {
490 code.push_str(" prax_query::filter::FilterValue::List(vec![\n");
491 for f in &id_field_objs {
492 code.push_str(&format!(
493 " <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{}),\n",
494 field_type_to_rust_with_boxing(&f.field_type, f.modifier, model.name(), relation_graph),
495 to_field_ident(f.name())
496 ));
497 }
498 code.push_str(" ])\n");
499 }
500 code.push_str(" }\n\n");
501
502 code.push_str(
503 " fn get_column_value(&self, column: &str)\n -> ::core::option::Option<prax_query::filter::FilterValue>\n {\n",
504 );
505 code.push_str(" match column {\n");
506 for field in model.scalar_fields() {
507 let field_name = to_field_ident(field.name());
509 let column = field
513 .get_attribute("map")
514 .and_then(|a| a.first_arg())
515 .and_then(|v| v.as_string())
516 .map(|s| s.to_string())
517 .unwrap_or_else(|| to_snake_case(field.name()));
518 let rust_type = field_type_to_rust_with_boxing(
519 &field.field_type,
520 field.modifier,
521 model.name(),
522 relation_graph,
523 );
524 code.push_str(&format!(
525 " \"{}\" => ::core::option::Option::Some(\n <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n ),\n",
526 column, rust_type, field_name
527 ));
528 }
529 code.push_str(" _ => ::core::option::Option::None,\n");
530 code.push_str(" }\n");
531 code.push_str(" }\n");
532 code.push_str("}\n\n");
533
534 code.push_str("#[allow(dead_code)]\n");
539 code.push_str(&format!("/// Operations for the {} model\n", model.name()));
540 code.push_str("pub struct Client<E: prax_query::QueryEngine> {\n");
541 code.push_str(" engine: E,\n");
542 code.push_str("}\n\n");
543
544 code.push_str("impl<E: prax_query::QueryEngine> Client<E> {\n");
545 code.push_str(" pub fn new(engine: E) -> Self {\n");
546 code.push_str(" Self { engine }\n");
547 code.push_str(" }\n\n");
548
549 let model_ty = model.name();
550 let crud_methods: &[(&str, &str, &str)] = &[
551 ("find_many", "FindManyOperation", "Find many records"),
552 ("find_unique", "FindUniqueOperation", "Find a unique record"),
553 (
554 "find_first",
555 "FindFirstOperation",
556 "Find the first matching record",
557 ),
558 ("create", "CreateOperation", "Create a new record"),
559 (
560 "create_many",
561 "CreateManyOperation",
562 "Create many records in one operation",
563 ),
564 ("update", "UpdateOperation", "Update a record"),
565 (
566 "update_many",
567 "UpdateManyOperation",
568 "Update many records matching a filter",
569 ),
570 ("upsert", "UpsertOperation", "Insert or update a record"),
571 ("delete", "DeleteOperation", "Delete a record"),
572 (
573 "delete_many",
574 "DeleteManyOperation",
575 "Delete many records matching a filter",
576 ),
577 ("count", "CountOperation", "Count records"),
578 ];
579 for (method, op_ty, doc) in crud_methods {
580 code.push_str(&format!(" /// {}\n", doc));
581 code.push_str(&format!(
582 " pub fn {}(&self) -> prax_query::operations::{}<E, {}> {{\n",
583 method, op_ty, model_ty,
584 ));
585 code.push_str(&format!(
586 " prax_query::operations::{}::new(self.engine.clone())\n",
587 op_ty,
588 ));
589 code.push_str(" }\n\n");
590 }
591
592 code.push_str("}\n");
593
594 Ok(code)
595}
596
597fn generate_enum_module(enum_def: &prax_schema::ast::Enum) -> CliResult<String> {
599 let mut code = String::new();
600
601 code.push_str(&format!(
602 "//! Auto-generated module for {} enum\n\n",
603 enum_def.name()
604 ));
605
606 code.push_str("#[allow(dead_code)]\n");
607 code.push_str(
608 "#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n",
609 );
610 code.push_str(&format!("pub enum {} {{\n", enum_def.name()));
611
612 for variant in &enum_def.variants {
613 let raw_name = variant.name();
614 let pascal_name = to_pascal_case(raw_name);
615
616 if let Some(attr) = variant.attributes.iter().find(|a| a.is("map"))
618 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
619 {
620 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
621 code.push_str(&format!(" {},\n", pascal_name));
622 continue;
623 }
624
625 if raw_name != pascal_name {
627 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", raw_name));
628 }
629 code.push_str(&format!(" {},\n", pascal_name));
630 }
631
632 code.push_str("}\n\n");
633
634 code.push_str(&format!(
636 "impl std::fmt::Display for {} {{\n",
637 enum_def.name()
638 ));
639 code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
640 code.push_str(" match self {\n");
641 for variant in &enum_def.variants {
642 let raw_name = variant.name();
643 let pascal_name = to_pascal_case(raw_name);
644 let db_value = variant.db_value();
645 code.push_str(&format!(
646 " Self::{} => write!(f, \"{}\"),\n",
647 pascal_name, db_value
648 ));
649 }
650 code.push_str(" }\n");
651 code.push_str(" }\n");
652 code.push_str("}\n\n");
653
654 if let Some(default_variant) = enum_def.variants.first() {
656 let pascal_name = to_pascal_case(default_variant.name());
657 code.push_str(&format!("impl Default for {} {{\n", enum_def.name()));
658 code.push_str(&format!(
659 " fn default() -> Self {{\n Self::{}\n }}\n",
660 pascal_name
661 ));
662 code.push_str("}\n\n");
663 }
664
665 code.push_str(&format!(
669 "impl std::str::FromStr for {} {{\n",
670 enum_def.name()
671 ));
672 code.push_str(" type Err = prax_query::row::RowError;\n");
673 code.push_str(" fn from_str(s: &str) -> Result<Self, Self::Err> {\n");
674 code.push_str(" match s {\n");
675 for variant in &enum_def.variants {
676 let raw_name = variant.name();
677 let pascal_name = to_pascal_case(raw_name);
678 let db_value = variant.db_value();
679 code.push_str(&format!(
680 " \"{}\" => Ok(Self::{}),\n",
681 db_value, pascal_name
682 ));
683 }
684 code.push_str(&format!(
685 " _ => Err(prax_query::row::RowError::TypeConversion {{\n column: String::new(),\n message: format!(\"unknown {} variant: {{}}\", s),\n }}),\n",
686 enum_def.name()
687 ));
688 code.push_str(" }\n");
689 code.push_str(" }\n");
690 code.push_str("}\n\n");
691
692 code.push_str(&format!(
696 "impl prax_query::row::FromColumn for {} {{\n",
697 enum_def.name()
698 ));
699 code.push_str(
700 " fn from_column(row: &impl prax_query::row::RowRef, column: &str)\n -> Result<Self, prax_query::row::RowError>\n {\n",
701 );
702 code.push_str(" let raw = row.get_string(column)?;\n");
703 code.push_str(" <Self as std::str::FromStr>::from_str(&raw).map_err(|e| {\n");
704 code.push_str(" let msg = match &e {\n");
705 code.push_str(
706 " prax_query::row::RowError::TypeConversion { message, .. } => message.clone(),\n",
707 );
708 code.push_str(" other => other.to_string(),\n");
709 code.push_str(" };\n");
710 code.push_str(" prax_query::row::RowError::TypeConversion {\n");
711 code.push_str(" column: column.to_string(),\n");
712 code.push_str(" message: msg,\n");
713 code.push_str(" }\n");
714 code.push_str(" })\n");
715 code.push_str(" }\n");
716 code.push_str("}\n\n");
717
718 code.push_str(&format!(
727 "impl prax_query::filter::ToFilterValue for {} {{\n",
728 enum_def.name()
729 ));
730 code.push_str(" fn to_filter_value(&self) -> prax_query::filter::FilterValue {\n");
731 code.push_str(" prax_query::filter::FilterValue::String(self.to_string())\n");
732 code.push_str(" }\n");
733 code.push_str("}\n");
734
735 Ok(code)
736}
737
738fn generate_types_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
740 let mut code = String::new();
741
742 code.push_str("//! Common type definitions\n\n");
743 code.push_str("#[allow(unused_imports)]\npub use chrono::{DateTime, Utc};\n");
744 code.push_str("#[allow(unused_imports)]\npub use uuid::Uuid;\n");
745 code.push_str("#[allow(unused_imports)]\npub use serde_json::Value as Json;\n");
746 code.push('\n');
747
748 for composite in schema.types.values() {
750 code.push_str("#[allow(dead_code)]\n");
751 code.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
752 code.push_str(&format!("pub struct {} {{\n", composite.name()));
753 for field in composite.fields.values() {
754 let rust_type = field_type_to_rust(&field.field_type, field.modifier);
755 let field_name = to_field_ident(field.name());
756 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
757 }
758 code.push_str("}\n\n");
759 }
760
761 Ok(code)
762}
763
764fn generate_filters_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
766 let mut code = String::new();
767
768 code.push_str("//! Filter types for queries\n\n");
769 code.push_str("#[allow(unused_imports)]\n");
770 code.push_str("use prax_query::filter::{Filter, ScalarFilter};\n");
771
772 let mut referenced_enums = HashSet::new();
774 for model in schema.models.values() {
775 for field in model.fields.values() {
776 if !field.is_relation()
777 && let prax_schema::ast::FieldType::Enum(ref name) = field.field_type
778 {
779 referenced_enums.insert(name.to_string());
780 }
781 }
782 }
783
784 for enum_name in &referenced_enums {
786 code.push_str(&format!(
787 "#[allow(unused_imports)]\nuse super::{}::{};\n",
788 to_snake_case(enum_name),
789 enum_name
790 ));
791 }
792
793 code.push('\n');
794
795 for model in schema.models.values() {
796 code.push_str("#[allow(dead_code)]\n");
798 code.push_str(&format!("/// Filter input for {} queries\n", model.name()));
799 code.push_str("#[derive(Debug, Default, Clone)]\n");
800 code.push_str(&format!("pub struct {}WhereInput {{\n", model.name()));
801
802 for field in model.fields.values() {
803 if !field.is_relation() {
804 let filter_type = field_to_filter_type(&field.field_type);
805 let field_name = to_field_ident(field.name());
806 code.push_str(&format!(
807 " pub {}: Option<{}>,\n",
808 field_name, filter_type
809 ));
810 }
811 }
812
813 code.push_str(" pub and: Option<Vec<Self>>,\n");
814 code.push_str(" pub or: Option<Vec<Self>>,\n");
815 code.push_str(" pub not: Option<Box<Self>>,\n");
816 code.push_str("}\n\n");
817
818 code.push_str("#[allow(dead_code)]\n");
820 code.push_str(&format!(
821 "/// Order by input for {} queries\n",
822 model.name()
823 ));
824 code.push_str("#[derive(Debug, Default, Clone)]\n");
825 code.push_str(&format!("pub struct {}OrderByInput {{\n", model.name()));
826
827 for field in model.fields.values() {
828 if !field.is_relation() {
829 let field_name = to_field_ident(field.name());
830 code.push_str(&format!(
831 " pub {}: Option<prax_query::SortOrder>,\n",
832 field_name
833 ));
834 }
835 }
836
837 code.push_str("}\n\n");
838 }
839
840 Ok(code)
841}
842
843fn field_type_to_rust(
845 field_type: &prax_schema::ast::FieldType,
846 modifier: prax_schema::ast::TypeModifier,
847) -> String {
848 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
849
850 let base_type = match field_type {
851 FieldType::Scalar(scalar) => match scalar {
852 ScalarType::Int => "i32".to_string(),
853 ScalarType::BigInt => "i64".to_string(),
854 ScalarType::Float => "f64".to_string(),
855 ScalarType::String => "String".to_string(),
856 ScalarType::Boolean => "bool".to_string(),
857 ScalarType::DateTime => "chrono::DateTime<chrono::Utc>".to_string(),
858 ScalarType::Date => "chrono::NaiveDate".to_string(),
859 ScalarType::Time => "chrono::NaiveTime".to_string(),
860 ScalarType::Json => "serde_json::Value".to_string(),
861 ScalarType::Bytes => "Vec<u8>".to_string(),
862 ScalarType::Decimal => "rust_decimal::Decimal".to_string(),
863 ScalarType::Uuid => "uuid::Uuid".to_string(),
864 ScalarType::Cuid => "String".to_string(),
865 ScalarType::Cuid2 => "String".to_string(),
866 ScalarType::NanoId => "String".to_string(),
867 ScalarType::Ulid => "String".to_string(),
868 ScalarType::Vector(_) | ScalarType::HalfVector(_) => "Vec<f32>".to_string(),
869 ScalarType::SparseVector(_) => "Vec<(u32, f32)>".to_string(),
870 ScalarType::Bit(_) => "Vec<u8>".to_string(),
871 },
872 FieldType::Model(name) => name.to_string(),
873 FieldType::Enum(name) => name.to_string(),
874 FieldType::Composite(name) => name.to_string(),
875 FieldType::Unsupported(_) => "serde_json::Value".to_string(),
876 };
877
878 match modifier {
879 TypeModifier::Optional | TypeModifier::OptionalList => format!("Option<{}>", base_type),
880 TypeModifier::List => format!("Vec<{}>", base_type),
881 TypeModifier::Required => base_type,
882 }
883}
884
885fn field_type_to_rust_with_boxing(
887 field_type: &prax_schema::ast::FieldType,
888 modifier: prax_schema::ast::TypeModifier,
889 source_model: &str,
890 relation_graph: &HashMap<String, HashSet<String>>,
891) -> String {
892 use prax_schema::ast::{FieldType, TypeModifier};
893
894 if let FieldType::Model(target) = field_type
903 && !matches!(modifier, TypeModifier::List)
904 {
905 let should_box = needs_boxing(source_model, target, relation_graph);
906 let base = target.to_string();
907 return if should_box {
908 format!("Option<Box<{}>>", base)
909 } else {
910 format!("Option<{}>", base)
911 };
912 }
913
914 field_type_to_rust(field_type, modifier)
916}
917
918fn field_to_filter_type(field_type: &prax_schema::ast::FieldType) -> String {
920 use prax_schema::ast::{FieldType, ScalarType};
921
922 match field_type {
923 FieldType::Scalar(scalar) => match scalar {
924 ScalarType::Int | ScalarType::BigInt => "ScalarFilter<i64>".to_string(),
925 ScalarType::Float | ScalarType::Decimal => "ScalarFilter<f64>".to_string(),
926 ScalarType::String
927 | ScalarType::Uuid
928 | ScalarType::Cuid
929 | ScalarType::Cuid2
930 | ScalarType::NanoId
931 | ScalarType::Ulid => "ScalarFilter<String>".to_string(),
932 ScalarType::Boolean => "ScalarFilter<bool>".to_string(),
933 ScalarType::DateTime => "ScalarFilter<chrono::DateTime<chrono::Utc>>".to_string(),
934 ScalarType::Date => "ScalarFilter<chrono::NaiveDate>".to_string(),
935 ScalarType::Time => "ScalarFilter<chrono::NaiveTime>".to_string(),
936 ScalarType::Json => "ScalarFilter<serde_json::Value>".to_string(),
937 ScalarType::Bytes => "ScalarFilter<Vec<u8>>".to_string(),
938 ScalarType::Vector(_) | ScalarType::HalfVector(_) => {
945 "prax_pgvector::filter::VectorFilter".to_string()
946 }
947 ScalarType::SparseVector(_) => "ScalarFilter<Vec<(u32, f32)>>".to_string(),
948 ScalarType::Bit(_) => "ScalarFilter<Vec<u8>>".to_string(),
949 },
950 FieldType::Enum(name) => format!("ScalarFilter<{}>", name),
951 _ => "Filter".to_string(),
952 }
953}
954
955fn to_snake_case(name: &str) -> String {
957 let mut result = String::new();
958 for (i, c) in name.chars().enumerate() {
959 if c.is_uppercase() {
960 if i > 0 {
961 result.push('_');
962 }
963 result.push(c.to_lowercase().next().unwrap());
964 } else {
965 result.push(c);
966 }
967 }
968 result
969}
970
971fn to_field_ident(name: &str) -> String {
986 let snake = to_snake_case(name);
987 if is_rust_keyword(&snake) {
988 format!("r#{}", snake)
989 } else {
990 snake
991 }
992}
993
994fn is_rust_keyword(s: &str) -> bool {
995 matches!(
996 s,
997 "abstract"
998 | "as"
999 | "async"
1000 | "await"
1001 | "become"
1002 | "box"
1003 | "break"
1004 | "const"
1005 | "continue"
1006 | "do"
1007 | "dyn"
1008 | "else"
1009 | "enum"
1010 | "extern"
1011 | "false"
1012 | "final"
1013 | "fn"
1014 | "for"
1015 | "gen"
1016 | "if"
1017 | "impl"
1018 | "in"
1019 | "let"
1020 | "loop"
1021 | "macro"
1022 | "match"
1023 | "mod"
1024 | "move"
1025 | "mut"
1026 | "override"
1027 | "priv"
1028 | "pub"
1029 | "ref"
1030 | "return"
1031 | "static"
1032 | "struct"
1033 | "trait"
1034 | "true"
1035 | "try"
1036 | "type"
1037 | "typeof"
1038 | "unsafe"
1039 | "unsized"
1040 | "use"
1041 | "virtual"
1042 | "where"
1043 | "while"
1044 | "yield"
1045 )
1046}
1047
1048fn to_pascal_case(name: &str) -> String {
1050 if name.is_empty() {
1051 return String::new();
1052 }
1053
1054 let first = name.chars().next().unwrap();
1056 if first.is_uppercase() && name.chars().any(|c| c.is_lowercase()) && !name.contains('_') {
1057 return name.to_string();
1058 }
1059
1060 name.split('_')
1062 .filter(|s| !s.is_empty())
1063 .map(|segment| {
1064 let mut chars = segment.chars();
1065 match chars.next() {
1066 None => String::new(),
1067 Some(first) => {
1068 let rest: String = chars.collect();
1069 format!("{}{}", first.to_uppercase(), rest.to_lowercase())
1070 }
1071 }
1072 })
1073 .collect()
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078 use super::*;
1079
1080 #[test]
1081 fn test_to_snake_case() {
1082 assert_eq!(to_snake_case("BoardMember"), "board_member");
1083 assert_eq!(to_snake_case("User"), "user");
1084 assert_eq!(to_snake_case("JiraImportConfig"), "jira_import_config");
1085 }
1086
1087 #[test]
1088 fn test_to_pascal_case_from_snake() {
1089 assert_eq!(to_pascal_case("card_created"), "CardCreated");
1090 assert_eq!(to_pascal_case("branch_deleted"), "BranchDeleted");
1091 assert_eq!(to_pascal_case("pr_merged"), "PrMerged");
1092 }
1093
1094 #[test]
1095 fn test_to_pascal_case_from_screaming() {
1096 assert_eq!(to_pascal_case("CARD_CREATED"), "CardCreated");
1097 assert_eq!(to_pascal_case("PR_MERGED"), "PrMerged");
1098 }
1099
1100 #[test]
1101 fn test_to_pascal_case_already_pascal() {
1102 assert_eq!(to_pascal_case("Admin"), "Admin");
1103 assert_eq!(to_pascal_case("SuperAdmin"), "SuperAdmin");
1104 assert_eq!(to_pascal_case("Low"), "Low");
1105 }
1106
1107 #[test]
1108 fn test_to_pascal_case_single_word() {
1109 assert_eq!(to_pascal_case("active"), "Active");
1110 assert_eq!(to_pascal_case("ACTIVE"), "Active");
1111 }
1112
1113 #[test]
1114 fn test_needs_boxing_direct_cycle() {
1115 let mut graph = HashMap::new();
1116 graph.insert(
1117 "Board".to_string(),
1118 HashSet::from(["JiraConfig".to_string()]),
1119 );
1120 graph.insert(
1121 "JiraConfig".to_string(),
1122 HashSet::from(["Board".to_string()]),
1123 );
1124
1125 assert!(needs_boxing("Board", "JiraConfig", &graph));
1126 assert!(needs_boxing("JiraConfig", "Board", &graph));
1127 }
1128
1129 #[test]
1130 fn test_needs_boxing_no_cycle() {
1131 let mut graph = HashMap::new();
1132 graph.insert("Post".to_string(), HashSet::from(["User".to_string()]));
1133 graph.insert("User".to_string(), HashSet::new());
1134
1135 assert!(!needs_boxing("Post", "User", &graph));
1136 }
1137
1138 #[test]
1139 fn test_needs_boxing_indirect_cycle() {
1140 let mut graph = HashMap::new();
1141 graph.insert("A".to_string(), HashSet::from(["B".to_string()]));
1142 graph.insert("B".to_string(), HashSet::from(["C".to_string()]));
1143 graph.insert("C".to_string(), HashSet::from(["A".to_string()]));
1144
1145 assert!(needs_boxing("A", "B", &graph));
1146 assert!(needs_boxing("B", "C", &graph));
1147 assert!(needs_boxing("C", "A", &graph));
1148 }
1149}