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 all_directives_with_context(&self) -> crate::context::AllDirectivesWithContextIter<'_> {
119 crate::context::AllDirectivesWithContextIter::new(&self.items, self.include_context.clone())
120 }
121
122 pub fn is_included_from(&self, context: &str) -> bool {
124 self.include_context.iter().any(|c| c == context)
125 }
126
127 pub fn is_included_from_http(&self) -> bool {
129 self.is_included_from("http")
130 }
131
132 pub fn is_included_from_http_server(&self) -> bool {
134 let ctx = &self.include_context;
135 ctx.iter().any(|c| c == "http")
136 && ctx.iter().any(|c| c == "server")
137 && ctx.iter().position(|c| c == "http") < ctx.iter().position(|c| c == "server")
138 }
139
140 pub fn is_included_from_http_location(&self) -> bool {
142 let ctx = &self.include_context;
143 ctx.iter().any(|c| c == "http")
144 && ctx.iter().any(|c| c == "location")
145 && ctx.iter().position(|c| c == "http") < ctx.iter().position(|c| c == "location")
146 }
147
148 pub fn is_included_from_stream(&self) -> bool {
150 self.is_included_from("stream")
151 }
152
153 pub fn immediate_parent_context(&self) -> Option<&str> {
155 self.include_context.last().map(|s| s.as_str())
156 }
157
158 pub fn to_source(&self) -> String {
160 let mut output = String::new();
161 for item in &self.items {
162 item.write_source(&mut output, 0);
163 }
164 output
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub enum ConfigItem {
171 Directive(Box<Directive>),
173 Comment(Comment),
175 BlankLine(BlankLine),
177}
178
179impl ConfigItem {
180 fn write_source(&self, output: &mut String, indent: usize) {
181 match self {
182 ConfigItem::Directive(d) => d.write_source(output, indent),
183 ConfigItem::Comment(c) => {
184 output.push_str(&c.leading_whitespace);
185 output.push_str(&c.text);
186 output.push_str(&c.trailing_whitespace);
187 output.push('\n');
188 }
189 ConfigItem::BlankLine(b) => {
190 output.push_str(&b.content);
191 output.push('\n');
192 }
193 }
194 }
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct BlankLine {
200 pub span: Span,
201 #[serde(default)]
203 pub content: String,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct Comment {
209 pub text: String, pub span: Span,
211 #[serde(default)]
213 pub leading_whitespace: String,
214 #[serde(default)]
216 pub trailing_whitespace: String,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct Directive {
226 pub name: String,
228 pub name_span: Span,
230 pub args: Vec<Argument>,
232 pub block: Option<Block>,
234 pub span: Span,
236 pub trailing_comment: Option<Comment>,
238 #[serde(default)]
240 pub leading_whitespace: String,
241 #[serde(default)]
243 pub space_before_terminator: String,
244 #[serde(default)]
246 pub trailing_whitespace: String,
247}
248
249impl Directive {
250 pub fn is(&self, name: &str) -> bool {
252 self.name == name
253 }
254
255 pub fn first_arg(&self) -> Option<&str> {
257 self.args.first().map(|a| a.as_str())
258 }
259
260 pub fn first_arg_is(&self, value: &str) -> bool {
262 self.first_arg() == Some(value)
263 }
264
265 fn write_source(&self, output: &mut String, indent: usize) {
266 let indent_str = if !self.leading_whitespace.is_empty() {
268 self.leading_whitespace.clone()
269 } else {
270 " ".repeat(indent)
271 };
272 output.push_str(&indent_str);
273 output.push_str(&self.name);
274
275 for arg in &self.args {
276 output.push(' ');
277 output.push_str(&arg.raw);
278 }
279
280 if let Some(block) = &self.block {
281 output.push_str(&self.space_before_terminator);
282 output.push('{');
283 output.push_str(&self.trailing_whitespace);
284 output.push('\n');
285 for item in &block.items {
286 item.write_source(output, indent + 1);
287 }
288 let closing_indent = if !block.closing_brace_leading_whitespace.is_empty() {
290 block.closing_brace_leading_whitespace.clone()
291 } else if !self.leading_whitespace.is_empty() {
292 self.leading_whitespace.clone()
293 } else {
294 " ".repeat(indent)
295 };
296 output.push_str(&closing_indent);
297 output.push('}');
298 output.push_str(&block.trailing_whitespace);
299 } else {
300 output.push_str(&self.space_before_terminator);
301 output.push(';');
302 output.push_str(&self.trailing_whitespace);
303 }
304
305 if let Some(comment) = &self.trailing_comment {
306 output.push(' ');
307 output.push_str(&comment.text);
308 }
309
310 output.push('\n');
311 }
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct Block {
321 pub items: Vec<ConfigItem>,
323 pub span: Span,
325 pub raw_content: Option<String>,
327 #[serde(default)]
329 pub closing_brace_leading_whitespace: String,
330 #[serde(default)]
332 pub trailing_whitespace: String,
333}
334
335impl Block {
336 pub fn directives(&self) -> impl Iterator<Item = &Directive> {
338 self.items.iter().filter_map(|item| match item {
339 ConfigItem::Directive(d) => Some(d.as_ref()),
340 _ => None,
341 })
342 }
343
344 pub fn is_raw(&self) -> bool {
346 self.raw_content.is_some()
347 }
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
355pub struct Argument {
356 pub value: ArgumentValue,
358 pub span: Span,
360 pub raw: String,
362}
363
364impl Argument {
365 pub fn as_str(&self) -> &str {
367 match &self.value {
368 ArgumentValue::Literal(s) => s,
369 ArgumentValue::QuotedString(s) => s,
370 ArgumentValue::SingleQuotedString(s) => s,
371 ArgumentValue::Variable(s) => s,
372 }
373 }
374
375 pub fn is_on(&self) -> bool {
377 self.as_str() == "on"
378 }
379
380 pub fn is_off(&self) -> bool {
382 self.as_str() == "off"
383 }
384
385 pub fn is_variable(&self) -> bool {
387 matches!(self.value, ArgumentValue::Variable(_))
388 }
389
390 pub fn is_quoted(&self) -> bool {
392 matches!(
393 self.value,
394 ArgumentValue::QuotedString(_) | ArgumentValue::SingleQuotedString(_)
395 )
396 }
397
398 pub fn is_literal(&self) -> bool {
400 matches!(self.value, ArgumentValue::Literal(_))
401 }
402
403 pub fn is_double_quoted(&self) -> bool {
405 matches!(self.value, ArgumentValue::QuotedString(_))
406 }
407
408 pub fn is_single_quoted(&self) -> bool {
410 matches!(self.value, ArgumentValue::SingleQuotedString(_))
411 }
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize)]
416pub enum ArgumentValue {
417 Literal(String),
419 QuotedString(String),
421 SingleQuotedString(String),
423 Variable(String),
425}
426
427pub struct AllDirectives<'a> {
431 stack: Vec<std::slice::Iter<'a, ConfigItem>>,
432}
433
434impl<'a> AllDirectives<'a> {
435 fn new(items: &'a [ConfigItem]) -> Self {
436 Self {
437 stack: vec![items.iter()],
438 }
439 }
440}
441
442impl<'a> Iterator for AllDirectives<'a> {
443 type Item = &'a Directive;
444
445 fn next(&mut self) -> Option<Self::Item> {
446 while let Some(iter) = self.stack.last_mut() {
447 if let Some(item) = iter.next() {
448 if let ConfigItem::Directive(directive) = item {
449 if let Some(block) = &directive.block {
451 self.stack.push(block.items.iter());
452 }
453 return Some(directive.as_ref());
454 }
455 } else {
457 self.stack.pop();
459 }
460 }
461 None
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 #[test]
470 fn test_all_directives_iterator() {
471 let config = Config {
472 items: vec![
473 ConfigItem::Directive(Box::new(Directive {
474 name: "worker_processes".to_string(),
475 name_span: Span::default(),
476 args: vec![Argument {
477 value: ArgumentValue::Literal("auto".to_string()),
478 span: Span::default(),
479 raw: "auto".to_string(),
480 }],
481 block: None,
482 span: Span::default(),
483 trailing_comment: None,
484 leading_whitespace: String::new(),
485 space_before_terminator: String::new(),
486 trailing_whitespace: String::new(),
487 })),
488 ConfigItem::Directive(Box::new(Directive {
489 name: "http".to_string(),
490 name_span: Span::default(),
491 args: vec![],
492 block: Some(Block {
493 items: vec![ConfigItem::Directive(Box::new(Directive {
494 name: "server".to_string(),
495 name_span: Span::default(),
496 args: vec![],
497 block: Some(Block {
498 items: vec![ConfigItem::Directive(Box::new(Directive {
499 name: "listen".to_string(),
500 name_span: Span::default(),
501 args: vec![Argument {
502 value: ArgumentValue::Literal("80".to_string()),
503 span: Span::default(),
504 raw: "80".to_string(),
505 }],
506 block: None,
507 span: Span::default(),
508 trailing_comment: None,
509 leading_whitespace: String::new(),
510 space_before_terminator: String::new(),
511 trailing_whitespace: String::new(),
512 }))],
513 span: Span::default(),
514 raw_content: None,
515 closing_brace_leading_whitespace: String::new(),
516 trailing_whitespace: String::new(),
517 }),
518 span: Span::default(),
519 trailing_comment: None,
520 leading_whitespace: String::new(),
521 space_before_terminator: String::new(),
522 trailing_whitespace: String::new(),
523 }))],
524 span: Span::default(),
525 raw_content: None,
526 closing_brace_leading_whitespace: String::new(),
527 trailing_whitespace: String::new(),
528 }),
529 span: Span::default(),
530 trailing_comment: None,
531 leading_whitespace: String::new(),
532 space_before_terminator: String::new(),
533 trailing_whitespace: String::new(),
534 })),
535 ],
536 include_context: Vec::new(),
537 };
538
539 let names: Vec<&str> = config.all_directives().map(|d| d.name.as_str()).collect();
540 assert_eq!(names, vec!["worker_processes", "http", "server", "listen"]);
541 }
542
543 #[test]
544 fn test_directive_helpers() {
545 let directive = Directive {
546 name: "server_tokens".to_string(),
547 name_span: Span::default(),
548 args: vec![Argument {
549 value: ArgumentValue::Literal("on".to_string()),
550 span: Span::default(),
551 raw: "on".to_string(),
552 }],
553 block: None,
554 span: Span::default(),
555 trailing_comment: None,
556 leading_whitespace: String::new(),
557 space_before_terminator: String::new(),
558 trailing_whitespace: String::new(),
559 };
560
561 assert!(directive.is("server_tokens"));
562 assert!(!directive.is("gzip"));
563 assert_eq!(directive.first_arg(), Some("on"));
564 assert!(directive.first_arg_is("on"));
565 assert!(directive.args[0].is_on());
566 assert!(!directive.args[0].is_off());
567 }
568}