1use crate::cli::FormatArgs;
4use crate::config::SCHEMA_FILE_PATH;
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_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 output::step(1, 3, "Reading schema...");
27 let schema_content = std::fs::read_to_string(&schema_path)?;
28
29 let schema = parse_schema(&schema_content)?;
31
32 output::step(2, 3, "Formatting...");
34 let formatted = format_schema(&schema);
35
36 let changed = formatted != schema_content;
38
39 if args.check {
40 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 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 prax_schema::validate_schema(content)
72 .map_err(|e| CliError::Schema(format!("Syntax error: {}", e)))
73}
74
75fn format_schema(schema: &prax_schema::ast::Schema) -> String {
77 let mut output = String::new();
78
79 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 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 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 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 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 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 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 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 for attr in &variant.attributes {
158 output.push_str(&format!(" {}", format_attribute(attr)));
159 }
160
161 output.push('\n');
162 }
163
164 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 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 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 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 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 for attr in &field.attributes {
215 output.push_str(&format!(" {}", format_attribute(attr)));
216 }
217
218 output.push('\n');
219 }
220
221 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 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 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 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 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 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 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}