1use serde::{Deserialize, Serialize};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
41pub struct Position {
42 pub line: usize,
44 pub column: usize,
46 pub offset: usize,
48}
49
50impl Position {
51 pub fn new(line: usize, column: usize, offset: usize) -> Self {
52 Self {
53 line,
54 column,
55 offset,
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
64pub struct Span {
65 pub start: Position,
67 pub end: Position,
69}
70
71impl Span {
72 pub fn new(start: Position, end: Position) -> Self {
73 Self { start, end }
74 }
75}
76
77#[derive(Debug, Clone, Default, Serialize, Deserialize)]
83pub struct Config {
84 pub items: Vec<ConfigItem>,
86 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 pub include_context: Vec<String>,
90}
91
92impl Config {
93 pub fn new() -> Self {
94 Self {
95 items: Vec::new(),
96 include_context: Vec::new(),
97 }
98 }
99
100 pub fn directives(&self) -> impl Iterator<Item = &Directive> {
102 self.items.iter().filter_map(|item| match item {
103 ConfigItem::Directive(d) => Some(d.as_ref()),
104 _ => None,
105 })
106 }
107
108 pub fn all_directives(&self) -> AllDirectives<'_> {
110 AllDirectives::new(&self.items)
111 }
112
113 pub fn to_source(&self) -> String {
115 let mut output = String::new();
116 for item in &self.items {
117 item.write_source(&mut output, 0);
118 }
119 output
120 }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125pub enum ConfigItem {
126 Directive(Box<Directive>),
128 Comment(Comment),
130 BlankLine(BlankLine),
132}
133
134impl ConfigItem {
135 fn write_source(&self, output: &mut String, indent: usize) {
136 match self {
137 ConfigItem::Directive(d) => d.write_source(output, indent),
138 ConfigItem::Comment(c) => {
139 output.push_str(&c.leading_whitespace);
140 output.push_str(&c.text);
141 output.push_str(&c.trailing_whitespace);
142 output.push('\n');
143 }
144 ConfigItem::BlankLine(b) => {
145 output.push_str(&b.content);
146 output.push('\n');
147 }
148 }
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct BlankLine {
155 pub span: Span,
156 #[serde(default)]
158 pub content: String,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Comment {
164 pub text: String, pub span: Span,
166 #[serde(default)]
168 pub leading_whitespace: String,
169 #[serde(default)]
171 pub trailing_whitespace: String,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct Directive {
181 pub name: String,
183 pub name_span: Span,
185 pub args: Vec<Argument>,
187 pub block: Option<Block>,
189 pub span: Span,
191 pub trailing_comment: Option<Comment>,
193 #[serde(default)]
195 pub leading_whitespace: String,
196 #[serde(default)]
198 pub space_before_terminator: String,
199 #[serde(default)]
201 pub trailing_whitespace: String,
202}
203
204impl Directive {
205 pub fn is(&self, name: &str) -> bool {
207 self.name == name
208 }
209
210 pub fn first_arg(&self) -> Option<&str> {
212 self.args.first().map(|a| a.as_str())
213 }
214
215 pub fn first_arg_is(&self, value: &str) -> bool {
217 self.first_arg() == Some(value)
218 }
219
220 fn write_source(&self, output: &mut String, indent: usize) {
221 let indent_str = if !self.leading_whitespace.is_empty() {
223 self.leading_whitespace.clone()
224 } else {
225 " ".repeat(indent)
226 };
227 output.push_str(&indent_str);
228 output.push_str(&self.name);
229
230 for arg in &self.args {
231 output.push(' ');
232 output.push_str(&arg.raw);
233 }
234
235 if let Some(block) = &self.block {
236 output.push_str(&self.space_before_terminator);
237 output.push('{');
238 output.push_str(&self.trailing_whitespace);
239 output.push('\n');
240 for item in &block.items {
241 item.write_source(output, indent + 1);
242 }
243 let closing_indent = if !block.closing_brace_leading_whitespace.is_empty() {
245 block.closing_brace_leading_whitespace.clone()
246 } else if !self.leading_whitespace.is_empty() {
247 self.leading_whitespace.clone()
248 } else {
249 " ".repeat(indent)
250 };
251 output.push_str(&closing_indent);
252 output.push('}');
253 output.push_str(&block.trailing_whitespace);
254 } else {
255 output.push_str(&self.space_before_terminator);
256 output.push(';');
257 output.push_str(&self.trailing_whitespace);
258 }
259
260 if let Some(comment) = &self.trailing_comment {
261 output.push(' ');
262 output.push_str(&comment.text);
263 }
264
265 output.push('\n');
266 }
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
275pub struct Block {
276 pub items: Vec<ConfigItem>,
278 pub span: Span,
280 pub raw_content: Option<String>,
282 #[serde(default)]
284 pub closing_brace_leading_whitespace: String,
285 #[serde(default)]
287 pub trailing_whitespace: String,
288}
289
290impl Block {
291 pub fn directives(&self) -> impl Iterator<Item = &Directive> {
293 self.items.iter().filter_map(|item| match item {
294 ConfigItem::Directive(d) => Some(d.as_ref()),
295 _ => None,
296 })
297 }
298
299 pub fn is_raw(&self) -> bool {
301 self.raw_content.is_some()
302 }
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct Argument {
311 pub value: ArgumentValue,
313 pub span: Span,
315 pub raw: String,
317}
318
319impl Argument {
320 pub fn as_str(&self) -> &str {
322 match &self.value {
323 ArgumentValue::Literal(s) => s,
324 ArgumentValue::QuotedString(s) => s,
325 ArgumentValue::SingleQuotedString(s) => s,
326 ArgumentValue::Variable(s) => s,
327 }
328 }
329
330 pub fn is_on(&self) -> bool {
332 self.as_str() == "on"
333 }
334
335 pub fn is_off(&self) -> bool {
337 self.as_str() == "off"
338 }
339
340 pub fn is_variable(&self) -> bool {
342 matches!(self.value, ArgumentValue::Variable(_))
343 }
344
345 pub fn is_quoted(&self) -> bool {
347 matches!(
348 self.value,
349 ArgumentValue::QuotedString(_) | ArgumentValue::SingleQuotedString(_)
350 )
351 }
352
353 pub fn is_literal(&self) -> bool {
355 matches!(self.value, ArgumentValue::Literal(_))
356 }
357
358 pub fn is_double_quoted(&self) -> bool {
360 matches!(self.value, ArgumentValue::QuotedString(_))
361 }
362
363 pub fn is_single_quoted(&self) -> bool {
365 matches!(self.value, ArgumentValue::SingleQuotedString(_))
366 }
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub enum ArgumentValue {
372 Literal(String),
374 QuotedString(String),
376 SingleQuotedString(String),
378 Variable(String),
380}
381
382pub struct AllDirectives<'a> {
386 stack: Vec<std::slice::Iter<'a, ConfigItem>>,
387}
388
389impl<'a> AllDirectives<'a> {
390 fn new(items: &'a [ConfigItem]) -> Self {
391 Self {
392 stack: vec![items.iter()],
393 }
394 }
395}
396
397impl<'a> Iterator for AllDirectives<'a> {
398 type Item = &'a Directive;
399
400 fn next(&mut self) -> Option<Self::Item> {
401 while let Some(iter) = self.stack.last_mut() {
402 if let Some(item) = iter.next() {
403 if let ConfigItem::Directive(directive) = item {
404 if let Some(block) = &directive.block {
406 self.stack.push(block.items.iter());
407 }
408 return Some(directive.as_ref());
409 }
410 } else {
412 self.stack.pop();
414 }
415 }
416 None
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_all_directives_iterator() {
426 let config = Config {
427 items: vec![
428 ConfigItem::Directive(Box::new(Directive {
429 name: "worker_processes".to_string(),
430 name_span: Span::default(),
431 args: vec![Argument {
432 value: ArgumentValue::Literal("auto".to_string()),
433 span: Span::default(),
434 raw: "auto".to_string(),
435 }],
436 block: None,
437 span: Span::default(),
438 trailing_comment: None,
439 leading_whitespace: String::new(),
440 space_before_terminator: String::new(),
441 trailing_whitespace: String::new(),
442 })),
443 ConfigItem::Directive(Box::new(Directive {
444 name: "http".to_string(),
445 name_span: Span::default(),
446 args: vec![],
447 block: Some(Block {
448 items: vec![ConfigItem::Directive(Box::new(Directive {
449 name: "server".to_string(),
450 name_span: Span::default(),
451 args: vec![],
452 block: Some(Block {
453 items: vec![ConfigItem::Directive(Box::new(Directive {
454 name: "listen".to_string(),
455 name_span: Span::default(),
456 args: vec![Argument {
457 value: ArgumentValue::Literal("80".to_string()),
458 span: Span::default(),
459 raw: "80".to_string(),
460 }],
461 block: None,
462 span: Span::default(),
463 trailing_comment: None,
464 leading_whitespace: String::new(),
465 space_before_terminator: String::new(),
466 trailing_whitespace: String::new(),
467 }))],
468 span: Span::default(),
469 raw_content: None,
470 closing_brace_leading_whitespace: String::new(),
471 trailing_whitespace: String::new(),
472 }),
473 span: Span::default(),
474 trailing_comment: None,
475 leading_whitespace: String::new(),
476 space_before_terminator: String::new(),
477 trailing_whitespace: String::new(),
478 }))],
479 span: Span::default(),
480 raw_content: None,
481 closing_brace_leading_whitespace: String::new(),
482 trailing_whitespace: String::new(),
483 }),
484 span: Span::default(),
485 trailing_comment: None,
486 leading_whitespace: String::new(),
487 space_before_terminator: String::new(),
488 trailing_whitespace: String::new(),
489 })),
490 ],
491 include_context: Vec::new(),
492 };
493
494 let names: Vec<&str> = config.all_directives().map(|d| d.name.as_str()).collect();
495 assert_eq!(names, vec!["worker_processes", "http", "server", "listen"]);
496 }
497
498 #[test]
499 fn test_directive_helpers() {
500 let directive = Directive {
501 name: "server_tokens".to_string(),
502 name_span: Span::default(),
503 args: vec![Argument {
504 value: ArgumentValue::Literal("on".to_string()),
505 span: Span::default(),
506 raw: "on".to_string(),
507 }],
508 block: None,
509 span: Span::default(),
510 trailing_comment: None,
511 leading_whitespace: String::new(),
512 space_before_terminator: String::new(),
513 trailing_whitespace: String::new(),
514 };
515
516 assert!(directive.is("server_tokens"));
517 assert!(!directive.is("gzip"));
518 assert_eq!(directive.first_arg(), Some("on"));
519 assert!(directive.first_arg_is("on"));
520 assert!(directive.args[0].is_on());
521 assert!(!directive.args[0].is_off());
522 }
523}