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::parse_schema(content).map_err(|e| CliError::Schema(format!("Syntax error: {}", e)))
120}
121
122fn format_schema(schema: &prax_schema::ast::Schema) -> String {
124 let mut output = String::new();
125 let mut wrote_section = false;
126
127 if let Some(datasource) = &schema.datasource {
130 format_datasource(&mut output, datasource);
131 wrote_section = true;
132 }
133
134 for generator in schema.generators.values() {
136 if wrote_section {
137 output.push('\n');
138 }
139 format_generator(&mut output, generator);
140 wrote_section = true;
141 }
142
143 for enum_def in schema.enums.values() {
145 if wrote_section {
146 output.push('\n');
147 }
148 format_enum(&mut output, enum_def);
149 wrote_section = true;
150 }
151
152 for model in schema.models.values() {
154 if wrote_section {
155 output.push('\n');
156 }
157 format_model(&mut output, model);
158 wrote_section = true;
159 }
160
161 for view in schema.views.values() {
163 if wrote_section {
164 output.push('\n');
165 }
166 format_view(&mut output, view);
167 wrote_section = true;
168 }
169
170 for composite in schema.types.values() {
172 if wrote_section {
173 output.push('\n');
174 }
175 format_composite(&mut output, composite);
176 wrote_section = true;
177 }
178
179 output
180}
181
182fn format_datasource(output: &mut String, datasource: &prax_schema::ast::Datasource) {
183 output.push_str(&format!("datasource {} {{\n", datasource.name));
184 output.push_str(&format!(
185 " provider = \"{}\"\n",
186 datasource.provider.as_str()
187 ));
188
189 if let Some(url_env) = &datasource.url_env {
190 output.push_str(&format!(" url = env(\"{}\")\n", url_env));
191 } else if let Some(url) = &datasource.url {
192 output.push_str(&format!(" url = \"{}\"\n", url));
193 }
194
195 if !datasource.extensions.is_empty() {
196 let extensions: Vec<String> = datasource
197 .extensions
198 .iter()
199 .map(|ext| {
200 let mut args = Vec::new();
201 if let Some(schema) = &ext.schema {
202 args.push(format!("schema: \"{}\"", schema));
203 }
204 if let Some(version) = &ext.version {
205 args.push(format!("version: \"{}\"", version));
206 }
207 if args.is_empty() {
208 ext.name().to_string()
209 } else {
210 format!("{}({})", ext.name(), args.join(", "))
211 }
212 })
213 .collect();
214 output.push_str(&format!(" extensions = [{}]\n", extensions.join(", ")));
215 }
216
217 for (key, value) in &datasource.properties {
218 if value.starts_with("env(") {
221 output.push_str(&format!(" {} = {}\n", key, value));
222 } else {
223 output.push_str(&format!(" {} = \"{}\"\n", key, value));
224 }
225 }
226
227 output.push_str("}\n");
228}
229
230fn format_generator(output: &mut String, generator: &prax_schema::ast::Generator) {
231 use prax_schema::ast::{GeneratorToggle, GeneratorValue};
232
233 output.push_str(&format!("generator {} {{\n", generator.name()));
234
235 if let Some(provider) = &generator.provider {
236 output.push_str(&format!(" provider = \"{}\"\n", provider));
237 }
238
239 if let Some(out) = &generator.output {
240 output.push_str(&format!(" output = \"{}\"\n", out));
241 }
242
243 match &generator.generate {
244 GeneratorToggle::Always => {}
245 GeneratorToggle::Never | GeneratorToggle::Literal(false) => {
246 output.push_str(" generate = false\n");
247 }
248 GeneratorToggle::Literal(true) => {
249 output.push_str(" generate = true\n");
250 }
251 GeneratorToggle::Env(var) => {
252 output.push_str(&format!(" generate = env(\"{}\")\n", var));
253 }
254 }
255
256 for (key, value) in &generator.properties {
257 let formatted = match value {
258 GeneratorValue::String(s) => format!("\"{}\"", s),
259 GeneratorValue::Bool(b) => b.to_string(),
260 GeneratorValue::Env(var) => format!("env(\"{}\")", var),
261 GeneratorValue::Ident(s) => s.to_string(),
262 };
263 output.push_str(&format!(" {} = {}\n", key, formatted));
264 }
265
266 output.push_str("}\n");
267}
268
269fn format_enum(output: &mut String, enum_def: &prax_schema::ast::Enum) {
270 if let Some(doc) = &enum_def.documentation {
272 for line in doc.text.lines() {
273 output.push_str(&format!("/// {}\n", line));
274 }
275 }
276
277 output.push_str(&format!("enum {} {{\n", enum_def.name()));
278
279 for variant in &enum_def.variants {
280 if let Some(doc) = &variant.documentation {
282 for line in doc.text.lines() {
283 output.push_str(&format!(" /// {}\n", line));
284 }
285 }
286
287 output.push_str(&format!(" {}", variant.name()));
288
289 for attr in &variant.attributes {
291 output.push_str(&format!(" {}", format_attribute(attr, AttrLevel::Field)));
292 }
293
294 output.push('\n');
295 }
296
297 for attr in &enum_def.attributes {
299 output.push_str(&format!(
300 "\n {}",
301 format_attribute(attr, AttrLevel::Block)
302 ));
303 }
304
305 output.push_str("}\n");
306}
307
308fn format_model(output: &mut String, model: &prax_schema::ast::Model) {
309 if let Some(doc) = &model.documentation {
311 for line in doc.text.lines() {
312 output.push_str(&format!("/// {}\n", line));
313 }
314 }
315
316 output.push_str(&format!("model {} {{\n", model.name()));
317
318 let max_name_len = model
320 .fields
321 .values()
322 .map(|f| f.name().len())
323 .max()
324 .unwrap_or(0);
325
326 let max_type_len = model
327 .fields
328 .values()
329 .map(|f| format_field_type(&f.field_type, f.modifier).len())
330 .max()
331 .unwrap_or(0);
332
333 for field in model.fields.values() {
334 if let Some(doc) = &field.documentation {
336 for line in doc.text.lines() {
337 output.push_str(&format!(" /// {}\n", line));
338 }
339 }
340
341 let type_str = format_field_type(&field.field_type, field.modifier);
342
343 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
345 let padded_type = format!("{:width$}", type_str, width = max_type_len);
346
347 output.push_str(&format!(" {} {}", padded_name, padded_type));
348
349 for attr in &field.attributes {
351 output.push_str(&format!(" {}", format_attribute(attr, AttrLevel::Field)));
352 }
353
354 output.push('\n');
355 }
356
357 let model_attrs: Vec<_> = model.attributes.iter().collect();
359 if !model_attrs.is_empty() {
360 output.push('\n');
361 for attr in model_attrs {
362 output.push_str(&format!(
363 " {}\n",
364 format_attribute(attr, AttrLevel::Block)
365 ));
366 }
367 }
368
369 output.push_str("}\n");
370}
371
372fn format_view(output: &mut String, view: &prax_schema::ast::View) {
373 if let Some(doc) = &view.documentation {
375 for line in doc.text.lines() {
376 output.push_str(&format!("/// {}\n", line));
377 }
378 }
379
380 output.push_str(&format!("view {} {{\n", view.name()));
381
382 let max_name_len = view
384 .fields
385 .values()
386 .map(|f| f.name().len())
387 .max()
388 .unwrap_or(0);
389
390 let max_type_len = view
391 .fields
392 .values()
393 .map(|f| format_field_type(&f.field_type, f.modifier).len())
394 .max()
395 .unwrap_or(0);
396
397 for field in view.fields.values() {
398 let type_str = format_field_type(&field.field_type, field.modifier);
399 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
400 let padded_type = format!("{:width$}", type_str, width = max_type_len);
401
402 output.push_str(&format!(" {} {}", padded_name, padded_type));
403
404 for attr in &field.attributes {
405 output.push_str(&format!(" {}", format_attribute(attr, AttrLevel::Field)));
406 }
407
408 output.push('\n');
409 }
410
411 let view_attrs: Vec<_> = view.attributes.iter().collect();
413 if !view_attrs.is_empty() {
414 output.push('\n');
415 for attr in view_attrs {
416 output.push_str(&format!(
417 " {}\n",
418 format_attribute(attr, AttrLevel::Block)
419 ));
420 }
421 }
422
423 output.push_str("}\n");
424}
425
426fn format_composite(output: &mut String, composite: &prax_schema::ast::CompositeType) {
427 if let Some(doc) = &composite.documentation {
429 for line in doc.text.lines() {
430 output.push_str(&format!("/// {}\n", line));
431 }
432 }
433
434 output.push_str(&format!("type {} {{\n", composite.name()));
435
436 let max_name_len = composite
438 .fields
439 .values()
440 .map(|f| f.name().len())
441 .max()
442 .unwrap_or(0);
443
444 let max_type_len = composite
445 .fields
446 .values()
447 .map(|f| format_field_type(&f.field_type, f.modifier).len())
448 .max()
449 .unwrap_or(0);
450
451 for field in composite.fields.values() {
452 let type_str = format_field_type(&field.field_type, field.modifier);
453 let padded_name = format!("{:width$}", field.name(), width = max_name_len);
454 let padded_type = format!("{:width$}", type_str, width = max_type_len);
455
456 output.push_str(&format!(" {} {}", padded_name, padded_type));
457
458 for attr in &field.attributes {
459 output.push_str(&format!(" {}", format_attribute(attr, AttrLevel::Field)));
460 }
461
462 output.push('\n');
463 }
464
465 output.push_str("}\n");
466}
467
468fn format_field_type(
469 field_type: &prax_schema::ast::FieldType,
470 modifier: prax_schema::ast::TypeModifier,
471) -> String {
472 use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
473
474 let base = match field_type {
475 FieldType::Scalar(scalar) => match scalar {
476 ScalarType::Int => "Int",
477 ScalarType::BigInt => "BigInt",
478 ScalarType::Float => "Float",
479 ScalarType::String => "String",
480 ScalarType::Boolean => "Boolean",
481 ScalarType::DateTime => "DateTime",
482 ScalarType::Date => "Date",
483 ScalarType::Time => "Time",
484 ScalarType::Json => "Json",
485 ScalarType::Bytes => "Bytes",
486 ScalarType::Decimal => "Decimal",
487 ScalarType::Uuid => "Uuid",
488 ScalarType::Cuid => "Cuid",
489 ScalarType::Cuid2 => "Cuid2",
490 ScalarType::NanoId => "NanoId",
491 ScalarType::Ulid => "Ulid",
492 ScalarType::Vector(_) => "Vector",
493 ScalarType::HalfVector(_) => "HalfVector",
494 ScalarType::SparseVector(_) => "SparseVector",
495 ScalarType::Bit(_) => "Bit",
496 }
497 .to_string(),
498 FieldType::Model(name) => name.to_string(),
499 FieldType::Enum(name) => name.to_string(),
500 FieldType::Composite(name) => name.to_string(),
501 FieldType::Unsupported(name) => format!("Unsupported(\"{}\")", name),
502 };
503
504 match modifier {
505 TypeModifier::Optional => format!("{}?", base),
506 TypeModifier::List => format!("{}[]", base),
507 TypeModifier::OptionalList => format!("{}[]?", base),
508 TypeModifier::Required => base,
509 }
510}
511
512#[derive(Clone, Copy)]
517enum AttrLevel {
518 Field,
519 Block,
520}
521
522fn format_attribute(attr: &prax_schema::ast::Attribute, level: AttrLevel) -> String {
523 let prefix = match level {
527 AttrLevel::Block => "@@",
528 AttrLevel::Field => "@",
529 };
530
531 if attr.args.is_empty() {
532 format!("{}{}", prefix, attr.name())
533 } else {
534 let args: Vec<String> = attr
535 .args
536 .iter()
537 .map(|arg| {
538 if let Some(name) = &arg.name {
539 format!("{}: {}", name.as_str(), format_attribute_value(&arg.value))
540 } else {
541 format_attribute_value(&arg.value)
542 }
543 })
544 .collect();
545
546 format!("{}{}({})", prefix, attr.name(), args.join(", "))
547 }
548}
549
550fn format_attribute_value(value: &prax_schema::ast::AttributeValue) -> String {
551 use prax_schema::ast::AttributeValue;
552
553 match value {
554 AttributeValue::String(s) => format!("\"{}\"", s),
555 AttributeValue::Int(i) => i.to_string(),
556 AttributeValue::Float(f) => f.to_string(),
557 AttributeValue::Boolean(b) => b.to_string(),
558 AttributeValue::Ident(id) => id.to_string(),
559 AttributeValue::Function(name, args) => {
560 if args.is_empty() {
561 format!("{}()", name)
562 } else {
563 let arg_strs: Vec<String> = args.iter().map(format_attribute_value).collect();
564 format!("{}({})", name, arg_strs.join(", "))
565 }
566 }
567 AttributeValue::Array(items) => {
568 let item_strs: Vec<String> = items.iter().map(format_attribute_value).collect();
569 format!("[{}]", item_strs.join(", "))
570 }
571 AttributeValue::FieldRef(field) => field.to_string(),
572 AttributeValue::FieldRefList(fields) => {
573 format!(
574 "[{}]",
575 fields
576 .iter()
577 .map(|f| f.to_string())
578 .collect::<Vec<_>>()
579 .join(", ")
580 )
581 }
582 }
583}