1#![allow(clippy::struct_excessive_bools)]
2
3use crate::error::AamlError;
10use crate::pipeline::parser::AstNode;
11
12#[derive(Debug, Clone)]
14pub struct FormattingOptions {
15 pub indent_size: usize,
17
18 pub use_tabs: bool,
20
21 pub line_width: usize,
23
24 pub sort_keys: bool,
26
27 pub trailing_newline: bool,
29
30 pub preserve_blank_lines: bool,
32}
33
34impl Default for FormattingOptions {
35 fn default() -> Self {
36 Self {
37 indent_size: 4,
38 use_tabs: false,
39 line_width: 100,
40 sort_keys: false,
41 trailing_newline: true,
42 preserve_blank_lines: true,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Copy)]
49pub struct FormatRange {
50 pub start_line: usize,
52 pub end_line: usize,
54}
55
56pub trait Formatter: Send + Sync {
57 fn format_document(
63 &self,
64 nodes: &[AstNode],
65 options: &FormattingOptions,
66 ) -> Result<String, AamlError>;
67
68 fn format_range(
74 &self,
75 nodes: &[AstNode],
76 range: FormatRange,
77 options: &FormattingOptions,
78 ) -> Result<String, AamlError>;
79
80 fn format_node(
86 &self,
87 node: &AstNode,
88 indent_level: usize,
89 options: &FormattingOptions,
90 ) -> Result<String, AamlError>;
91
92 fn normalize_comments(
98 &self,
99 content: &str,
100 options: &FormattingOptions,
101 ) -> Result<String, AamlError>;
102
103 fn normalize_whitespace(&self, content: &str) -> Result<String, AamlError>;
109}
110
111pub struct DefaultFormatter;
112
113impl DefaultFormatter {
114 #[must_use]
115 pub const fn new() -> Self {
116 Self
117 }
118
119 fn create_indent(level: usize, options: &FormattingOptions) -> String {
121 if options.use_tabs {
122 "\t".repeat(level)
123 } else {
124 " ".repeat(level * options.indent_size)
125 }
126 }
127
128 fn is_hoistable(node: &AstNode) -> bool {
130 if let AstNode::Directive { name, .. } = node {
131 matches!(name.as_ref(), "import" | "derive")
132 } else {
133 false
134 }
135 }
136
137 fn format_assignment(
139 key: &str,
140 value: &str,
141 indent_level: usize,
142 options: &FormattingOptions,
143 ) -> String {
144 let indent = Self::create_indent(indent_level, options);
145 format!("{}{} = {}", indent, key.trim(), value.trim())
147 }
148
149 fn format_type_alias(args: &str, indent_level: usize, options: &FormattingOptions) -> String {
151 let indent = Self::create_indent(indent_level, options);
152 if let Some((name, alias)) = args.split_once('=') {
153 format!("{}@type {} = {}", indent, name.trim(), alias.trim())
154 } else {
155 format!("{}@type {}", indent, args.trim())
156 }
157 }
158
159 fn format_schema(args: &str, indent_level: usize, options: &FormattingOptions) -> String {
160 let indent = Self::create_indent(indent_level, options);
161
162 let Some((name_part, body_part)) = args.split_once('{') else {
163 return Self::format_directive("schema", args, indent_level, options);
164 };
165
166 let schema_name = name_part.trim();
167 let body = body_part.trim_end_matches('}').trim();
168
169 let pairs: Vec<_> = body
170 .split([',', '\n'])
171 .map(str::trim)
172 .filter(|s| !s.is_empty())
173 .collect();
174
175 let mut formatted_pairs = Vec::new();
176 for pair in pairs {
177 if let Some((k, v)) = pair.split_once(':') {
178 formatted_pairs.push(format!("{}: {}", k.trim(), v.trim()));
179 } else {
180 formatted_pairs.push(pair.to_string());
181 }
182 }
183
184 let single_line = format!(
185 "{}@schema {} {{ {} }}",
186 indent,
187 schema_name,
188 formatted_pairs.join(", ")
189 );
190
191 if options.line_width == 0 || single_line.len() <= options.line_width {
192 return single_line;
193 }
194
195 let inner_indent = Self::create_indent(indent_level + 1, options);
196 let mut lines = vec![format!("{}@schema {} {{", indent, schema_name)];
197
198 for pair in formatted_pairs {
199 lines.push(format!("{inner_indent}{pair}"));
200 }
201 lines.push(format!("{indent}}}"));
202
203 lines.join("\n")
204 }
205
206 fn format_directive(
207 name: &str,
208 args: &str,
209 indent_level: usize,
210 options: &FormattingOptions,
211 ) -> String {
212 let indent = Self::create_indent(indent_level, options);
213 if args.trim().is_empty() {
214 format!("{}@{}", indent, name.trim())
215 } else {
216 format!("{}@{} {}", indent, name.trim(), args.trim())
217 }
218 }
219}
220
221impl Default for DefaultFormatter {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl Formatter for DefaultFormatter {
228 fn format_document(
229 &self,
230 nodes: &[AstNode],
231 options: &FormattingOptions,
232 ) -> Result<String, AamlError> {
233 let (hoistable, others): (Vec<_>, Vec<_>) =
234 nodes.iter().partition(|n| Self::is_hoistable(n));
235
236 let mut header: Vec<String> = hoistable
237 .into_iter()
238 .map(|n| self.format_node(n, 0, options))
239 .collect::<Result<_, _>>()?;
240
241 let body: Vec<String> = others
242 .into_iter()
243 .map(|n| self.format_node(n, 0, options))
244 .collect::<Result<_, _>>()?;
245
246 if !header.is_empty() && !body.is_empty() && options.preserve_blank_lines {
247 header.push(String::new());
248 }
249
250 let mut result = [header, body].concat().join("\n");
251
252 if options.trailing_newline && !result.is_empty() && !result.ends_with('\n') {
253 result.push('\n');
254 }
255
256 Ok(result)
257 }
258
259 fn format_range(
260 &self,
261 nodes: &[AstNode],
262 range: FormatRange,
263 options: &FormattingOptions,
264 ) -> Result<String, AamlError> {
265 let mut output = Vec::new();
266
267 for node in nodes {
268 let line = node.line(); if line >= range.start_line && line <= range.end_line {
270 let formatted = self.format_node(node, 0, options)?;
271 output.push(formatted);
272 } else {
273 output.push(format!("(original line {line})"));
274 }
275 }
276
277 Ok(output.join("\n"))
278 }
279
280 fn format_node(
281 &self,
282 node: &AstNode,
283 indent_level: usize,
284 options: &FormattingOptions,
285 ) -> Result<String, AamlError> {
286 let formatted = match node {
287 AstNode::Assignment { key, value, .. } => {
288 Self::format_assignment(key, &value.to_string(), indent_level, options)
289 }
290 AstNode::Directive { name, args, .. } => match name.as_ref() {
291 "schema" => Self::format_schema(args, indent_level, options),
292 "type" => Self::format_type_alias(args, indent_level, options),
293 _ => Self::format_directive(name.as_ref(), args, indent_level, options),
294 },
295 };
296
297 Ok(formatted)
298 }
299
300 fn normalize_comments(
301 &self,
302 content: &str,
303 _options: &FormattingOptions,
304 ) -> Result<String, AamlError> {
305 let lines: Vec<&str> = content.lines().collect();
306 let normalized: Vec<String> = lines
307 .iter()
308 .map(|line| {
309 if let Some(pos) = line.find('#') {
310 let before = &line[..pos];
311 let after = &line[pos + 1..];
312
313 if pos > 0
314 && pos < line.len() - 1
315 && before.ends_with(' ')
316 && !after.starts_with('#')
317 {
318 let comment = after.trim_start();
319 return format!("{}# {}", before.trim_end(), comment);
320 }
321 }
322 line.to_string()
323 })
324 .collect();
325
326 Ok(normalized.join("\n"))
327 }
328
329 fn normalize_whitespace(&self, content: &str) -> Result<String, AamlError> {
330 let lines: Vec<&str> = content.lines().collect();
331 let normalized: Vec<String> = lines
332 .iter()
333 .map(|line| line.trim_end().to_string())
334 .collect();
335
336 Ok(normalized.join("\n"))
337 }
338}