use crate::ast::{
ComputedKind, Declaration, Expr, FieldAttribute, FieldModifier, FieldType, Literal,
ModelAttribute, ReferentialAction, Schema, StorageStrategy, TypeDecl,
};
pub fn format_schema(schema: &Schema, source: &str) -> String {
let mut parts: Vec<(String, usize)> = Vec::new();
for decl in &schema.declarations {
let block = match decl {
Declaration::Datasource(ds) => {
let max_key = ds
.fields
.iter()
.map(|f| f.name.value.len())
.max()
.unwrap_or(0);
let mut lines = vec![format!("datasource {} {{", ds.name.value)];
for (idx, field) in ds.fields.iter().enumerate() {
if idx > 0 {
let prev = &ds.fields[idx - 1];
push_gap_content(source, prev.span.end, field.span.start, &mut lines, " ");
}
let padding = max_key - field.name.value.len() + 1;
let mut line = format!(
" {}{}= {}",
field.name.value,
" ".repeat(padding),
format_expr(&field.value)
);
if let Some(c) = trailing_inline_comment(source, field.span.end) {
line.push_str(" ");
line.push_str(&c);
}
lines.push(line);
}
lines.push("}".to_string());
lines.join("\n")
}
Declaration::Generator(gen) => {
let max_key = gen
.fields
.iter()
.map(|f| f.name.value.len())
.max()
.unwrap_or(0);
let mut lines = vec![format!("generator {} {{", gen.name.value)];
for (idx, field) in gen.fields.iter().enumerate() {
if idx > 0 {
let prev = &gen.fields[idx - 1];
push_gap_content(source, prev.span.end, field.span.start, &mut lines, " ");
}
let padding = max_key - field.name.value.len() + 1;
let mut line = format!(
" {}{}= {}",
field.name.value,
" ".repeat(padding),
format_expr(&field.value)
);
if let Some(c) = trailing_inline_comment(source, field.span.end) {
line.push_str(" ");
line.push_str(&c);
}
lines.push(line);
}
lines.push("}".to_string());
lines.join("\n")
}
Declaration::Enum(e) => {
let mut lines = vec![format!("enum {} {{", e.name.value)];
for (idx, variant) in e.variants.iter().enumerate() {
if idx > 0 {
let prev = &e.variants[idx - 1];
push_gap_content(
source,
prev.span.end,
variant.span.start,
&mut lines,
" ",
);
}
let mut line = format!(" {}", variant.name.value);
if let Some(c) = trailing_inline_comment(source, variant.span.end) {
line.push_str(" ");
line.push_str(&c);
}
lines.push(line);
}
lines.push("}".to_string());
lines.join("\n")
}
Declaration::Model(model) => {
let max_name = model
.fields
.iter()
.map(|f| f.name.value.len())
.max()
.unwrap_or(0);
let max_type = model
.fields
.iter()
.map(|f| format_field_type_with_modifier(&f.field_type, f.modifier).len())
.max()
.unwrap_or(0);
let mut lines = vec![format!("model {} {{", model.name.value)];
for (idx, field) in model.fields.iter().enumerate() {
if idx > 0 {
let prev = &model.fields[idx - 1];
push_gap_content(source, prev.span.end, field.span.start, &mut lines, " ");
}
let type_str =
format_field_type_with_modifier(&field.field_type, field.modifier);
let attrs: Vec<String> =
field.attributes.iter().map(format_field_attr).collect();
let name_padding = max_name - field.name.value.len() + 1;
let mut line = if attrs.is_empty() {
format!(
" {}{}{}",
field.name.value,
" ".repeat(name_padding),
type_str,
)
} else {
let type_padding = max_type - type_str.len() + 1;
format!(
" {}{}{}{}{}",
field.name.value,
" ".repeat(name_padding),
type_str,
" ".repeat(type_padding),
attrs.join(" "),
)
};
line = line.trim_end().to_string();
if let Some(c) = trailing_inline_comment(source, field.span.end) {
line.push_str(" ");
line.push_str(&c);
}
lines.push(line);
}
if !model.attributes.is_empty() && !model.fields.is_empty() {
lines.push(String::new());
}
for attr in &model.attributes {
lines.push(format!(" {}", format_model_attr(attr)));
}
lines.push("}".to_string());
lines.join("\n")
}
Declaration::Type(type_decl) => format_type_decl(type_decl, source),
};
parts.push((block, decl.span().end));
}
let mut out = String::new();
if let Some((_, _)) = parts.first() {
if let Some(first_decl) = schema.declarations.first() {
let leading = top_level_comments(source, 0, first_decl.span().start);
for line in &leading {
out.push_str(line);
out.push('\n');
}
if !leading.is_empty() {
out.push('\n');
}
}
}
for (i, (block, span_end)) in parts.iter().enumerate() {
if i > 0 {
let prev_end = parts[i - 1].1;
let curr_start = schema.declarations[i].span().start;
let gap_comments = top_level_comments(source, prev_end, curr_start);
if gap_comments.is_empty() {
out.push_str("\n\n");
} else {
out.push_str("\n\n");
for comment in &gap_comments {
out.push_str(comment);
out.push('\n');
}
out.push('\n');
}
}
out.push_str(block);
let _ = span_end;
}
if let Some(last_decl) = schema.declarations.last() {
let trailing = top_level_comments(source, last_decl.span().end, source.len());
for comment in &trailing {
out.push('\n');
out.push_str(comment);
}
}
if !out.ends_with('\n') {
out.push('\n');
}
out
}
fn trailing_inline_comment(source: &str, pos: usize) -> Option<String> {
let rest = source.get(pos..)?;
let line_end = rest.find('\n').unwrap_or(rest.len());
let rest_of_line = rest[..line_end].trim();
if rest_of_line.starts_with("//") {
Some(rest_of_line.to_string())
} else {
None
}
}
fn push_gap_content(
source: &str,
prev_end: usize,
curr_start: usize,
lines: &mut Vec<String>,
indent: &str,
) {
if prev_end >= curr_start {
return;
}
let gap = match source.get(prev_end..curr_start) {
Some(s) => s,
None => return,
};
let after_first_nl = match gap.find('\n') {
Some(pos) => &gap[pos + 1..],
None => return,
};
let mut comment_lines: Vec<String> = Vec::new();
let mut blank_before_first = false;
for raw_line in after_first_nl.lines() {
let trimmed = raw_line.trim();
if trimmed.starts_with("//") {
comment_lines.push(trimmed.to_string());
} else if trimmed.is_empty() && comment_lines.is_empty() {
blank_before_first = true;
}
}
if !comment_lines.is_empty() {
if blank_before_first {
lines.push(String::new());
}
for c in &comment_lines {
lines.push(format!("{}{}", indent, c));
}
} else {
let nl_count = after_first_nl.chars().filter(|&c| c == '\n').count();
if nl_count >= 1 {
lines.push(String::new());
}
}
}
fn top_level_comments(source: &str, from: usize, to: usize) -> Vec<String> {
let slice = match source.get(from..to) {
Some(s) => s,
None => return Vec::new(),
};
let mut result: Vec<String> = Vec::new();
for raw_line in slice.lines() {
let trimmed = raw_line.trim();
if trimmed.starts_with("//") {
result.push(trimmed.to_string());
} else if trimmed.is_empty() && !result.is_empty() {
result.push(String::new());
}
}
while result
.last()
.map(|s: &String| s.is_empty())
.unwrap_or(false)
{
result.pop();
}
result
}
fn format_type_decl(type_decl: &TypeDecl, source: &str) -> String {
let max_name = type_decl
.fields
.iter()
.map(|f| f.name.value.len())
.max()
.unwrap_or(0);
let max_type = type_decl
.fields
.iter()
.map(|f| format_field_type_with_modifier(&f.field_type, f.modifier).len())
.max()
.unwrap_or(0);
let mut lines = vec![format!("type {} {{", type_decl.name.value)];
for (idx, field) in type_decl.fields.iter().enumerate() {
if idx > 0 {
let prev = &type_decl.fields[idx - 1];
push_gap_content(source, prev.span.end, field.span.start, &mut lines, " ");
}
let type_str = format_field_type_with_modifier(&field.field_type, field.modifier);
let attrs: Vec<String> = field.attributes.iter().map(format_field_attr).collect();
let name_padding = max_name - field.name.value.len() + 1;
let mut line = if attrs.is_empty() {
format!(
" {}{}{}",
field.name.value,
" ".repeat(name_padding),
type_str,
)
} else {
let type_padding = max_type - type_str.len() + 1;
format!(
" {}{}{}{}{}",
field.name.value,
" ".repeat(name_padding),
type_str,
" ".repeat(type_padding),
attrs.join(" "),
)
};
line = line.trim_end().to_string();
if let Some(c) = trailing_inline_comment(source, field.span.end) {
line.push_str(" ");
line.push_str(&c);
}
lines.push(line);
}
lines.push("}".to_string());
lines.join("\n")
}
fn format_field_type_with_modifier(ft: &FieldType, modifier: FieldModifier) -> String {
let base = ft.to_string();
match modifier {
FieldModifier::None => base,
FieldModifier::Optional => format!("{}?", base),
FieldModifier::NotNull => format!("{}!", base),
FieldModifier::Array => format!("{}[]", base),
}
}
fn format_field_attr(attr: &FieldAttribute) -> String {
match attr {
FieldAttribute::Id => "@id".to_string(),
FieldAttribute::Unique => "@unique".to_string(),
FieldAttribute::Default(expr, _) => format!("@default({})", format_expr(expr)),
FieldAttribute::Map(name) => format!("@map(\"{}\")", name),
FieldAttribute::Store { strategy, .. } => match strategy {
StorageStrategy::Json => "@store(json)".to_string(),
StorageStrategy::Native => "@store(native)".to_string(),
},
FieldAttribute::Relation {
name,
fields,
references,
on_delete,
on_update,
..
} => {
let mut args: Vec<String> = Vec::new();
if let Some(n) = name {
args.push(format!("name: \"{}\"", n));
}
if let Some(flds) = fields {
let names: Vec<_> = flds.iter().map(|i| i.value.clone()).collect();
args.push(format!("fields: [{}]", names.join(", ")));
}
if let Some(refs) = references {
let names: Vec<_> = refs.iter().map(|i| i.value.clone()).collect();
args.push(format!("references: [{}]", names.join(", ")));
}
if let Some(action) = on_delete {
args.push(format!("onDelete: {}", format_referential_action(action)));
}
if let Some(action) = on_update {
args.push(format!("onUpdate: {}", format_referential_action(action)));
}
format!("@relation({})", args.join(", "))
}
FieldAttribute::UpdatedAt { .. } => "@updatedAt".to_string(),
FieldAttribute::Computed { expr, kind, .. } => {
let kind_str = match kind {
ComputedKind::Stored => "Stored",
ComputedKind::Virtual => "Virtual",
};
format!("@computed({}, {})", expr, kind_str)
}
FieldAttribute::Check { expr, .. } => format!("@check({})", expr),
}
}
fn format_model_attr(attr: &ModelAttribute) -> String {
match attr {
ModelAttribute::Map(name) => format!("@@map(\"{}\")", name),
ModelAttribute::Id(fields) => {
let names: Vec<_> = fields.iter().map(|i| i.value.clone()).collect();
format!("@@id([{}])", names.join(", "))
}
ModelAttribute::Unique(fields) => {
let names: Vec<_> = fields.iter().map(|i| i.value.clone()).collect();
format!("@@unique([{}])", names.join(", "))
}
ModelAttribute::Index {
fields,
index_type,
name,
map,
} => {
let names: Vec<_> = fields.iter().map(|i| i.value.clone()).collect();
let mut s = format!("@@index([{}])", names.join(", "));
if index_type.is_some() || name.is_some() || map.is_some() {
s.pop();
if let Some(t) = index_type {
s.push_str(&format!(", type: {}", t.value));
}
if let Some(n) = name {
s.push_str(&format!(", name: \"{}\"", n));
}
if let Some(m) = map {
s.push_str(&format!(", map: \"{}\"", m));
}
s.push(')');
}
s
}
ModelAttribute::Check { expr, .. } => format!("@@check({})", expr),
}
}
fn format_referential_action(action: &ReferentialAction) -> &'static str {
match action {
ReferentialAction::Cascade => "Cascade",
ReferentialAction::Restrict => "Restrict",
ReferentialAction::NoAction => "NoAction",
ReferentialAction::SetNull => "SetNull",
ReferentialAction::SetDefault => "SetDefault",
}
}
pub(crate) fn format_expr(expr: &Expr) -> String {
match expr {
Expr::Literal(lit) => format_literal(lit),
Expr::FunctionCall { name, args, .. } => {
if args.is_empty() {
format!("{}()", name.value)
} else {
let formatted: Vec<_> = args.iter().map(format_expr).collect();
format!("{}({})", name.value, formatted.join(", "))
}
}
Expr::Array { elements, .. } => {
let formatted: Vec<_> = elements.iter().map(format_expr).collect();
format!("[{}]", formatted.join(", "))
}
Expr::NamedArg { name, value, .. } => {
format!("{}: {}", name.value, format_expr(value))
}
Expr::Ident(ident) => ident.value.clone(),
}
}
fn format_literal(lit: &Literal) -> String {
match lit {
Literal::String(s, _) => format!("\"{}\"", s),
Literal::Number(n, _) => n.clone(),
Literal::Boolean(b, _) => b.to_string(),
}
}