1use crate::cli::FormatArgs;
4use crate::config::SCHEMA_FILE_NAME;
5use crate::error::{CliError, CliResult};
6use crate::output::{self, success};
7
8pub 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 output::step(1, 3, "Reading schema...");
26 let schema_content = std::fs::read_to_string(&schema_path)?;
27
28 let schema = parse_schema(&schema_content)?;
30
31 output::step(2, 3, "Formatting...");
33 let formatted = format_schema(&schema);
34
35 let changed = formatted != schema_content;
37
38 if args.check {
39 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 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
71fn format_schema(schema: &prax_schema::ast::Schema) -> String {
73 let mut output = String::new();
74
75 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 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 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 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 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 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 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 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 for attr in &variant.attributes {
154 output.push_str(&format!(" {}", format_attribute(attr)));
155 }
156
157 output.push('\n');
158 }
159
160 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 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 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 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 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 for attr in &field.attributes {
211 output.push_str(&format!(" {}", format_attribute(attr)));
212 }
213
214 output.push('\n');
215 }
216
217 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 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 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 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 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 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 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}