Skip to main content

prax_cli/commands/
format.rs

1//! `prax format` command - Format Prax schema file.
2
3use crate::cli::FormatArgs;
4use crate::config::SCHEMA_FILE_PATH;
5use crate::error::{CliError, CliResult};
6use crate::output::{self, success};
7
8/// Run the format command
9pub async fn run(args: FormatArgs) -> CliResult<()> {
10    output::header("Format Schema");
11
12    let cwd = std::env::current_dir()?;
13    let schema_path = args.schema.unwrap_or_else(|| cwd.join(SCHEMA_FILE_PATH));
14
15    if !schema_path.exists() {
16        return Err(CliError::Config(format!(
17            "Schema file not found: {}",
18            schema_path.display()
19        )));
20    }
21
22    output::kv("Schema", &schema_path.display().to_string());
23    output::newline();
24
25    // Read schema
26    output::step(1, 3, "Reading schema...");
27    let schema_content = std::fs::read_to_string(&schema_path)?;
28
29    // Parse schema to validate it first
30    let schema = parse_schema(&schema_content)?;
31
32    // Format schema
33    output::step(2, 3, "Formatting...");
34    let formatted = format_schema(&schema);
35
36    // Check if formatting changed anything
37    let changed = formatted != schema_content;
38
39    if args.check {
40        // Check mode - just report if formatting is needed
41        if changed {
42            output::newline();
43            output::error("Schema is not formatted correctly!");
44            output::info("Run `prax format` to fix formatting.");
45            return Err(CliError::Format("Schema needs formatting".to_string()));
46        } else {
47            output::newline();
48            success("Schema is already formatted!");
49            return Ok(());
50        }
51    }
52
53    // Write formatted schema
54    output::step(3, 3, "Writing formatted schema...");
55
56    if changed {
57        std::fs::write(&schema_path, &formatted)?;
58        output::newline();
59        success("Schema formatted successfully!");
60    } else {
61        output::newline();
62        success("Schema is already formatted!");
63    }
64
65    Ok(())
66}
67
68fn parse_schema(content: &str) -> CliResult<prax_schema::Schema> {
69    // Use validate_schema to ensure field types are properly resolved
70    // (e.g., FieldType::Model -> FieldType::Enum for enum references)
71    prax_schema::validate_schema(content)
72        .map_err(|e| CliError::Schema(format!("Syntax error: {}", e)))
73}
74
75/// Format a schema AST into a formatted string
76fn format_schema(schema: &prax_schema::ast::Schema) -> String {
77    let mut output = String::new();
78
79    // Format datasource (if present in schema)
80    // For now, just add a standard datasource section
81    output.push_str("datasource db {\n");
82    output.push_str("    provider = \"postgresql\"\n");
83    output.push_str("    url      = env(\"DATABASE_URL\")\n");
84    output.push_str("}\n");
85    let mut first_section = false;
86
87    // Format generator
88    if !first_section {
89        output.push('\n');
90    }
91    output.push_str("generator client {\n");
92    output.push_str("    provider = \"prax-client-rust\"\n");
93    output.push_str("    output   = \"./src/generated\"\n");
94    output.push_str("}\n");
95    first_section = false;
96
97    // Format enums first (since they're used by models)
98    for enum_def in schema.enums.values() {
99        if !first_section {
100            output.push('\n');
101        }
102        format_enum(&mut output, enum_def);
103        first_section = false;
104    }
105
106    // Format models
107    for model in schema.models.values() {
108        if !first_section {
109            output.push('\n');
110        }
111        format_model(&mut output, model);
112        first_section = false;
113    }
114
115    // Format views
116    for view in schema.views.values() {
117        if !first_section {
118            output.push('\n');
119        }
120        format_view(&mut output, view);
121        first_section = false;
122    }
123
124    // Format composite types
125    for composite in schema.types.values() {
126        if !first_section {
127            output.push('\n');
128        }
129        format_composite(&mut output, composite);
130        first_section = false;
131    }
132
133    output
134}
135
136fn format_enum(output: &mut String, enum_def: &prax_schema::ast::Enum) {
137    // Documentation
138    if let Some(doc) = &enum_def.documentation {
139        for line in doc.text.lines() {
140            output.push_str(&format!("/// {}\n", line));
141        }
142    }
143
144    output.push_str(&format!("enum {} {{\n", enum_def.name()));
145
146    for variant in &enum_def.variants {
147        // Documentation
148        if let Some(doc) = &variant.documentation {
149            for line in doc.text.lines() {
150                output.push_str(&format!("    /// {}\n", line));
151            }
152        }
153
154        output.push_str(&format!("    {}", variant.name()));
155
156        // Format attributes
157        for attr in &variant.attributes {
158            output.push_str(&format!(" {}", format_attribute(attr)));
159        }
160
161        output.push('\n');
162    }
163
164    // Enum-level attributes
165    for attr in &enum_def.attributes {
166        output.push_str(&format!("\n    {}", format_attribute(attr)));
167    }
168
169    output.push_str("}\n");
170}
171
172fn format_model(output: &mut String, model: &prax_schema::ast::Model) {
173    // Documentation
174    if let Some(doc) = &model.documentation {
175        for line in doc.text.lines() {
176            output.push_str(&format!("/// {}\n", line));
177        }
178    }
179
180    output.push_str(&format!("model {} {{\n", model.name()));
181
182    // Calculate alignment for fields
183    let max_name_len = model
184        .fields
185        .values()
186        .map(|f| f.name().len())
187        .max()
188        .unwrap_or(0);
189
190    let max_type_len = model
191        .fields
192        .values()
193        .map(|f| format_field_type(&f.field_type, f.modifier).len())
194        .max()
195        .unwrap_or(0);
196
197    for field in model.fields.values() {
198        // Documentation
199        if let Some(doc) = &field.documentation {
200            for line in doc.text.lines() {
201                output.push_str(&format!("    /// {}\n", line));
202            }
203        }
204
205        let type_str = format_field_type(&field.field_type, field.modifier);
206
207        // Pad name and type for alignment
208        let padded_name = format!("{:width$}", field.name(), width = max_name_len);
209        let padded_type = format!("{:width$}", type_str, width = max_type_len);
210
211        output.push_str(&format!("    {} {}", padded_name, padded_type));
212
213        // Format attributes
214        for attr in &field.attributes {
215            output.push_str(&format!(" {}", format_attribute(attr)));
216        }
217
218        output.push('\n');
219    }
220
221    // Model-level attributes
222    let model_attrs: Vec<_> = model.attributes.iter().collect();
223    if !model_attrs.is_empty() {
224        output.push('\n');
225        for attr in model_attrs {
226            output.push_str(&format!("    {}\n", format_attribute(attr)));
227        }
228    }
229
230    output.push_str("}\n");
231}
232
233fn format_view(output: &mut String, view: &prax_schema::ast::View) {
234    // Documentation
235    if let Some(doc) = &view.documentation {
236        for line in doc.text.lines() {
237            output.push_str(&format!("/// {}\n", line));
238        }
239    }
240
241    output.push_str(&format!("view {} {{\n", view.name()));
242
243    // Calculate alignment for fields
244    let max_name_len = view
245        .fields
246        .values()
247        .map(|f| f.name().len())
248        .max()
249        .unwrap_or(0);
250
251    let max_type_len = view
252        .fields
253        .values()
254        .map(|f| format_field_type(&f.field_type, f.modifier).len())
255        .max()
256        .unwrap_or(0);
257
258    for field in view.fields.values() {
259        let type_str = format_field_type(&field.field_type, field.modifier);
260        let padded_name = format!("{:width$}", field.name(), width = max_name_len);
261        let padded_type = format!("{:width$}", type_str, width = max_type_len);
262
263        output.push_str(&format!("    {} {}", padded_name, padded_type));
264
265        for attr in &field.attributes {
266            output.push_str(&format!(" {}", format_attribute(attr)));
267        }
268
269        output.push('\n');
270    }
271
272    // View-level attributes
273    let view_attrs: Vec<_> = view.attributes.iter().collect();
274    if !view_attrs.is_empty() {
275        output.push('\n');
276        for attr in view_attrs {
277            output.push_str(&format!("    {}\n", format_attribute(attr)));
278        }
279    }
280
281    output.push_str("}\n");
282}
283
284fn format_composite(output: &mut String, composite: &prax_schema::ast::CompositeType) {
285    // Documentation
286    if let Some(doc) = &composite.documentation {
287        for line in doc.text.lines() {
288            output.push_str(&format!("/// {}\n", line));
289        }
290    }
291
292    output.push_str(&format!("type {} {{\n", composite.name()));
293
294    // Calculate alignment for fields
295    let max_name_len = composite
296        .fields
297        .values()
298        .map(|f| f.name().len())
299        .max()
300        .unwrap_or(0);
301
302    let max_type_len = composite
303        .fields
304        .values()
305        .map(|f| format_field_type(&f.field_type, f.modifier).len())
306        .max()
307        .unwrap_or(0);
308
309    for field in composite.fields.values() {
310        let type_str = format_field_type(&field.field_type, field.modifier);
311        let padded_name = format!("{:width$}", field.name(), width = max_name_len);
312        let padded_type = format!("{:width$}", type_str, width = max_type_len);
313
314        output.push_str(&format!("    {} {}", padded_name, padded_type));
315
316        for attr in &field.attributes {
317            output.push_str(&format!(" {}", format_attribute(attr)));
318        }
319
320        output.push('\n');
321    }
322
323    output.push_str("}\n");
324}
325
326fn format_field_type(
327    field_type: &prax_schema::ast::FieldType,
328    modifier: prax_schema::ast::TypeModifier,
329) -> String {
330    use prax_schema::ast::{FieldType, ScalarType, TypeModifier};
331
332    let base = match field_type {
333        FieldType::Scalar(scalar) => match scalar {
334            ScalarType::Int => "Int",
335            ScalarType::BigInt => "BigInt",
336            ScalarType::Float => "Float",
337            ScalarType::String => "String",
338            ScalarType::Boolean => "Boolean",
339            ScalarType::DateTime => "DateTime",
340            ScalarType::Date => "Date",
341            ScalarType::Time => "Time",
342            ScalarType::Json => "Json",
343            ScalarType::Bytes => "Bytes",
344            ScalarType::Decimal => "Decimal",
345            ScalarType::Uuid => "Uuid",
346            ScalarType::Cuid => "Cuid",
347            ScalarType::Cuid2 => "Cuid2",
348            ScalarType::NanoId => "NanoId",
349            ScalarType::Ulid => "Ulid",
350            ScalarType::Vector(_) => "Vector",
351            ScalarType::HalfVector(_) => "HalfVector",
352            ScalarType::SparseVector(_) => "SparseVector",
353            ScalarType::Bit(_) => "Bit",
354        }
355        .to_string(),
356        FieldType::Model(name) => name.to_string(),
357        FieldType::Enum(name) => name.to_string(),
358        FieldType::Composite(name) => name.to_string(),
359        FieldType::Unsupported(name) => format!("Unsupported(\"{}\")", name),
360    };
361
362    match modifier {
363        TypeModifier::Optional => format!("{}?", base),
364        TypeModifier::List => format!("{}[]", base),
365        TypeModifier::OptionalList => format!("{}[]?", base),
366        TypeModifier::Required => base,
367    }
368}
369
370fn format_attribute(attr: &prax_schema::ast::Attribute) -> String {
371    // For model-level attributes we check if it's a known model attribute
372    let prefix = if attr.is_model_attribute() { "@@" } else { "@" };
373
374    if attr.args.is_empty() {
375        format!("{}{}", prefix, attr.name())
376    } else {
377        let args: Vec<String> = attr
378            .args
379            .iter()
380            .map(|arg| {
381                if let Some(name) = &arg.name {
382                    format!("{}: {}", name.as_str(), format_attribute_value(&arg.value))
383                } else {
384                    format_attribute_value(&arg.value)
385                }
386            })
387            .collect();
388
389        format!("{}{}({})", prefix, attr.name(), args.join(", "))
390    }
391}
392
393fn format_attribute_value(value: &prax_schema::ast::AttributeValue) -> String {
394    use prax_schema::ast::AttributeValue;
395
396    match value {
397        AttributeValue::String(s) => format!("\"{}\"", s),
398        AttributeValue::Int(i) => i.to_string(),
399        AttributeValue::Float(f) => f.to_string(),
400        AttributeValue::Boolean(b) => b.to_string(),
401        AttributeValue::Ident(id) => id.to_string(),
402        AttributeValue::Function(name, args) => {
403            if args.is_empty() {
404                format!("{}()", name)
405            } else {
406                let arg_strs: Vec<String> = args.iter().map(format_attribute_value).collect();
407                format!("{}({})", name, arg_strs.join(", "))
408            }
409        }
410        AttributeValue::Array(items) => {
411            let item_strs: Vec<String> = items.iter().map(format_attribute_value).collect();
412            format!("[{}]", item_strs.join(", "))
413        }
414        AttributeValue::FieldRef(field) => field.to_string(),
415        AttributeValue::FieldRefList(fields) => {
416            format!(
417                "[{}]",
418                fields
419                    .iter()
420                    .map(|f| f.to_string())
421                    .collect::<Vec<_>>()
422                    .join(", ")
423            )
424        }
425    }
426}