1use std::path::Path;
11
12use crate::cli::FormatArgs;
13use crate::config::SCHEMA_FILE_PATH;
14use crate::error::{CliError, CliResult};
15use crate::output::{self, success};
16
17pub async fn run(args: FormatArgs) -> CliResult<()> {
19 output::header("Format Schema");
20
21 let cwd = std::env::current_dir()?;
22 let schema_path = args.schema.unwrap_or_else(|| cwd.join(SCHEMA_FILE_PATH));
23
24 if !schema_path.exists() {
25 return Err(CliError::Config(format!(
26 "Schema path not found: {}",
27 schema_path.display()
28 )));
29 }
30
31 output::kv("Schema", &schema_path.display().to_string());
32 output::newline();
33
34 let files: Vec<std::path::PathBuf> = if schema_path.is_dir() {
35 let discovered = prax_schema::loader::discover(&schema_path).map_err(CliError::from)?;
36 if discovered.is_empty() {
37 return Err(CliError::Config(format!(
38 "No .prax files found under {}",
39 schema_path.display()
40 )));
41 }
42 discovered.into_iter().map(|d| d.absolute).collect()
43 } else {
44 vec![schema_path.clone()]
45 };
46
47 let mut any_changed = false;
48 let mut any_needs_format = false;
49 for file in &files {
50 match format_one(file, args.check)? {
51 FormatOutcome::Unchanged => {}
52 FormatOutcome::Reformatted => any_changed = true,
53 FormatOutcome::NeedsFormatting => any_needs_format = true,
54 }
55 }
56
57 output::newline();
58 if args.check {
59 if any_needs_format {
60 output::error("Some schema files are not formatted correctly.");
61 output::info("Run `prax format` to fix formatting.");
62 return Err(CliError::Format(
63 "One or more schema files need formatting".to_string(),
64 ));
65 }
66 success(&format!(
67 "All {} schema file(s) are formatted!",
68 files.len()
69 ));
70 } else if any_changed {
71 success(&format!("Formatted {} schema file(s).", files.len()));
72 } else {
73 success(&format!(
74 "All {} schema file(s) are already formatted!",
75 files.len()
76 ));
77 }
78
79 Ok(())
80}
81
82enum FormatOutcome {
83 Unchanged,
84 Reformatted,
85 NeedsFormatting,
86}
87
88fn format_one(path: &Path, check: bool) -> CliResult<FormatOutcome> {
89 let content = std::fs::read_to_string(path)?;
90 let schema = parse_schema(&content)?;
91 let formatted = format_schema(&schema);
92 let changed = formatted != content;
93
94 if check {
95 return Ok(if changed {
96 output::error(&format!("Needs formatting: {}", path.display()));
97 FormatOutcome::NeedsFormatting
98 } else {
99 FormatOutcome::Unchanged
100 });
101 }
102
103 if changed {
104 std::fs::write(path, &formatted)?;
105 output::list_item(&format!("Formatted {}", path.display()));
106 Ok(FormatOutcome::Reformatted)
107 } else {
108 Ok(FormatOutcome::Unchanged)
109 }
110}
111
112fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
113 prax_schema::validate_schema(content)
116 .map_err(|e| CliError::Schema(format!("Syntax error: {}", e)))
117}
118
119fn format_schema(schema: &prax_schema::ast::Schema) -> String {
121 let mut output = String::new();
122 let mut wrote_section = false;
123
124 if let Some(datasource) = &schema.datasource {
127 format_datasource(&mut output, datasource);
128 wrote_section = true;
129 }
130
131 for generator in schema.generators.values() {
133 if wrote_section {
134 output.push('\n');
135 }
136 format_generator(&mut output, generator);
137 wrote_section = true;
138 }
139
140 for enum_def in schema.enums.values() {
142 if wrote_section {
143 output.push('\n');
144 }
145 format_enum(&mut output, enum_def);
146 wrote_section = true;
147 }
148
149 for model in schema.models.values() {
151 if wrote_section {
152 output.push('\n');
153 }
154 format_model(&mut output, model);
155 wrote_section = true;
156 }
157
158 for view in schema.views.values() {
160 if wrote_section {
161 output.push('\n');
162 }
163 format_view(&mut output, view);
164 wrote_section = true;
165 }
166
167 for composite in schema.types.values() {
169 if wrote_section {
170 output.push('\n');
171 }
172 format_composite(&mut output, composite);
173 wrote_section = true;
174 }
175
176 output
177}
178
179fn format_datasource(output: &mut String, datasource: &prax_schema::ast::Datasource) {
180 output.push_str(&format!("datasource {} {{\n", datasource.name));
181 output.push_str(&format!(
182 " provider = \"{}\"\n",
183 datasource.provider.as_str()
184 ));
185
186 if let Some(url_env) = &datasource.url_env {
187 output.push_str(&format!(" url = env(\"{}\")\n", url_env));
188 } else if let Some(url) = &datasource.url {
189 output.push_str(&format!(" url = \"{}\"\n", url));
190 }
191
192 if !datasource.extensions.is_empty() {
193 let extensions: Vec<String> = datasource
194 .extensions
195 .iter()
196 .map(|ext| {
197 let mut args = Vec::new();
198 if let Some(schema) = &ext.schema {
199 args.push(format!("schema: \"{}\"", schema));
200 }
201 if let Some(version) = &ext.version {
202 args.push(format!("version: \"{}\"", version));
203 }
204 if args.is_empty() {
205 ext.name().to_string()
206 } else {
207 format!("{}({})", ext.name(), args.join(", "))
208 }
209 })
210 .collect();
211 output.push_str(&format!(" extensions = [{}]\n", extensions.join(", ")));
212 }
213
214 for (key, value) in &datasource.properties {
215 if value.starts_with("env(") {
218 output.push_str(&format!(" {} = {}\n", key, value));
219 } else {
220 output.push_str(&format!(" {} = \"{}\"\n", key, value));
221 }
222 }
223
224 output.push_str("}\n");
225}
226
227fn format_generator(output: &mut String, generator: &prax_schema::ast::Generator) {
228 use prax_schema::ast::{GeneratorToggle, GeneratorValue};
229
230 output.push_str(&format!("generator {} {{\n", generator.name()));
231
232 if let Some(provider) = &generator.provider {
233 output.push_str(&format!(" provider = \"{}\"\n", provider));
234 }
235
236 if let Some(out) = &generator.output {
237 output.push_str(&format!(" output = \"{}\"\n", out));
238 }
239
240 match &generator.generate {
241 GeneratorToggle::Always => {}
242 GeneratorToggle::Never | GeneratorToggle::Literal(false) => {
243 output.push_str(" generate = false\n");
244 }
245 GeneratorToggle::Literal(true) => {
246 output.push_str(" generate = true\n");
247 }
248 GeneratorToggle::Env(var) => {
249 output.push_str(&format!(" generate = env(\"{}\")\n", var));
250 }
251 }
252
253 for (key, value) in &generator.properties {
254 let formatted = match value {
255 GeneratorValue::String(s) => format!("\"{}\"", s),
256 GeneratorValue::Bool(b) => b.to_string(),
257 GeneratorValue::Env(var) => format!("env(\"{}\")", var),
258 GeneratorValue::Ident(s) => s.to_string(),
259 };
260 output.push_str(&format!(" {} = {}\n", key, formatted));
261 }
262
263 output.push_str("}\n");
264}
265
266fn format_enum(output: &mut String, enum_def: &prax_schema::ast::Enum) {
267 if let Some(doc) = &enum_def.documentation {
269 for line in doc.text.lines() {
270 output.push_str(&format!("/// {}\n", line));
271 }
272 }
273
274 output.push_str(&format!("enum {} {{\n", enum_def.name()));
275
276 for variant in &enum_def.variants {
277 if let Some(doc) = &variant.documentation {
279 for line in doc.text.lines() {
280 output.push_str(&format!(" /// {}\n", line));
281 }
282 }
283
284 output.push_str(&format!(" {}", variant.name()));
285
286 for attr in &variant.attributes {
288 output.push_str(&format!(" {}", format_attribute(attr)));
289 }
290
291 output.push('\n');
292 }
293
294 for attr in &enum_def.attributes {
296 output.push_str(&format!("\n {}", format_attribute(attr)));
297 }
298
299 output.push_str("}\n");
300}
301
302fn format_model(output: &mut String, model: &prax_schema::ast::Model) {
303 if let Some(doc) = &model.documentation {
305 for line in doc.text.lines() {
306 output.push_str(&format!("/// {}\n", line));
307 }
308 }
309
310 output.push_str(&format!("model {} {{\n", model.name()));
311
312 let max_name_len = model
314 .fields
315 .values()
316 .map(|f| f.name().len())
317 .max()
318 .unwrap_or(0);
319
320 let max_type_len = model
321 .fields
322 .values()
323 .map(|f| format_field_type(&f.field_type, f.modifier).len())
324 .max()
325 .unwrap_or(0);
326
327 for field in model.fields.values() {
328 if let Some(doc) = &field.documentation {
330 for line in doc.text.lines() {
331 output.push_str(&format!(" /// {}\n", line));
332 }
333 }
334
335 let type_str = format_field_type(&field.field_type, field.modifier);
336
337 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
339 let padded_type = format!("{:width$}", type_str, width = max_type_len);
340
341 output.push_str(&format!(" {} {}", padded_name, padded_type));
342
343 for attr in &field.attributes {
345 output.push_str(&format!(" {}", format_attribute(attr)));
346 }
347
348 output.push('\n');
349 }
350
351 let model_attrs: Vec<_> = model.attributes.iter().collect();
353 if !model_attrs.is_empty() {
354 output.push('\n');
355 for attr in model_attrs {
356 output.push_str(&format!(" {}\n", format_attribute(attr)));
357 }
358 }
359
360 output.push_str("}\n");
361}
362
363fn format_view(output: &mut String, view: &prax_schema::ast::View) {
364 if let Some(doc) = &view.documentation {
366 for line in doc.text.lines() {
367 output.push_str(&format!("/// {}\n", line));
368 }
369 }
370
371 output.push_str(&format!("view {} {{\n", view.name()));
372
373 let max_name_len = view
375 .fields
376 .values()
377 .map(|f| f.name().len())
378 .max()
379 .unwrap_or(0);
380
381 let max_type_len = view
382 .fields
383 .values()
384 .map(|f| format_field_type(&f.field_type, f.modifier).len())
385 .max()
386 .unwrap_or(0);
387
388 for field in view.fields.values() {
389 let type_str = format_field_type(&field.field_type, field.modifier);
390 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
391 let padded_type = format!("{:width$}", type_str, width = max_type_len);
392
393 output.push_str(&format!(" {} {}", padded_name, padded_type));
394
395 for attr in &field.attributes {
396 output.push_str(&format!(" {}", format_attribute(attr)));
397 }
398
399 output.push('\n');
400 }
401
402 let view_attrs: Vec<_> = view.attributes.iter().collect();
404 if !view_attrs.is_empty() {
405 output.push('\n');
406 for attr in view_attrs {
407 output.push_str(&format!(" {}\n", format_attribute(attr)));
408 }
409 }
410
411 output.push_str("}\n");
412}
413
414fn format_composite(output: &mut String, composite: &prax_schema::ast::CompositeType) {
415 if let Some(doc) = &composite.documentation {
417 for line in doc.text.lines() {
418 output.push_str(&format!("/// {}\n", line));
419 }
420 }
421
422 output.push_str(&format!("type {} {{\n", composite.name()));
423
424 let max_name_len = composite
426 .fields
427 .values()
428 .map(|f| f.name().len())
429 .max()
430 .unwrap_or(0);
431
432 let max_type_len = composite
433 .fields
434 .values()
435 .map(|f| format_field_type(&f.field_type, f.modifier).len())
436 .max()
437 .unwrap_or(0);
438
439 for field in composite.fields.values() {
440 let type_str = format_field_type(&field.field_type, field.modifier);
441 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
442 let padded_type = format!("{:width$}", type_str, width = max_type_len);
443
444 output.push_str(&format!(" {} {}", padded_name, padded_type));
445
446 for attr in &field.attributes {
447 output.push_str(&format!(" {}", format_attribute(attr)));
448 }
449
450 output.push('\n');
451 }
452
453 output.push_str("}\n");
454}
455
456fn format_field_type(
457 field_type: &prax_schema::ast::FieldType,
458 modifier: prax_schema::ast::TypeModifier,
459) -> String {
460 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
461
462 let base = match field_type {
463 FieldType::Scalar(scalar) => match scalar {
464 ScalarType::Int => "Int",
465 ScalarType::BigInt => "BigInt",
466 ScalarType::Float => "Float",
467 ScalarType::String => "String",
468 ScalarType::Boolean => "Boolean",
469 ScalarType::DateTime => "DateTime",
470 ScalarType::Date => "Date",
471 ScalarType::Time => "Time",
472 ScalarType::Json => "Json",
473 ScalarType::Bytes => "Bytes",
474 ScalarType::Decimal => "Decimal",
475 ScalarType::Uuid => "Uuid",
476 ScalarType::Cuid => "Cuid",
477 ScalarType::Cuid2 => "Cuid2",
478 ScalarType::NanoId => "NanoId",
479 ScalarType::Ulid => "Ulid",
480 ScalarType::Vector(_) => "Vector",
481 ScalarType::HalfVector(_) => "HalfVector",
482 ScalarType::SparseVector(_) => "SparseVector",
483 ScalarType::Bit(_) => "Bit",
484 }
485 .to_string(),
486 FieldType::Model(name) => name.to_string(),
487 FieldType::Enum(name) => name.to_string(),
488 FieldType::Composite(name) => name.to_string(),
489 FieldType::Unsupported(name) => format!("Unsupported(\"{}\")", name),
490 };
491
492 match modifier {
493 TypeModifier::Optional => format!("{}?", base),
494 TypeModifier::List => format!("{}[]", base),
495 TypeModifier::OptionalList => format!("{}[]?", base),
496 TypeModifier::Required => base,
497 }
498}
499
500fn format_attribute(attr: &prax_schema::ast::Attribute) -> String {
501 let prefix = if attr.is_model_attribute() { "@@" } else { "@" };
503
504 if attr.args.is_empty() {
505 format!("{}{}", prefix, attr.name())
506 } else {
507 let args: Vec<String> = attr
508 .args
509 .iter()
510 .map(|arg| {
511 if let Some(name) = &arg.name {
512 format!("{}: {}", name.as_str(), format_attribute_value(&arg.value))
513 } else {
514 format_attribute_value(&arg.value)
515 }
516 })
517 .collect();
518
519 format!("{}{}({})", prefix, attr.name(), args.join(", "))
520 }
521}
522
523fn format_attribute_value(value: &prax_schema::ast::AttributeValue) -> String {
524 use prax_schema::ast::AttributeValue;
525
526 match value {
527 AttributeValue::String(s) => format!("\"{}\"", s),
528 AttributeValue::Int(i) => i.to_string(),
529 AttributeValue::Float(f) => f.to_string(),
530 AttributeValue::Boolean(b) => b.to_string(),
531 AttributeValue::Ident(id) => id.to_string(),
532 AttributeValue::Function(name, args) => {
533 if args.is_empty() {
534 format!("{}()", name)
535 } else {
536 let arg_strs: Vec<String> = args.iter().map(format_attribute_value).collect();
537 format!("{}({})", name, arg_strs.join(", "))
538 }
539 }
540 AttributeValue::Array(items) => {
541 let item_strs: Vec<String> = items.iter().map(format_attribute_value).collect();
542 format!("[{}]", item_strs.join(", "))
543 }
544 AttributeValue::FieldRef(field) => field.to_string(),
545 AttributeValue::FieldRefList(fields) => {
546 format!(
547 "[{}]",
548 fields
549 .iter()
550 .map(|f| f.to_string())
551 .collect::<Vec<_>>()
552 .join(", ")
553 )
554 }
555 }
556}