use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Position {
pub line: usize,
pub column: usize,
pub offset: usize,
}
impl Position {
pub fn new(line: usize, column: usize, offset: usize) -> Self {
Self {
line,
column,
offset,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct Span {
pub start: Position,
pub end: Position,
}
impl Span {
pub fn new(start: Position, end: Position) -> Self {
Self { start, end }
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
pub items: Vec<ConfigItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub include_context: Vec<String>,
}
impl Config {
pub fn new() -> Self {
Self {
items: Vec::new(),
include_context: Vec::new(),
}
}
pub fn directives(&self) -> impl Iterator<Item = &Directive> {
self.items.iter().filter_map(|item| match item {
ConfigItem::Directive(d) => Some(d.as_ref()),
_ => None,
})
}
pub fn all_directives(&self) -> AllDirectives<'_> {
AllDirectives::new(&self.items)
}
pub fn all_directives_with_context(&self) -> crate::context::AllDirectivesWithContextIter<'_> {
crate::context::AllDirectivesWithContextIter::new(&self.items, self.include_context.clone())
}
pub fn is_included_from(&self, context: &str) -> bool {
self.include_context.iter().any(|c| c == context)
}
pub fn is_included_from_http(&self) -> bool {
self.is_included_from("http")
}
pub fn is_included_from_http_server(&self) -> bool {
let ctx = &self.include_context;
ctx.iter().any(|c| c == "http")
&& ctx.iter().any(|c| c == "server")
&& ctx.iter().position(|c| c == "http") < ctx.iter().position(|c| c == "server")
}
pub fn is_included_from_http_location(&self) -> bool {
let ctx = &self.include_context;
ctx.iter().any(|c| c == "http")
&& ctx.iter().any(|c| c == "location")
&& ctx.iter().position(|c| c == "http") < ctx.iter().position(|c| c == "location")
}
pub fn is_included_from_stream(&self) -> bool {
self.is_included_from("stream")
}
pub fn immediate_parent_context(&self) -> Option<&str> {
self.include_context.last().map(|s| s.as_str())
}
pub fn to_source(&self) -> String {
let mut output = String::new();
for item in &self.items {
item.write_source(&mut output, 0);
}
output
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConfigItem {
Directive(Box<Directive>),
Comment(Comment),
BlankLine(BlankLine),
}
impl ConfigItem {
fn write_source(&self, output: &mut String, indent: usize) {
match self {
ConfigItem::Directive(d) => d.write_source(output, indent),
ConfigItem::Comment(c) => {
output.push_str(&c.leading_whitespace);
output.push_str(&c.text);
output.push_str(&c.trailing_whitespace);
output.push('\n');
}
ConfigItem::BlankLine(b) => {
output.push_str(&b.content);
output.push('\n');
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlankLine {
pub span: Span,
#[serde(default)]
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Comment {
pub text: String, pub span: Span,
#[serde(default)]
pub leading_whitespace: String,
#[serde(default)]
pub trailing_whitespace: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Directive {
pub name: String,
pub name_span: Span,
pub args: Vec<Argument>,
pub block: Option<Block>,
pub span: Span,
pub trailing_comment: Option<Comment>,
#[serde(default)]
pub leading_whitespace: String,
#[serde(default)]
pub space_before_terminator: String,
#[serde(default)]
pub trailing_whitespace: String,
}
impl Directive {
pub fn is(&self, name: &str) -> bool {
self.name == name
}
pub fn first_arg(&self) -> Option<&str> {
self.args.first().map(|a| a.as_str())
}
pub fn first_arg_is(&self, value: &str) -> bool {
self.first_arg() == Some(value)
}
fn write_source(&self, output: &mut String, indent: usize) {
let indent_str = if !self.leading_whitespace.is_empty() {
self.leading_whitespace.clone()
} else {
" ".repeat(indent)
};
output.push_str(&indent_str);
output.push_str(&self.name);
for arg in &self.args {
output.push(' ');
output.push_str(&arg.raw);
}
if let Some(block) = &self.block {
output.push_str(&self.space_before_terminator);
output.push('{');
output.push_str(&self.trailing_whitespace);
output.push('\n');
for item in &block.items {
item.write_source(output, indent + 1);
}
let closing_indent = if !block.closing_brace_leading_whitespace.is_empty() {
block.closing_brace_leading_whitespace.clone()
} else if !self.leading_whitespace.is_empty() {
self.leading_whitespace.clone()
} else {
" ".repeat(indent)
};
output.push_str(&closing_indent);
output.push('}');
output.push_str(&block.trailing_whitespace);
} else {
output.push_str(&self.space_before_terminator);
output.push(';');
output.push_str(&self.trailing_whitespace);
}
if let Some(comment) = &self.trailing_comment {
output.push(' ');
output.push_str(&comment.text);
}
output.push('\n');
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Block {
pub items: Vec<ConfigItem>,
pub span: Span,
pub raw_content: Option<String>,
#[serde(default)]
pub closing_brace_leading_whitespace: String,
#[serde(default)]
pub trailing_whitespace: String,
}
impl Block {
pub fn directives(&self) -> impl Iterator<Item = &Directive> {
self.items.iter().filter_map(|item| match item {
ConfigItem::Directive(d) => Some(d.as_ref()),
_ => None,
})
}
pub fn is_raw(&self) -> bool {
self.raw_content.is_some()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Argument {
pub value: ArgumentValue,
pub span: Span,
pub raw: String,
}
impl Argument {
pub fn as_str(&self) -> &str {
match &self.value {
ArgumentValue::Literal(s) => s,
ArgumentValue::QuotedString(s) => s,
ArgumentValue::SingleQuotedString(s) => s,
ArgumentValue::Variable(s) => s,
}
}
pub fn is_on(&self) -> bool {
self.as_str() == "on"
}
pub fn is_off(&self) -> bool {
self.as_str() == "off"
}
pub fn is_variable(&self) -> bool {
matches!(self.value, ArgumentValue::Variable(_))
}
pub fn is_quoted(&self) -> bool {
matches!(
self.value,
ArgumentValue::QuotedString(_) | ArgumentValue::SingleQuotedString(_)
)
}
pub fn is_literal(&self) -> bool {
matches!(self.value, ArgumentValue::Literal(_))
}
pub fn is_double_quoted(&self) -> bool {
matches!(self.value, ArgumentValue::QuotedString(_))
}
pub fn is_single_quoted(&self) -> bool {
matches!(self.value, ArgumentValue::SingleQuotedString(_))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ArgumentValue {
Literal(String),
QuotedString(String),
SingleQuotedString(String),
Variable(String),
}
pub struct AllDirectives<'a> {
stack: Vec<std::slice::Iter<'a, ConfigItem>>,
}
impl<'a> AllDirectives<'a> {
fn new(items: &'a [ConfigItem]) -> Self {
Self {
stack: vec![items.iter()],
}
}
}
impl<'a> Iterator for AllDirectives<'a> {
type Item = &'a Directive;
fn next(&mut self) -> Option<Self::Item> {
while let Some(iter) = self.stack.last_mut() {
if let Some(item) = iter.next() {
if let ConfigItem::Directive(directive) = item {
if let Some(block) = &directive.block {
self.stack.push(block.items.iter());
}
return Some(directive.as_ref());
}
} else {
self.stack.pop();
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_directives_iterator() {
let config = Config {
items: vec![
ConfigItem::Directive(Box::new(Directive {
name: "worker_processes".to_string(),
name_span: Span::default(),
args: vec![Argument {
value: ArgumentValue::Literal("auto".to_string()),
span: Span::default(),
raw: "auto".to_string(),
}],
block: None,
span: Span::default(),
trailing_comment: None,
leading_whitespace: String::new(),
space_before_terminator: String::new(),
trailing_whitespace: String::new(),
})),
ConfigItem::Directive(Box::new(Directive {
name: "http".to_string(),
name_span: Span::default(),
args: vec![],
block: Some(Block {
items: vec![ConfigItem::Directive(Box::new(Directive {
name: "server".to_string(),
name_span: Span::default(),
args: vec![],
block: Some(Block {
items: vec![ConfigItem::Directive(Box::new(Directive {
name: "listen".to_string(),
name_span: Span::default(),
args: vec![Argument {
value: ArgumentValue::Literal("80".to_string()),
span: Span::default(),
raw: "80".to_string(),
}],
block: None,
span: Span::default(),
trailing_comment: None,
leading_whitespace: String::new(),
space_before_terminator: String::new(),
trailing_whitespace: String::new(),
}))],
span: Span::default(),
raw_content: None,
closing_brace_leading_whitespace: String::new(),
trailing_whitespace: String::new(),
}),
span: Span::default(),
trailing_comment: None,
leading_whitespace: String::new(),
space_before_terminator: String::new(),
trailing_whitespace: String::new(),
}))],
span: Span::default(),
raw_content: None,
closing_brace_leading_whitespace: String::new(),
trailing_whitespace: String::new(),
}),
span: Span::default(),
trailing_comment: None,
leading_whitespace: String::new(),
space_before_terminator: String::new(),
trailing_whitespace: String::new(),
})),
],
include_context: Vec::new(),
};
let names: Vec<&str> = config.all_directives().map(|d| d.name.as_str()).collect();
assert_eq!(names, vec!["worker_processes", "http", "server", "listen"]);
}
#[test]
fn test_directive_helpers() {
let directive = Directive {
name: "server_tokens".to_string(),
name_span: Span::default(),
args: vec![Argument {
value: ArgumentValue::Literal("on".to_string()),
span: Span::default(),
raw: "on".to_string(),
}],
block: None,
span: Span::default(),
trailing_comment: None,
leading_whitespace: String::new(),
space_before_terminator: String::new(),
trailing_whitespace: String::new(),
};
assert!(directive.is("server_tokens"));
assert!(!directive.is("gzip"));
assert_eq!(directive.first_arg(), Some("on"));
assert!(directive.first_arg_is("on"));
assert!(directive.args[0].is_on());
assert!(!directive.args[0].is_off());
}
}