1use serde::{Deserialize, Serialize};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
44pub struct Position {
45 pub line: usize,
47 pub column: usize,
49 pub offset: usize,
51}
52
53impl Position {
54 pub fn new(line: usize, column: usize, offset: usize) -> Self {
55 Self {
56 line,
57 column,
58 offset,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
67pub struct Span {
68 pub start: Position,
70 pub end: Position,
72}
73
74impl Span {
75 pub fn new(start: Position, end: Position) -> Self {
76 Self { start, end }
77 }
78}
79
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
86pub struct Config {
87 pub items: Vec<ConfigItem>,
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
92 pub include_context: Vec<String>,
93}
94
95impl Config {
96 pub fn new() -> Self {
97 Self {
98 items: Vec::new(),
99 include_context: Vec::new(),
100 }
101 }
102
103 pub fn directives(&self) -> impl Iterator<Item = &Directive> {
105 self.items.iter().filter_map(|item| match item {
106 ConfigItem::Directive(d) => Some(d.as_ref()),
107 _ => None,
108 })
109 }
110
111 pub fn all_directives(&self) -> AllDirectives<'_> {
113 AllDirectives::new(&self.items)
114 }
115
116 pub fn all_directives_with_context(&self) -> crate::context::AllDirectivesWithContextIter<'_> {
122 crate::context::AllDirectivesWithContextIter::new(&self.items, self.include_context.clone())
123 }
124
125 pub fn is_included_from(&self, context: &str) -> bool {
127 self.include_context.iter().any(|c| c == context)
128 }
129
130 pub fn is_included_from_http(&self) -> bool {
132 self.is_included_from("http")
133 }
134
135 pub fn is_included_from_http_server(&self) -> bool {
137 let ctx = &self.include_context;
138 ctx.iter().any(|c| c == "http")
139 && ctx.iter().any(|c| c == "server")
140 && ctx.iter().position(|c| c == "http") < ctx.iter().position(|c| c == "server")
141 }
142
143 pub fn is_included_from_http_location(&self) -> bool {
145 let ctx = &self.include_context;
146 ctx.iter().any(|c| c == "http")
147 && ctx.iter().any(|c| c == "location")
148 && ctx.iter().position(|c| c == "http") < ctx.iter().position(|c| c == "location")
149 }
150
151 pub fn is_included_from_stream(&self) -> bool {
153 self.is_included_from("stream")
154 }
155
156 pub fn immediate_parent_context(&self) -> Option<&str> {
158 self.include_context.last().map(|s| s.as_str())
159 }
160
161 pub fn to_source(&self) -> String {
163 let mut output = String::new();
164 for item in &self.items {
165 item.write_source(&mut output, 0);
166 }
167 output
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub enum ConfigItem {
174 Directive(Box<Directive>),
176 Comment(Comment),
178 BlankLine(BlankLine),
180}
181
182impl ConfigItem {
183 fn write_source(&self, output: &mut String, indent: usize) {
184 match self {
185 ConfigItem::Directive(d) => d.write_source(output, indent),
186 ConfigItem::Comment(c) => {
187 output.push_str(&c.leading_whitespace);
188 output.push_str(&c.text);
189 output.push_str(&c.trailing_whitespace);
190 output.push('\n');
191 }
192 ConfigItem::BlankLine(b) => {
193 output.push_str(&b.content);
194 output.push('\n');
195 }
196 }
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
202pub struct BlankLine {
203 pub span: Span,
204 #[serde(default)]
206 pub content: String,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct Comment {
212 pub text: String, pub span: Span,
214 #[serde(default)]
216 pub leading_whitespace: String,
217 #[serde(default)]
219 pub trailing_whitespace: String,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct Directive {
229 pub name: String,
231 pub name_span: Span,
233 pub args: Vec<Argument>,
235 pub block: Option<Block>,
237 pub span: Span,
239 pub trailing_comment: Option<Comment>,
241 #[serde(default)]
243 pub leading_whitespace: String,
244 #[serde(default)]
246 pub space_before_terminator: String,
247 #[serde(default)]
249 pub trailing_whitespace: String,
250}
251
252impl Directive {
253 pub fn is(&self, name: &str) -> bool {
255 self.name == name
256 }
257
258 pub fn first_arg(&self) -> Option<&str> {
260 self.args.first().map(|a| a.as_str())
261 }
262
263 pub fn first_arg_is(&self, value: &str) -> bool {
265 self.first_arg() == Some(value)
266 }
267
268 fn write_source(&self, output: &mut String, indent: usize) {
269 let indent_str = if !self.leading_whitespace.is_empty() {
271 self.leading_whitespace.clone()
272 } else {
273 " ".repeat(indent)
274 };
275 output.push_str(&indent_str);
276 output.push_str(&self.name);
277
278 for arg in &self.args {
279 output.push(' ');
280 output.push_str(&arg.raw);
281 }
282
283 if let Some(block) = &self.block {
284 output.push_str(&self.space_before_terminator);
285 output.push('{');
286 output.push_str(&self.trailing_whitespace);
287 output.push('\n');
288 for item in &block.items {
289 item.write_source(output, indent + 1);
290 }
291 let closing_indent = if !block.closing_brace_leading_whitespace.is_empty() {
293 block.closing_brace_leading_whitespace.clone()
294 } else if !self.leading_whitespace.is_empty() {
295 self.leading_whitespace.clone()
296 } else {
297 " ".repeat(indent)
298 };
299 output.push_str(&closing_indent);
300 output.push('}');
301 output.push_str(&block.trailing_whitespace);
302 } else {
303 output.push_str(&self.space_before_terminator);
304 output.push(';');
305 output.push_str(&self.trailing_whitespace);
306 }
307
308 if let Some(comment) = &self.trailing_comment {
309 output.push(' ');
310 output.push_str(&comment.text);
311 }
312
313 output.push('\n');
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct Block {
324 pub items: Vec<ConfigItem>,
326 pub span: Span,
328 pub raw_content: Option<String>,
330 #[serde(default)]
332 pub closing_brace_leading_whitespace: String,
333 #[serde(default)]
335 pub trailing_whitespace: String,
336}
337
338impl Block {
339 pub fn directives(&self) -> impl Iterator<Item = &Directive> {
341 self.items.iter().filter_map(|item| match item {
342 ConfigItem::Directive(d) => Some(d.as_ref()),
343 _ => None,
344 })
345 }
346
347 pub fn is_raw(&self) -> bool {
349 self.raw_content.is_some()
350 }
351}
352
353#[derive(Debug, Clone, Serialize, Deserialize)]
358pub struct Argument {
359 pub value: ArgumentValue,
361 pub span: Span,
363 pub raw: String,
365}
366
367impl Argument {
368 pub fn as_str(&self) -> &str {
370 match &self.value {
371 ArgumentValue::Literal(s) => s,
372 ArgumentValue::QuotedString(s) => s,
373 ArgumentValue::SingleQuotedString(s) => s,
374 ArgumentValue::Variable(s) => s,
375 }
376 }
377
378 pub fn is_on(&self) -> bool {
380 self.as_str() == "on"
381 }
382
383 pub fn is_off(&self) -> bool {
385 self.as_str() == "off"
386 }
387
388 pub fn is_variable(&self) -> bool {
390 matches!(self.value, ArgumentValue::Variable(_))
391 }
392
393 pub fn is_quoted(&self) -> bool {
395 matches!(
396 self.value,
397 ArgumentValue::QuotedString(_) | ArgumentValue::SingleQuotedString(_)
398 )
399 }
400
401 pub fn is_literal(&self) -> bool {
403 matches!(self.value, ArgumentValue::Literal(_))
404 }
405
406 pub fn is_double_quoted(&self) -> bool {
408 matches!(self.value, ArgumentValue::QuotedString(_))
409 }
410
411 pub fn is_single_quoted(&self) -> bool {
413 matches!(self.value, ArgumentValue::SingleQuotedString(_))
414 }
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
419pub enum ArgumentValue {
420 Literal(String),
422 QuotedString(String),
424 SingleQuotedString(String),
426 Variable(String),
428}
429
430pub struct AllDirectives<'a> {
434 stack: Vec<std::slice::Iter<'a, ConfigItem>>,
435}
436
437impl<'a> AllDirectives<'a> {
438 fn new(items: &'a [ConfigItem]) -> Self {
439 Self {
440 stack: vec![items.iter()],
441 }
442 }
443}
444
445impl<'a> Iterator for AllDirectives<'a> {
446 type Item = &'a Directive;
447
448 fn next(&mut self) -> Option<Self::Item> {
449 while let Some(iter) = self.stack.last_mut() {
450 if let Some(item) = iter.next() {
451 if let ConfigItem::Directive(directive) = item {
452 if let Some(block) = &directive.block {
454 self.stack.push(block.items.iter());
455 }
456 return Some(directive.as_ref());
457 }
458 } else {
460 self.stack.pop();
462 }
463 }
464 None
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471
472 #[test]
473 fn test_all_directives_iterator() {
474 let config = Config {
475 items: vec![
476 ConfigItem::Directive(Box::new(Directive {
477 name: "worker_processes".to_string(),
478 name_span: Span::default(),
479 args: vec![Argument {
480 value: ArgumentValue::Literal("auto".to_string()),
481 span: Span::default(),
482 raw: "auto".to_string(),
483 }],
484 block: None,
485 span: Span::default(),
486 trailing_comment: None,
487 leading_whitespace: String::new(),
488 space_before_terminator: String::new(),
489 trailing_whitespace: String::new(),
490 })),
491 ConfigItem::Directive(Box::new(Directive {
492 name: "http".to_string(),
493 name_span: Span::default(),
494 args: vec![],
495 block: Some(Block {
496 items: vec![ConfigItem::Directive(Box::new(Directive {
497 name: "server".to_string(),
498 name_span: Span::default(),
499 args: vec![],
500 block: Some(Block {
501 items: vec![ConfigItem::Directive(Box::new(Directive {
502 name: "listen".to_string(),
503 name_span: Span::default(),
504 args: vec![Argument {
505 value: ArgumentValue::Literal("80".to_string()),
506 span: Span::default(),
507 raw: "80".to_string(),
508 }],
509 block: None,
510 span: Span::default(),
511 trailing_comment: None,
512 leading_whitespace: String::new(),
513 space_before_terminator: String::new(),
514 trailing_whitespace: String::new(),
515 }))],
516 span: Span::default(),
517 raw_content: None,
518 closing_brace_leading_whitespace: String::new(),
519 trailing_whitespace: String::new(),
520 }),
521 span: Span::default(),
522 trailing_comment: None,
523 leading_whitespace: String::new(),
524 space_before_terminator: String::new(),
525 trailing_whitespace: String::new(),
526 }))],
527 span: Span::default(),
528 raw_content: None,
529 closing_brace_leading_whitespace: String::new(),
530 trailing_whitespace: String::new(),
531 }),
532 span: Span::default(),
533 trailing_comment: None,
534 leading_whitespace: String::new(),
535 space_before_terminator: String::new(),
536 trailing_whitespace: String::new(),
537 })),
538 ],
539 include_context: Vec::new(),
540 };
541
542 let names: Vec<&str> = config.all_directives().map(|d| d.name.as_str()).collect();
543 assert_eq!(names, vec!["worker_processes", "http", "server", "listen"]);
544 }
545
546 #[test]
547 fn test_directive_helpers() {
548 let directive = Directive {
549 name: "server_tokens".to_string(),
550 name_span: Span::default(),
551 args: vec![Argument {
552 value: ArgumentValue::Literal("on".to_string()),
553 span: Span::default(),
554 raw: "on".to_string(),
555 }],
556 block: None,
557 span: Span::default(),
558 trailing_comment: None,
559 leading_whitespace: String::new(),
560 space_before_terminator: String::new(),
561 trailing_whitespace: String::new(),
562 };
563
564 assert!(directive.is("server_tokens"));
565 assert!(!directive.is("gzip"));
566 assert_eq!(directive.first_arg(), Some("on"));
567 assert!(directive.first_arg_is("on"));
568 assert!(directive.args[0].is_on());
569 assert!(!directive.args[0].is_off());
570 }
571}