prax_cli/commands/
format.rs

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