1use std::path::Path;
7
8use crate::cli::FormatArgs;
9use crate::config::SCHEMA_FILE_PATH;
10use crate::error::{CliError, CliResult};
11use crate::output::{self, success};
12
13pub async fn run(args: FormatArgs) -> CliResult<()> {
15 output::header("Format Schema");
16
17 let cwd = std::env::current_dir()?;
18 let schema_path = args.schema.unwrap_or_else(|| cwd.join(SCHEMA_FILE_PATH));
19
20 if !schema_path.exists() {
21 return Err(CliError::Config(format!(
22 "Schema path not found: {}",
23 schema_path.display()
24 )));
25 }
26
27 output::kv("Schema", &schema_path.display().to_string());
28 output::newline();
29
30 let files: Vec<std::path::PathBuf> = if schema_path.is_dir() {
31 let discovered = prax_schema::loader::discover(&schema_path).map_err(CliError::from)?;
32 if discovered.is_empty() {
33 return Err(CliError::Config(format!(
34 "No .prax files found under {}",
35 schema_path.display()
36 )));
37 }
38 discovered.into_iter().map(|d| d.absolute).collect()
39 } else {
40 vec![schema_path.clone()]
41 };
42
43 let mut any_changed = false;
44 let mut any_needs_format = false;
45 for file in &files {
46 match format_one(file, args.check)? {
47 FormatOutcome::Unchanged => {}
48 FormatOutcome::Reformatted => any_changed = true,
49 FormatOutcome::NeedsFormatting => any_needs_format = true,
50 }
51 }
52
53 output::newline();
54 if args.check {
55 if any_needs_format {
56 output::error("Some schema files are not formatted correctly.");
57 output::info("Run `prax format` to fix formatting.");
58 return Err(CliError::Format(
59 "One or more schema files need formatting".to_string(),
60 ));
61 }
62 success(&format!(
63 "All {} schema file(s) are formatted!",
64 files.len()
65 ));
66 } else if any_changed {
67 success(&format!("Formatted {} schema file(s).", files.len()));
68 } else {
69 success(&format!(
70 "All {} schema file(s) are already formatted!",
71 files.len()
72 ));
73 }
74
75 Ok(())
76}
77
78enum FormatOutcome {
79 Unchanged,
80 Reformatted,
81 NeedsFormatting,
82}
83
84fn format_one(path: &Path, check: bool) -> CliResult<FormatOutcome> {
85 let content = std::fs::read_to_string(path)?;
86 let schema = parse_schema(&content)?;
87 let formatted = format_schema(&schema);
88 let changed = formatted != content;
89
90 if check {
91 return Ok(if changed {
92 output::error(&format!("Needs formatting: {}", path.display()));
93 FormatOutcome::NeedsFormatting
94 } else {
95 FormatOutcome::Unchanged
96 });
97 }
98
99 if changed {
100 std::fs::write(path, &formatted)?;
101 output::list_item(&format!("Formatted {}", path.display()));
102 Ok(FormatOutcome::Reformatted)
103 } else {
104 Ok(FormatOutcome::Unchanged)
105 }
106}
107
108fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
109 prax_schema::validate_schema(content)
112 .map_err(|e| CliError::Schema(format!("Syntax error: {}", e)))
113}
114
115fn format_schema(schema: &prax_schema::ast::Schema) -> String {
117 let mut output = String::new();
118
119 output.push_str("datasource db {\n");
122 output.push_str(" provider = \"postgresql\"\n");
123 output.push_str(" url = env(\"DATABASE_URL\")\n");
124 output.push_str("}\n");
125 let mut first_section = false;
126
127 if !first_section {
129 output.push('\n');
130 }
131 output.push_str("generator client {\n");
132 output.push_str(" provider = \"prax-client-rust\"\n");
133 output.push_str(" output = \"./src/generated\"\n");
134 output.push_str("}\n");
135 first_section = false;
136
137 for enum_def in schema.enums.values() {
139 if !first_section {
140 output.push('\n');
141 }
142 format_enum(&mut output, enum_def);
143 first_section = false;
144 }
145
146 for model in schema.models.values() {
148 if !first_section {
149 output.push('\n');
150 }
151 format_model(&mut output, model);
152 first_section = false;
153 }
154
155 for view in schema.views.values() {
157 if !first_section {
158 output.push('\n');
159 }
160 format_view(&mut output, view);
161 first_section = false;
162 }
163
164 for composite in schema.types.values() {
166 if !first_section {
167 output.push('\n');
168 }
169 format_composite(&mut output, composite);
170 first_section = false;
171 }
172
173 output
174}
175
176fn format_enum(output: &mut String, enum_def: &prax_schema::ast::Enum) {
177 if let Some(doc) = &enum_def.documentation {
179 for line in doc.text.lines() {
180 output.push_str(&format!("/// {}\n", line));
181 }
182 }
183
184 output.push_str(&format!("enum {} {{\n", enum_def.name()));
185
186 for variant in &enum_def.variants {
187 if let Some(doc) = &variant.documentation {
189 for line in doc.text.lines() {
190 output.push_str(&format!(" /// {}\n", line));
191 }
192 }
193
194 output.push_str(&format!(" {}", variant.name()));
195
196 for attr in &variant.attributes {
198 output.push_str(&format!(" {}", format_attribute(attr)));
199 }
200
201 output.push('\n');
202 }
203
204 for attr in &enum_def.attributes {
206 output.push_str(&format!("\n {}", format_attribute(attr)));
207 }
208
209 output.push_str("}\n");
210}
211
212fn format_model(output: &mut String, model: &prax_schema::ast::Model) {
213 if let Some(doc) = &model.documentation {
215 for line in doc.text.lines() {
216 output.push_str(&format!("/// {}\n", line));
217 }
218 }
219
220 output.push_str(&format!("model {} {{\n", model.name()));
221
222 let max_name_len = model
224 .fields
225 .values()
226 .map(|f| f.name().len())
227 .max()
228 .unwrap_or(0);
229
230 let max_type_len = model
231 .fields
232 .values()
233 .map(|f| format_field_type(&f.field_type, f.modifier).len())
234 .max()
235 .unwrap_or(0);
236
237 for field in model.fields.values() {
238 if let Some(doc) = &field.documentation {
240 for line in doc.text.lines() {
241 output.push_str(&format!(" /// {}\n", line));
242 }
243 }
244
245 let type_str = format_field_type(&field.field_type, field.modifier);
246
247 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
249 let padded_type = format!("{:width$}", type_str, width = max_type_len);
250
251 output.push_str(&format!(" {} {}", padded_name, padded_type));
252
253 for attr in &field.attributes {
255 output.push_str(&format!(" {}", format_attribute(attr)));
256 }
257
258 output.push('\n');
259 }
260
261 let model_attrs: Vec<_> = model.attributes.iter().collect();
263 if !model_attrs.is_empty() {
264 output.push('\n');
265 for attr in model_attrs {
266 output.push_str(&format!(" {}\n", format_attribute(attr)));
267 }
268 }
269
270 output.push_str("}\n");
271}
272
273fn format_view(output: &mut String, view: &prax_schema::ast::View) {
274 if let Some(doc) = &view.documentation {
276 for line in doc.text.lines() {
277 output.push_str(&format!("/// {}\n", line));
278 }
279 }
280
281 output.push_str(&format!("view {} {{\n", view.name()));
282
283 let max_name_len = view
285 .fields
286 .values()
287 .map(|f| f.name().len())
288 .max()
289 .unwrap_or(0);
290
291 let max_type_len = view
292 .fields
293 .values()
294 .map(|f| format_field_type(&f.field_type, f.modifier).len())
295 .max()
296 .unwrap_or(0);
297
298 for field in view.fields.values() {
299 let type_str = format_field_type(&field.field_type, field.modifier);
300 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
301 let padded_type = format!("{:width$}", type_str, width = max_type_len);
302
303 output.push_str(&format!(" {} {}", padded_name, padded_type));
304
305 for attr in &field.attributes {
306 output.push_str(&format!(" {}", format_attribute(attr)));
307 }
308
309 output.push('\n');
310 }
311
312 let view_attrs: Vec<_> = view.attributes.iter().collect();
314 if !view_attrs.is_empty() {
315 output.push('\n');
316 for attr in view_attrs {
317 output.push_str(&format!(" {}\n", format_attribute(attr)));
318 }
319 }
320
321 output.push_str("}\n");
322}
323
324fn format_composite(output: &mut String, composite: &prax_schema::ast::CompositeType) {
325 if let Some(doc) = &composite.documentation {
327 for line in doc.text.lines() {
328 output.push_str(&format!("/// {}\n", line));
329 }
330 }
331
332 output.push_str(&format!("type {} {{\n", composite.name()));
333
334 let max_name_len = composite
336 .fields
337 .values()
338 .map(|f| f.name().len())
339 .max()
340 .unwrap_or(0);
341
342 let max_type_len = composite
343 .fields
344 .values()
345 .map(|f| format_field_type(&f.field_type, f.modifier).len())
346 .max()
347 .unwrap_or(0);
348
349 for field in composite.fields.values() {
350 let type_str = format_field_type(&field.field_type, field.modifier);
351 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
352 let padded_type = format!("{:width$}", type_str, width = max_type_len);
353
354 output.push_str(&format!(" {} {}", padded_name, padded_type));
355
356 for attr in &field.attributes {
357 output.push_str(&format!(" {}", format_attribute(attr)));
358 }
359
360 output.push('\n');
361 }
362
363 output.push_str("}\n");
364}
365
366fn format_field_type(
367 field_type: &prax_schema::ast::FieldType,
368 modifier: prax_schema::ast::TypeModifier,
369) -> String {
370 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
371
372 let base = match field_type {
373 FieldType::Scalar(scalar) => match scalar {
374 ScalarType::Int => "Int",
375 ScalarType::BigInt => "BigInt",
376 ScalarType::Float => "Float",
377 ScalarType::String => "String",
378 ScalarType::Boolean => "Boolean",
379 ScalarType::DateTime => "DateTime",
380 ScalarType::Date => "Date",
381 ScalarType::Time => "Time",
382 ScalarType::Json => "Json",
383 ScalarType::Bytes => "Bytes",
384 ScalarType::Decimal => "Decimal",
385 ScalarType::Uuid => "Uuid",
386 ScalarType::Cuid => "Cuid",
387 ScalarType::Cuid2 => "Cuid2",
388 ScalarType::NanoId => "NanoId",
389 ScalarType::Ulid => "Ulid",
390 ScalarType::Vector(_) => "Vector",
391 ScalarType::HalfVector(_) => "HalfVector",
392 ScalarType::SparseVector(_) => "SparseVector",
393 ScalarType::Bit(_) => "Bit",
394 }
395 .to_string(),
396 FieldType::Model(name) => name.to_string(),
397 FieldType::Enum(name) => name.to_string(),
398 FieldType::Composite(name) => name.to_string(),
399 FieldType::Unsupported(name) => format!("Unsupported(\"{}\")", name),
400 };
401
402 match modifier {
403 TypeModifier::Optional => format!("{}?", base),
404 TypeModifier::List => format!("{}[]", base),
405 TypeModifier::OptionalList => format!("{}[]?", base),
406 TypeModifier::Required => base,
407 }
408}
409
410fn format_attribute(attr: &prax_schema::ast::Attribute) -> String {
411 let prefix = if attr.is_model_attribute() { "@@" } else { "@" };
413
414 if attr.args.is_empty() {
415 format!("{}{}", prefix, attr.name())
416 } else {
417 let args: Vec<String> = attr
418 .args
419 .iter()
420 .map(|arg| {
421 if let Some(name) = &arg.name {
422 format!("{}: {}", name.as_str(), format_attribute_value(&arg.value))
423 } else {
424 format_attribute_value(&arg.value)
425 }
426 })
427 .collect();
428
429 format!("{}{}({})", prefix, attr.name(), args.join(", "))
430 }
431}
432
433fn format_attribute_value(value: &prax_schema::ast::AttributeValue) -> String {
434 use prax_schema::ast::AttributeValue;
435
436 match value {
437 AttributeValue::String(s) => format!("\"{}\"", s),
438 AttributeValue::Int(i) => i.to_string(),
439 AttributeValue::Float(f) => f.to_string(),
440 AttributeValue::Boolean(b) => b.to_string(),
441 AttributeValue::Ident(id) => id.to_string(),
442 AttributeValue::Function(name, args) => {
443 if args.is_empty() {
444 format!("{}()", name)
445 } else {
446 let arg_strs: Vec<String> = args.iter().map(format_attribute_value).collect();
447 format!("{}({})", name, arg_strs.join(", "))
448 }
449 }
450 AttributeValue::Array(items) => {
451 let item_strs: Vec<String> = items.iter().map(format_attribute_value).collect();
452 format!("[{}]", item_strs.join(", "))
453 }
454 AttributeValue::FieldRef(field) => field.to_string(),
455 AttributeValue::FieldRefList(fields) => {
456 format!(
457 "[{}]",
458 fields
459 .iter()
460 .map(|f| f.to_string())
461 .collect::<Vec<_>>()
462 .join(", ")
463 )
464 }
465 }
466}