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 write_formatted(&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 write_formatted(&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 write_formatted(&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 write_formatted(&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 write_formatted(&filters_path, &filters_code)?;
160 generated_files.push(filters_path);
161
162 Ok(generated_files)
163}
164
165fn write_formatted(path: &Path, code: &str) -> CliResult<()> {
180 let formatted = match syn::parse_file(code) {
181 Ok(file) => prettyplease::unparse(&file),
182 Err(e) => {
183 output::warn(&format!(
184 "generated code at {} did not parse; writing unformatted. \
185 This is a codegen bug: {}",
186 path.display(),
187 e
188 ));
189 code.to_string()
190 }
191 };
192 std::fs::write(path, formatted)?;
193 Ok(())
194}
195
196fn build_relation_graph(schema: &prax_schema::ast::Schema) -> HashMap<String, HashSet<String>> {
200 let mut graph: HashMap<String, HashSet<String>> = HashMap::new();
201
202 for model in schema.models.values() {
203 let entry = graph.entry(model.name().to_string()).or_default();
204 for field in model.fields.values() {
205 if let prax_schema::ast::FieldType::Model(ref target) = field.field_type
206 && !field.is_list()
207 {
208 entry.insert(target.to_string());
209 }
210 }
211 }
212
213 graph
214}
215
216fn needs_boxing(
220 source_model: &str,
221 target_model: &str,
222 graph: &HashMap<String, HashSet<String>>,
223) -> bool {
224 let mut visited = HashSet::new();
225 let mut stack = vec![target_model.to_string()];
226
227 while let Some(current) = stack.pop() {
228 if current == source_model {
229 return true;
230 }
231 if !visited.insert(current.clone()) {
232 continue;
233 }
234 if let Some(neighbors) = graph.get(¤t) {
235 for neighbor in neighbors {
236 stack.push(neighbor.clone());
237 }
238 }
239 }
240
241 false
242}
243
244fn generate_client_module(
246 schema: &prax_schema::ast::Schema,
247 _features: &[String],
248) -> CliResult<String> {
249 let mut code = String::new();
250
251 code.push_str("//! Auto-generated by Prax - DO NOT EDIT\n");
252 code.push_str("//!\n");
253 code.push_str("//! This module contains the generated Prax client.\n\n");
254 code.push_str("#![allow(dead_code)]\n");
261 code.push_str("#![allow(clippy::derivable_impls)]\n");
262 code.push_str("#![allow(clippy::needless_update)]\n");
263 code.push_str("#![allow(clippy::too_many_arguments)]\n\n");
264
265 code.push_str("pub mod types;\n");
267 code.push_str("pub mod filters;\n\n");
268
269 for model in schema.models.values() {
270 code.push_str(&format!("pub mod {};\n", to_snake_case(model.name())));
271 }
272
273 for enum_def in schema.enums.values() {
274 code.push_str(&format!("pub mod {};\n", to_snake_case(enum_def.name())));
275 }
276
277 code.push('\n');
278
279 code.push_str("#[allow(unused_imports)]\npub use types::*;\n");
281 code.push_str("#[allow(unused_imports)]\npub use filters::*;\n\n");
282
283 for model in schema.models.values() {
284 code.push_str(&format!(
285 "#[allow(unused_imports)]\npub use {}::{};\n",
286 to_snake_case(model.name()),
287 model.name()
288 ));
289 }
290
291 for enum_def in schema.enums.values() {
292 code.push_str(&format!(
293 "#[allow(unused_imports)]\npub use {}::{};\n",
294 to_snake_case(enum_def.name()),
295 enum_def.name()
296 ));
297 }
298
299 code.push('\n');
300
301 code.push_str("#[allow(dead_code)]\n");
303 code.push_str("/// The Prax database client\n");
304 code.push_str("#[derive(Clone)]\n");
305 code.push_str("pub struct PraxClient<E: prax_query::QueryEngine> {\n");
306 code.push_str(" engine: E,\n");
307 code.push_str("}\n\n");
308
309 code.push_str("impl<E: prax_query::QueryEngine> PraxClient<E> {\n");
310 code.push_str(" /// Create a new Prax client with the given query engine\n");
311 code.push_str(" pub fn new(engine: E) -> Self {\n");
312 code.push_str(" Self { engine }\n");
313 code.push_str(" }\n\n");
314
315 code.push_str(" /// Borrow the underlying engine. Useful when composing\n");
316 code.push_str(" /// per-model operations directly or running raw SQL.\n");
317 code.push_str(" pub fn engine(&self) -> &E {\n");
318 code.push_str(" &self.engine\n");
319 code.push_str(" }\n\n");
320
321 code.push_str(" /// Execute a typed raw SQL query, decoding each returned\n");
322 code.push_str(" /// row as `T`. Mirrors `prax_orm::PraxClient::query_raw`.\n");
323 code.push_str(" ///\n");
324 code.push_str(" /// The typed per-model API covers the common CRUD cases;\n");
325 code.push_str(" /// use this for window functions, vendor-specific\n");
326 code.push_str(" /// extensions, CTEs, JOIN-driven row shapes, and aggregates\n");
327 code.push_str(" /// that the fluent builder doesn't model yet. `T` must\n");
328 code.push_str(" /// implement both `Model` (for the table association) and\n");
329 code.push_str(" /// `FromRow` (for row decoding); the generator emits both\n");
330 code.push_str(" /// impls on every model in this client.\n");
331 code.push_str(" pub async fn query_raw<T>(&self, sql: prax_query::raw::Sql)\n");
332 code.push_str(" -> prax_query::error::QueryResult<Vec<T>>\n");
333 code.push_str(" where\n");
334 code.push_str(
335 " T: prax_query::traits::Model + prax_query::row::FromRow + Send + 'static,\n",
336 );
337 code.push_str(" {\n");
338 code.push_str(" let (s, p) = sql.build();\n");
339 code.push_str(" self.engine.query_many::<T>(&s, p).await\n");
340 code.push_str(" }\n\n");
341
342 code.push_str(" /// Execute a raw statement that doesn't return rows\n");
343 code.push_str(" /// (INSERT / UPDATE / DELETE / DDL). Returns the\n");
344 code.push_str(" /// driver-reported affected-row count. Mirrors\n");
345 code.push_str(" /// `prax_orm::PraxClient::execute_raw`.\n");
346 code.push_str(" pub async fn execute_raw(&self, sql: prax_query::raw::Sql)\n");
347 code.push_str(" -> prax_query::error::QueryResult<u64>\n");
348 code.push_str(" {\n");
349 code.push_str(" let (s, p) = sql.build();\n");
350 code.push_str(" self.engine.execute_raw(&s, p).await\n");
351 code.push_str(" }\n\n");
352
353 code.push_str(" /// Run a closure inside a transaction. The closure receives\n");
354 code.push_str(" /// a fresh `PraxClient<E>` wrapping the transaction's engine;\n");
355 code.push_str(" /// every query issued through it participates in the same\n");
356 code.push_str(" /// transactional scope. Commits on `Ok`, rolls back on\n");
357 code.push_str(" /// `Err`. Mirrors `prax_orm::PraxClient::transaction`.\n");
358 code.push_str(" ///\n");
359 code.push_str(" /// Nested `transaction()` calls on the same engine currently\n");
360 code.push_str(" /// return `QueryError::internal(...)` until dialect-aware\n");
361 code.push_str(" /// SAVEPOINT support lands.\n");
362 code.push_str(" pub async fn transaction<R, Fut, F>(&self, f: F)\n");
363 code.push_str(" -> prax_query::error::QueryResult<R>\n");
364 code.push_str(" where\n");
365 code.push_str(" F: FnOnce(PraxClient<E>) -> Fut + Send + 'static,\n");
366 code.push_str(" Fut: ::core::future::Future<Output = prax_query::error::QueryResult<R>> + Send + 'static,\n");
367 code.push_str(" R: Send + 'static,\n");
368 code.push_str(" {\n");
369 code.push_str(" self.engine\n");
370 code.push_str(" .transaction(move |tx_engine| async move { f(PraxClient::new(tx_engine)).await })\n");
371 code.push_str(" .await\n");
372 code.push_str(" }\n\n");
373
374 for model in schema.models.values() {
375 let snake_name = to_snake_case(model.name());
376 code.push_str(&format!(" /// Access {} operations\n", model.name()));
377 code.push_str(&format!(
378 " pub fn {}(&self) -> {}::Client<E> {{\n",
379 snake_name, snake_name,
380 ));
381 code.push_str(&format!(
382 " {}::Client::new(self.engine.clone())\n",
383 snake_name,
384 ));
385 code.push_str(" }\n\n");
386 }
387
388 code.push_str("}\n");
389
390 Ok(code)
391}
392
393fn generate_model_module(
395 model: &prax_schema::ast::Model,
396 features: &[String],
397 relation_graph: &HashMap<String, HashSet<String>>,
398) -> CliResult<String> {
399 let mut code = String::new();
400
401 code.push_str(&format!(
402 "//! Auto-generated module for {} model\n\n",
403 model.name()
404 ));
405
406 code.push_str("#[allow(unused_imports)]\n");
408 code.push_str("use super::*;\n");
409 code.push_str("#[allow(unused_imports)]\n");
410 code.push_str("use prax_query::traits::Model;\n\n");
411
412 let mut derives = vec!["Debug", "Clone"];
414 if features.contains(&"serde".to_string()) {
415 derives.push("serde::Serialize");
416 derives.push("serde::Deserialize");
417 }
418
419 code.push_str("#[allow(dead_code)]\n");
421 code.push_str(&format!("#[derive({})]\n", derives.join(", ")));
422 code.push_str(&format!("pub struct {} {{\n", model.name()));
423
424 for field in model.fields.values() {
425 let field_name = to_field_ident(field.name());
426
427 if let Some(attr) = field.get_attribute("map")
429 && features.contains(&"serde".to_string())
430 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
431 {
432 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
433 }
434
435 let rust_type = field_type_to_rust_with_boxing(
436 &field.field_type,
437 field.modifier,
438 model.name(),
439 relation_graph,
440 );
441 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
442 }
443
444 code.push_str("}\n\n");
445
446 let table_name = model.table_name();
448 let id_fields: Vec<&str> = model.id_fields().iter().map(|f| f.name()).collect();
449 let scalar_columns: Vec<String> = model
450 .scalar_fields()
451 .iter()
452 .map(|f| {
453 f.get_attribute("map")
455 .and_then(|a| a.first_arg())
456 .and_then(|v| v.as_string())
457 .map(|s| s.to_string())
458 .unwrap_or_else(|| to_snake_case(f.name()))
459 })
460 .collect();
461
462 code.push_str(&format!("impl Model for {} {{\n", model.name()));
463 code.push_str(&format!(
464 " const MODEL_NAME: &'static str = \"{}\";\n",
465 model.name()
466 ));
467 code.push_str(&format!(
468 " const TABLE_NAME: &'static str = \"{}\";\n",
469 table_name
470 ));
471 code.push_str(&format!(
472 " const PRIMARY_KEY: &'static [&'static str] = &[{}];\n",
473 id_fields
474 .iter()
475 .map(|f| format!("\"{}\"", to_snake_case(f)))
476 .collect::<Vec<_>>()
477 .join(", ")
478 ));
479 code.push_str(&format!(
480 " const COLUMNS: &'static [&'static str] = &[{}];\n",
481 scalar_columns
482 .iter()
483 .map(|c| format!("\"{}\"", c))
484 .collect::<Vec<_>>()
485 .join(", ")
486 ));
487 code.push_str("}\n\n");
488
489 code.push_str(&format!(
495 "impl prax_query::row::FromRow for {} {{\n",
496 model.name()
497 ));
498 code.push_str(
499 " fn from_row(row: &impl prax_query::row::RowRef)\n -> Result<Self, prax_query::row::RowError>\n {\n",
500 );
501 code.push_str(" Ok(Self {\n");
502 for field in model.fields.values() {
503 let field_name = to_field_ident(field.name());
504 let rust_type = field_type_to_rust_with_boxing(
505 &field.field_type,
506 field.modifier,
507 model.name(),
508 relation_graph,
509 );
510 if field.is_relation() {
511 code.push_str(&format!(
512 " {}: ::core::default::Default::default(),\n",
513 field_name
514 ));
515 } else {
516 let column = field
517 .get_attribute("map")
518 .and_then(|a| a.first_arg())
519 .and_then(|v| v.as_string())
520 .map(|s| s.to_string())
521 .unwrap_or_else(|| field_name.clone());
522 code.push_str(&format!(
523 " {}: <{} as prax_query::row::FromColumn>::from_column(row, \"{}\")?,\n",
524 field_name, rust_type, column
525 ));
526 }
527 }
528 code.push_str(" })\n");
529 code.push_str(" }\n");
530 code.push_str("}\n\n");
531
532 code.push_str(&format!(
537 "impl prax_query::traits::ModelWithPk for {} {{\n",
538 model.name()
539 ));
540 code.push_str(" fn pk_value(&self) -> prax_query::filter::FilterValue {\n");
541 let id_field_objs: Vec<_> = model.id_fields();
542 if id_field_objs.len() == 1 {
543 let f = id_field_objs[0];
544 code.push_str(&format!(
545 " <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n",
546 field_type_to_rust_with_boxing(&f.field_type, f.modifier, model.name(), relation_graph),
547 to_field_ident(f.name())
548 ));
549 } else if id_field_objs.is_empty() {
550 code.push_str(" prax_query::filter::FilterValue::Null\n");
551 } else {
552 code.push_str(" prax_query::filter::FilterValue::List(vec![\n");
553 for f in &id_field_objs {
554 code.push_str(&format!(
555 " <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{}),\n",
556 field_type_to_rust_with_boxing(&f.field_type, f.modifier, model.name(), relation_graph),
557 to_field_ident(f.name())
558 ));
559 }
560 code.push_str(" ])\n");
561 }
562 code.push_str(" }\n\n");
563
564 code.push_str(
565 " fn get_column_value(&self, column: &str)\n -> ::core::option::Option<prax_query::filter::FilterValue>\n {\n",
566 );
567 code.push_str(" match column {\n");
568 for field in model.scalar_fields() {
569 let field_name = to_field_ident(field.name());
571 let column = field
575 .get_attribute("map")
576 .and_then(|a| a.first_arg())
577 .and_then(|v| v.as_string())
578 .map(|s| s.to_string())
579 .unwrap_or_else(|| to_snake_case(field.name()));
580 let rust_type = field_type_to_rust_with_boxing(
581 &field.field_type,
582 field.modifier,
583 model.name(),
584 relation_graph,
585 );
586 code.push_str(&format!(
587 " \"{}\" => ::core::option::Option::Some(\n <{} as prax_query::filter::ToFilterValue>::to_filter_value(&self.{})\n ),\n",
588 column, rust_type, field_name
589 ));
590 }
591 code.push_str(" _ => ::core::option::Option::None,\n");
592 code.push_str(" }\n");
593 code.push_str(" }\n");
594 code.push_str("}\n\n");
595
596 code.push_str(&format!(
607 "impl<E: prax_query::traits::QueryEngine>\n prax_query::traits::ModelRelationLoader<E>\n for {}\n{{\n",
608 model.name()
609 ));
610 code.push_str(" fn load_relation<'a>(\n");
611 code.push_str(" _engine: &'a E,\n");
612 code.push_str(" _parents: &'a mut [Self],\n");
613 code.push_str(" spec: &'a prax_query::relations::IncludeSpec,\n");
614 code.push_str(
615 " ) -> prax_query::traits::BoxFuture<'a, prax_query::error::QueryResult<()>> {\n",
616 );
617 code.push_str(" let name = spec.relation_name.clone();\n");
618 code.push_str(&format!(" let model_name = \"{}\";\n", model.name()));
619 code.push_str(" Box::pin(async move {\n");
620 code.push_str(" Err(prax_query::error::QueryError::internal(format!(\n");
621 code.push_str(" \"relation '{}' on schema-generated model '{}' is not wired for .include() — use query_raw for JOINs\",\n");
622 code.push_str(" name,\n");
623 code.push_str(" model_name,\n");
624 code.push_str(" )))\n");
625 code.push_str(" })\n");
626 code.push_str(" }\n");
627 code.push_str("}\n\n");
628
629 code.push_str("#[allow(dead_code)]\n");
634 code.push_str(&format!("/// Operations for the {} model\n", model.name()));
635 code.push_str("pub struct Client<E: prax_query::QueryEngine> {\n");
636 code.push_str(" engine: E,\n");
637 code.push_str("}\n\n");
638
639 code.push_str("impl<E: prax_query::QueryEngine> Client<E> {\n");
640 code.push_str(" pub fn new(engine: E) -> Self {\n");
641 code.push_str(" Self { engine }\n");
642 code.push_str(" }\n\n");
643
644 let model_ty = model.name();
645 let crud_methods: &[(&str, &str, &str)] = &[
646 ("find_many", "FindManyOperation", "Find many records"),
647 ("find_unique", "FindUniqueOperation", "Find a unique record"),
648 (
649 "find_first",
650 "FindFirstOperation",
651 "Find the first matching record",
652 ),
653 ("create", "CreateOperation", "Create a new record"),
654 (
655 "create_many",
656 "CreateManyOperation",
657 "Create many records in one operation",
658 ),
659 ("update", "UpdateOperation", "Update a record"),
660 (
661 "update_many",
662 "UpdateManyOperation",
663 "Update many records matching a filter",
664 ),
665 ("upsert", "UpsertOperation", "Insert or update a record"),
666 ("delete", "DeleteOperation", "Delete a record"),
667 (
668 "delete_many",
669 "DeleteManyOperation",
670 "Delete many records matching a filter",
671 ),
672 ("count", "CountOperation", "Count records"),
673 ];
674 for (method, op_ty, doc) in crud_methods {
675 code.push_str(&format!(" /// {}\n", doc));
676 code.push_str(&format!(
677 " pub fn {}(&self) -> prax_query::operations::{}<E, {}> {{\n",
678 method, op_ty, model_ty,
679 ));
680 code.push_str(&format!(
681 " prax_query::operations::{}::new(self.engine.clone())\n",
682 op_ty,
683 ));
684 code.push_str(" }\n\n");
685 }
686
687 code.push_str("}\n");
688
689 Ok(code)
690}
691
692fn generate_enum_module(enum_def: &prax_schema::ast::Enum) -> CliResult<String> {
694 let mut code = String::new();
695
696 code.push_str(&format!(
697 "//! Auto-generated module for {} enum\n\n",
698 enum_def.name()
699 ));
700
701 code.push_str("#[allow(dead_code)]\n");
702 code.push_str(
703 "#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]\n",
704 );
705 code.push_str(&format!("pub enum {} {{\n", enum_def.name()));
706
707 for variant in &enum_def.variants {
708 let raw_name = variant.name();
709 let pascal_name = to_pascal_case(raw_name);
710
711 if let Some(attr) = variant.attributes.iter().find(|a| a.is("map"))
713 && let Some(value) = attr.first_arg().and_then(|v| v.as_string())
714 {
715 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", value));
716 code.push_str(&format!(" {},\n", pascal_name));
717 continue;
718 }
719
720 if raw_name != pascal_name {
722 code.push_str(&format!(" #[serde(rename = \"{}\")]\n", raw_name));
723 }
724 code.push_str(&format!(" {},\n", pascal_name));
725 }
726
727 code.push_str("}\n\n");
728
729 code.push_str(&format!(
731 "impl std::fmt::Display for {} {{\n",
732 enum_def.name()
733 ));
734 code.push_str(" fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n");
735 code.push_str(" match self {\n");
736 for variant in &enum_def.variants {
737 let raw_name = variant.name();
738 let pascal_name = to_pascal_case(raw_name);
739 let db_value = variant.db_value();
740 code.push_str(&format!(
741 " Self::{} => write!(f, \"{}\"),\n",
742 pascal_name, db_value
743 ));
744 }
745 code.push_str(" }\n");
746 code.push_str(" }\n");
747 code.push_str("}\n\n");
748
749 if let Some(default_variant) = enum_def.variants.first() {
751 let pascal_name = to_pascal_case(default_variant.name());
752 code.push_str(&format!("impl Default for {} {{\n", enum_def.name()));
753 code.push_str(&format!(
754 " fn default() -> Self {{\n Self::{}\n }}\n",
755 pascal_name
756 ));
757 code.push_str("}\n\n");
758 }
759
760 code.push_str(&format!(
764 "impl std::str::FromStr for {} {{\n",
765 enum_def.name()
766 ));
767 code.push_str(" type Err = prax_query::row::RowError;\n");
768 code.push_str(" fn from_str(s: &str) -> Result<Self, Self::Err> {\n");
769 code.push_str(" match s {\n");
770 for variant in &enum_def.variants {
771 let raw_name = variant.name();
772 let pascal_name = to_pascal_case(raw_name);
773 let db_value = variant.db_value();
774 code.push_str(&format!(
775 " \"{}\" => Ok(Self::{}),\n",
776 db_value, pascal_name
777 ));
778 }
779 code.push_str(&format!(
780 " _ => Err(prax_query::row::RowError::TypeConversion {{\n column: String::new(),\n message: format!(\"unknown {} variant: {{}}\", s),\n }}),\n",
781 enum_def.name()
782 ));
783 code.push_str(" }\n");
784 code.push_str(" }\n");
785 code.push_str("}\n\n");
786
787 code.push_str(&format!(
791 "impl prax_query::row::FromColumn for {} {{\n",
792 enum_def.name()
793 ));
794 code.push_str(
795 " fn from_column(row: &impl prax_query::row::RowRef, column: &str)\n -> Result<Self, prax_query::row::RowError>\n {\n",
796 );
797 code.push_str(" let raw = row.get_string(column)?;\n");
798 code.push_str(" <Self as std::str::FromStr>::from_str(&raw).map_err(|e| {\n");
799 code.push_str(" let msg = match &e {\n");
800 code.push_str(
801 " prax_query::row::RowError::TypeConversion { message, .. } => message.clone(),\n",
802 );
803 code.push_str(" other => other.to_string(),\n");
804 code.push_str(" };\n");
805 code.push_str(" prax_query::row::RowError::TypeConversion {\n");
806 code.push_str(" column: column.to_string(),\n");
807 code.push_str(" message: msg,\n");
808 code.push_str(" }\n");
809 code.push_str(" })\n");
810 code.push_str(" }\n");
811 code.push_str("}\n\n");
812
813 code.push_str(&format!(
822 "impl prax_query::filter::ToFilterValue for {} {{\n",
823 enum_def.name()
824 ));
825 code.push_str(" fn to_filter_value(&self) -> prax_query::filter::FilterValue {\n");
826 code.push_str(" prax_query::filter::FilterValue::String(self.to_string())\n");
827 code.push_str(" }\n");
828 code.push_str("}\n");
829
830 Ok(code)
831}
832
833fn generate_types_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
835 let mut code = String::new();
836
837 code.push_str("//! Common type definitions\n\n");
838 code.push_str("#[allow(unused_imports)]\npub use chrono::{DateTime, Utc};\n");
839 code.push_str("#[allow(unused_imports)]\npub use uuid::Uuid;\n");
840 code.push_str("#[allow(unused_imports)]\npub use serde_json::Value as Json;\n");
841 code.push('\n');
842
843 for composite in schema.types.values() {
845 code.push_str("#[allow(dead_code)]\n");
846 code.push_str("#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\n");
847 code.push_str(&format!("pub struct {} {{\n", composite.name()));
848 for field in composite.fields.values() {
849 let rust_type = field_type_to_rust(&field.field_type, field.modifier);
850 let field_name = to_field_ident(field.name());
851 code.push_str(&format!(" pub {}: {},\n", field_name, rust_type));
852 }
853 code.push_str("}\n\n");
854 }
855
856 Ok(code)
857}
858
859fn generate_filters_module(schema: &prax_schema::ast::Schema) -> CliResult<String> {
861 let mut code = String::new();
862
863 code.push_str("//! Filter types for queries\n\n");
864 code.push_str("#[allow(unused_imports)]\n");
865 code.push_str("use prax_query::filter::{Filter, ScalarFilter};\n");
866
867 let mut referenced_enums = HashSet::new();
869 for model in schema.models.values() {
870 for field in model.fields.values() {
871 if !field.is_relation()
872 && let prax_schema::ast::FieldType::Enum(ref name) = field.field_type
873 {
874 referenced_enums.insert(name.to_string());
875 }
876 }
877 }
878
879 for enum_name in &referenced_enums {
881 code.push_str(&format!(
882 "#[allow(unused_imports)]\nuse super::{}::{};\n",
883 to_snake_case(enum_name),
884 enum_name
885 ));
886 }
887
888 code.push('\n');
889
890 for model in schema.models.values() {
891 code.push_str("#[allow(dead_code)]\n");
893 code.push_str(&format!("/// Filter input for {} queries\n", model.name()));
894 code.push_str("#[derive(Debug, Default, Clone)]\n");
895 code.push_str(&format!("pub struct {}WhereInput {{\n", model.name()));
896
897 for field in model.fields.values() {
898 if !field.is_relation() {
899 let filter_type = field_to_filter_type(&field.field_type);
900 let field_name = to_field_ident(field.name());
901 code.push_str(&format!(
902 " pub {}: Option<{}>,\n",
903 field_name, filter_type
904 ));
905 }
906 }
907
908 code.push_str(" pub and: Option<Vec<Self>>,\n");
909 code.push_str(" pub or: Option<Vec<Self>>,\n");
910 code.push_str(" pub not: Option<Box<Self>>,\n");
911 code.push_str("}\n\n");
912
913 code.push_str("#[allow(dead_code)]\n");
915 code.push_str(&format!(
916 "/// Order by input for {} queries\n",
917 model.name()
918 ));
919 code.push_str("#[derive(Debug, Default, Clone)]\n");
920 code.push_str(&format!("pub struct {}OrderByInput {{\n", model.name()));
921
922 for field in model.fields.values() {
923 if !field.is_relation() {
924 let field_name = to_field_ident(field.name());
925 code.push_str(&format!(
926 " pub {}: Option<prax_query::SortOrder>,\n",
927 field_name
928 ));
929 }
930 }
931
932 code.push_str("}\n\n");
933 }
934
935 Ok(code)
936}
937
938fn field_type_to_rust(
940 field_type: &prax_schema::ast::FieldType,
941 modifier: prax_schema::ast::TypeModifier,
942) -> String {
943 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
944
945 let base_type = match field_type {
946 FieldType::Scalar(scalar) => match scalar {
947 ScalarType::Int => "i32".to_string(),
948 ScalarType::BigInt => "i64".to_string(),
949 ScalarType::Float => "f64".to_string(),
950 ScalarType::String => "String".to_string(),
951 ScalarType::Boolean => "bool".to_string(),
952 ScalarType::DateTime => "chrono::DateTime<chrono::Utc>".to_string(),
953 ScalarType::Date => "chrono::NaiveDate".to_string(),
954 ScalarType::Time => "chrono::NaiveTime".to_string(),
955 ScalarType::Json => "serde_json::Value".to_string(),
956 ScalarType::Bytes => "Vec<u8>".to_string(),
957 ScalarType::Decimal => "rust_decimal::Decimal".to_string(),
958 ScalarType::Uuid => "uuid::Uuid".to_string(),
959 ScalarType::Cuid => "String".to_string(),
960 ScalarType::Cuid2 => "String".to_string(),
961 ScalarType::NanoId => "String".to_string(),
962 ScalarType::Ulid => "String".to_string(),
963 ScalarType::Vector(_) | ScalarType::HalfVector(_) => "Vec<f32>".to_string(),
964 ScalarType::SparseVector(_) => "Vec<(u32, f32)>".to_string(),
965 ScalarType::Bit(_) => "Vec<u8>".to_string(),
966 },
967 FieldType::Model(name) => name.to_string(),
968 FieldType::Enum(name) => name.to_string(),
969 FieldType::Composite(name) => name.to_string(),
970 FieldType::Unsupported(_) => "serde_json::Value".to_string(),
971 };
972
973 match modifier {
974 TypeModifier::Optional | TypeModifier::OptionalList => format!("Option<{}>", base_type),
975 TypeModifier::List => format!("Vec<{}>", base_type),
976 TypeModifier::Required => base_type,
977 }
978}
979
980fn field_type_to_rust_with_boxing(
982 field_type: &prax_schema::ast::FieldType,
983 modifier: prax_schema::ast::TypeModifier,
984 source_model: &str,
985 relation_graph: &HashMap<String, HashSet<String>>,
986) -> String {
987 use prax_schema::ast::{FieldType, TypeModifier};
988
989 if let FieldType::Model(target) = field_type
998 && !matches!(modifier, TypeModifier::List)
999 {
1000 let should_box = needs_boxing(source_model, target, relation_graph);
1001 let base = target.to_string();
1002 return if should_box {
1003 format!("Option<Box<{}>>", base)
1004 } else {
1005 format!("Option<{}>", base)
1006 };
1007 }
1008
1009 field_type_to_rust(field_type, modifier)
1011}
1012
1013fn field_to_filter_type(field_type: &prax_schema::ast::FieldType) -> String {
1015 use prax_schema::ast::{FieldType, ScalarType};
1016
1017 match field_type {
1018 FieldType::Scalar(scalar) => match scalar {
1019 ScalarType::Int | ScalarType::BigInt => "ScalarFilter<i64>".to_string(),
1020 ScalarType::Float | ScalarType::Decimal => "ScalarFilter<f64>".to_string(),
1021 ScalarType::String
1022 | ScalarType::Uuid
1023 | ScalarType::Cuid
1024 | ScalarType::Cuid2
1025 | ScalarType::NanoId
1026 | ScalarType::Ulid => "ScalarFilter<String>".to_string(),
1027 ScalarType::Boolean => "ScalarFilter<bool>".to_string(),
1028 ScalarType::DateTime => "ScalarFilter<chrono::DateTime<chrono::Utc>>".to_string(),
1029 ScalarType::Date => "ScalarFilter<chrono::NaiveDate>".to_string(),
1030 ScalarType::Time => "ScalarFilter<chrono::NaiveTime>".to_string(),
1031 ScalarType::Json => "ScalarFilter<serde_json::Value>".to_string(),
1032 ScalarType::Bytes => "ScalarFilter<Vec<u8>>".to_string(),
1033 ScalarType::Vector(_) | ScalarType::HalfVector(_) => {
1040 "prax_pgvector::filter::VectorFilter".to_string()
1041 }
1042 ScalarType::SparseVector(_) => "ScalarFilter<Vec<(u32, f32)>>".to_string(),
1043 ScalarType::Bit(_) => "ScalarFilter<Vec<u8>>".to_string(),
1044 },
1045 FieldType::Enum(name) => format!("ScalarFilter<{}>", name),
1046 _ => "Filter".to_string(),
1047 }
1048}
1049
1050fn to_snake_case(name: &str) -> String {
1052 let mut result = String::new();
1053 for (i, c) in name.chars().enumerate() {
1054 if c.is_uppercase() {
1055 if i > 0 {
1056 result.push('_');
1057 }
1058 result.push(c.to_lowercase().next().unwrap());
1059 } else {
1060 result.push(c);
1061 }
1062 }
1063 result
1064}
1065
1066fn to_field_ident(name: &str) -> String {
1081 let snake = to_snake_case(name);
1082 if is_rust_keyword(&snake) {
1083 format!("r#{}", snake)
1084 } else {
1085 snake
1086 }
1087}
1088
1089fn is_rust_keyword(s: &str) -> bool {
1090 matches!(
1091 s,
1092 "abstract"
1093 | "as"
1094 | "async"
1095 | "await"
1096 | "become"
1097 | "box"
1098 | "break"
1099 | "const"
1100 | "continue"
1101 | "do"
1102 | "dyn"
1103 | "else"
1104 | "enum"
1105 | "extern"
1106 | "false"
1107 | "final"
1108 | "fn"
1109 | "for"
1110 | "gen"
1111 | "if"
1112 | "impl"
1113 | "in"
1114 | "let"
1115 | "loop"
1116 | "macro"
1117 | "match"
1118 | "mod"
1119 | "move"
1120 | "mut"
1121 | "override"
1122 | "priv"
1123 | "pub"
1124 | "ref"
1125 | "return"
1126 | "static"
1127 | "struct"
1128 | "trait"
1129 | "true"
1130 | "try"
1131 | "type"
1132 | "typeof"
1133 | "unsafe"
1134 | "unsized"
1135 | "use"
1136 | "virtual"
1137 | "where"
1138 | "while"
1139 | "yield"
1140 )
1141}
1142
1143fn to_pascal_case(name: &str) -> String {
1145 if name.is_empty() {
1146 return String::new();
1147 }
1148
1149 let first = name.chars().next().unwrap();
1151 if first.is_uppercase() && name.chars().any(|c| c.is_lowercase()) && !name.contains('_') {
1152 return name.to_string();
1153 }
1154
1155 name.split('_')
1157 .filter(|s| !s.is_empty())
1158 .map(|segment| {
1159 let mut chars = segment.chars();
1160 match chars.next() {
1161 None => String::new(),
1162 Some(first) => {
1163 let rest: String = chars.collect();
1164 format!("{}{}", first.to_uppercase(), rest.to_lowercase())
1165 }
1166 }
1167 })
1168 .collect()
1169}
1170
1171#[cfg(test)]
1172mod tests {
1173 use super::*;
1174
1175 #[test]
1176 fn test_to_snake_case() {
1177 assert_eq!(to_snake_case("BoardMember"), "board_member");
1178 assert_eq!(to_snake_case("User"), "user");
1179 assert_eq!(to_snake_case("JiraImportConfig"), "jira_import_config");
1180 }
1181
1182 #[test]
1183 fn test_to_pascal_case_from_snake() {
1184 assert_eq!(to_pascal_case("card_created"), "CardCreated");
1185 assert_eq!(to_pascal_case("branch_deleted"), "BranchDeleted");
1186 assert_eq!(to_pascal_case("pr_merged"), "PrMerged");
1187 }
1188
1189 #[test]
1190 fn test_to_pascal_case_from_screaming() {
1191 assert_eq!(to_pascal_case("CARD_CREATED"), "CardCreated");
1192 assert_eq!(to_pascal_case("PR_MERGED"), "PrMerged");
1193 }
1194
1195 #[test]
1196 fn test_to_pascal_case_already_pascal() {
1197 assert_eq!(to_pascal_case("Admin"), "Admin");
1198 assert_eq!(to_pascal_case("SuperAdmin"), "SuperAdmin");
1199 assert_eq!(to_pascal_case("Low"), "Low");
1200 }
1201
1202 #[test]
1203 fn test_to_pascal_case_single_word() {
1204 assert_eq!(to_pascal_case("active"), "Active");
1205 assert_eq!(to_pascal_case("ACTIVE"), "Active");
1206 }
1207
1208 #[test]
1209 fn test_needs_boxing_direct_cycle() {
1210 let mut graph = HashMap::new();
1211 graph.insert(
1212 "Board".to_string(),
1213 HashSet::from(["JiraConfig".to_string()]),
1214 );
1215 graph.insert(
1216 "JiraConfig".to_string(),
1217 HashSet::from(["Board".to_string()]),
1218 );
1219
1220 assert!(needs_boxing("Board", "JiraConfig", &graph));
1221 assert!(needs_boxing("JiraConfig", "Board", &graph));
1222 }
1223
1224 #[test]
1225 fn test_needs_boxing_no_cycle() {
1226 let mut graph = HashMap::new();
1227 graph.insert("Post".to_string(), HashSet::from(["User".to_string()]));
1228 graph.insert("User".to_string(), HashSet::new());
1229
1230 assert!(!needs_boxing("Post", "User", &graph));
1231 }
1232
1233 #[test]
1234 fn test_needs_boxing_indirect_cycle() {
1235 let mut graph = HashMap::new();
1236 graph.insert("A".to_string(), HashSet::from(["B".to_string()]));
1237 graph.insert("B".to_string(), HashSet::from(["C".to_string()]));
1238 graph.insert("C".to_string(), HashSet::from(["A".to_string()]));
1239
1240 assert!(needs_boxing("A", "B", &graph));
1241 assert!(needs_boxing("B", "C", &graph));
1242 assert!(needs_boxing("C", "A", &graph));
1243 }
1244}