//! Nginx-style configuration parser.
//!
//! Parses configuration text into an AST of [`Block`]s and [`Directive`]s.
//!
//! # Grammar (simplified)
//!
//! ```text
//! config = directive*
//! directive = name arg* (';' | '{' directive* '}')
//! name = word
//! arg = word | string | number | size | time
//! ```
//!
//! # Suffix conventions
//!
//! | Suffix | Category | Meaning |
//! |--------|----------|----------------------|
//! | `k` | Size | Kilobytes (x1024) |
//! | `m` | Size | Megabytes (x1024^2) |
//! | `g` | Size | Gigabytes (x1024^3) |
//! | `ms` | Time | Milliseconds |
//! | `s` | Time | Seconds (x1000) |
//! | `h` | Time | Hours (x3600000) |
//! | `d` | Time | Days (x86400000) |
//!
//! The `m` suffix is ambiguous in nginx configs -- it can mean either "minutes"
//! (time) or "megabytes" (size). This parser resolves `m` as **megabytes**
//! because that is the overwhelmingly more common usage (e.g. `client_max_body_size 10m`).
//! For time values in minutes, express the value in seconds instead (e.g. `1800s`
//! rather than `30m`).
use super::ast::{Block, Directive, Value};
// ---------------------------------------------------------------------------
// Error type
// ---------------------------------------------------------------------------
/// A configuration parse error with a line number for diagnostics.
#[derive(Debug, Clone)]
pub struct ConfigError {
pub line: usize,
pub message: String,
}
impl ConfigError {
fn new(line: usize, message: impl Into<String>) -> Self {
Self {
line,
message: message.into(),
}
}
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "config error at line {}: {}", self.line, self.message)
}
}
impl std::error::Error for ConfigError {}
// ---------------------------------------------------------------------------
// Token (internal to the lexer / parser)
// ---------------------------------------------------------------------------
/// A single lexical token produced by the [`Lexer`].
///
/// Each variant carries its source line number so that later stages can
/// produce meaningful error messages.
#[derive(Debug, Clone)]
enum Token {
/// An unquoted word (directive name, bare identifier, etc.).
Word(String, usize),
/// A double-quoted string whose escape sequences have already been resolved.
String(String, usize),
/// A plain integer literal (no suffix).
Integer(i64, usize),
/// A plain floating-point literal (no suffix).
Float(f64, usize),
/// A number with a size suffix (`k`, `m`, `g`). Value is in **bytes**.
Size(u64, usize),
/// A number with a time suffix (`ms`, `s`, `h`, `d`). Value is in **milliseconds**.
Time(u64, usize),
/// `{`
LBrace(usize),
/// `}`
RBrace(usize),
/// `;`
Semicolon(usize),
}
impl Token {
/// Return the source line where this token was produced.
fn line(&self) -> usize {
match self {
Token::Word(_, l)
| Token::String(_, l)
| Token::Integer(_, l)
| Token::Float(_, l)
| Token::Size(_, l)
| Token::Time(_, l)
| Token::LBrace(l)
| Token::RBrace(l)
| Token::Semicolon(l) => *l,
}
}
}
// ---------------------------------------------------------------------------
// Lexer
// ---------------------------------------------------------------------------
/// Character-level lexer that turns raw configuration text into a flat stream
/// of [`Token`]s.
///
/// The lexer handles:
/// - `#` line comments (everything from `#` to end-of-line is discarded)
/// - Double-quoted strings with `\n`, `\r`, `\t`, `\\`, `\"` escapes
/// - Bare words (any contiguous run of non-special, non-whitespace characters)
/// - Numeric literals with optional size/time suffixes
/// - Single-character punctuation: `{`, `}`, `;`
struct Lexer {
chars: Vec<char>,
pos: usize,
line: usize,
}
impl Lexer {
fn new(input: &str) -> Self {
Self {
chars: input.chars().collect(),
pos: 0,
line: 1,
}
}
// -- character-level helpers -------------------------------------------
/// Peek at the current character without consuming it.
fn peek(&self) -> Option<char> {
self.chars.get(self.pos).copied()
}
/// Advance past the current character and return it.
/// Increments the line counter when a newline is consumed.
fn advance(&mut self) -> Option<char> {
let ch = self.chars.get(self.pos).copied();
if ch == Some('\n') {
self.line += 1;
}
self.pos += 1;
ch
}
/// Skip all contiguous whitespace characters.
fn skip_whitespace(&mut self) {
while let Some(ch) = self.peek() {
if ch.is_whitespace() {
self.advance();
} else {
break;
}
}
}
/// Skip a `#` line comment. The `#` has already been consumed.
fn skip_comment(&mut self) {
while let Some(ch) = self.peek() {
if ch == '\n' {
break;
}
self.advance();
}
}
// -- token readers -----------------------------------------------------
/// Read a double-quoted string literal. The opening `"` has already been
/// consumed. Supports the following escape sequences:
///
/// | Sequence | Replacement |
/// |----------|-------------|
/// | `\n` | newline |
/// | `\r` | carriage return |
/// | `\t` | tab |
/// | `\\` | backslash |
/// | `\"` | double quote |
fn read_string(&mut self) -> Result<String, ConfigError> {
let mut s = String::new();
loop {
match self.advance() {
None => return Err(ConfigError::new(self.line, "unterminated string literal")),
Some('"') => return Ok(s),
Some('\\') => {
// Escape sequence
match self.advance() {
Some('n') => s.push('\n'),
Some('r') => s.push('\r'),
Some('t') => s.push('\t'),
Some('\\') => s.push('\\'),
Some('"') => s.push('"'),
Some(other) => {
// Unknown escape -- pass through literally so that
// the user sees the raw backslash + character.
s.push('\\');
s.push(other);
}
None => {
return Err(ConfigError::new(
self.line,
"unterminated escape sequence in string literal",
))
}
}
}
Some(ch) => s.push(ch),
}
}
}
/// Read a bare word or number (anything not whitespace, not a comment
/// character, not a brace, not a semicolon, not a quote).
///
/// After reading the raw text, the lexer attempts to interpret it as a
/// numeric literal with an optional size/time suffix.
fn read_word_or_number(&mut self) -> Result<Token, ConfigError> {
let start_line = self.line;
let mut buf = String::new();
while let Some(ch) = self.peek() {
// A word terminates at whitespace or any structural character.
if ch.is_whitespace() || ch == '{' || ch == '}' || ch == ';' || ch == '#' || ch == '"' {
break;
}
buf.push(ch);
self.advance();
}
if buf.is_empty() {
return Err(ConfigError::new(start_line, "unexpected end of input"));
}
// Try to interpret the raw text as a number (possibly with a suffix).
if let Some(token) = try_parse_number(&buf, start_line)? {
return Ok(token);
}
// Fall back to a plain word.
Ok(Token::Word(buf, start_line))
}
// -- public entry point ------------------------------------------------
/// Produce a flat vector of tokens from the input text.
fn tokenize(&mut self) -> Result<Vec<Token>, ConfigError> {
let mut tokens = Vec::new();
loop {
self.skip_whitespace();
match self.peek() {
// End of input
None => break,
// Line comment -- skip to end of line
Some('#') => {
self.advance();
self.skip_comment();
}
// Punctuation
Some('{') => {
tokens.push(Token::LBrace(self.line));
self.advance();
}
Some('}') => {
tokens.push(Token::RBrace(self.line));
self.advance();
}
Some(';') => {
tokens.push(Token::Semicolon(self.line));
self.advance();
}
// Quoted string
Some('"') => {
let line = self.line;
self.advance(); // consume opening "
let value = self.read_string()?;
tokens.push(Token::String(value, line));
}
// Everything else: word or number (possibly with suffix).
Some(_) => {
tokens.push(self.read_word_or_number()?);
}
}
}
Ok(tokens)
}
}
// ---------------------------------------------------------------------------
// Numeric / suffix helpers
// ---------------------------------------------------------------------------
/// Size multipliers (bytes).
const SIZE_K: u64 = 1024;
const SIZE_M: u64 = SIZE_K * 1024;
const SIZE_G: u64 = SIZE_M * 1024;
/// Time multipliers (milliseconds).
const TIME_MS: u64 = 1;
const TIME_S: u64 = 1000;
const TIME_H: u64 = 3600 * 1000;
const TIME_D: u64 = 86400 * 1000;
/// Try to parse a raw word as a number with an optional suffix.
///
/// Returns `Ok(None)` when the word does not look like a number at all (the
/// caller should then treat it as a plain `Word` token).
///
/// Resolution order (longest suffix match first):
///
/// 1. `ms` -- time (milliseconds)
/// 2. `k` -- size (kilobytes)
/// 3. `m` -- size (megabytes)
/// 4. `g` -- size (gigabytes)
/// 5. `s` -- time (seconds)
/// 6. `h` -- time (hours)
/// 7. `d` -- time (days)
/// 8. (no suffix) -- plain integer or float
///
/// The `m` suffix is assigned to **size** rather than time because that is its
/// dominant usage in real nginx configs (`client_max_body_size`, `proxy_buffer_size`,
/// etc.). See the module-level documentation for details.
fn try_parse_number(word: &str, line: usize) -> Result<Option<Token>, ConfigError> {
// Quick check: if the word doesn't start with a digit or '-', it's not a number.
if !word.starts_with(|c: char| c.is_ascii_digit() || c == '-') {
return Ok(None);
}
// Helper: parse the numeric prefix (everything before the suffix).
let parse_u64_prefix = |prefix: &str| -> Result<u64, ConfigError> {
prefix
.parse::<u64>()
.map_err(|_| ConfigError::new(line, format!("invalid number: '{}'", word)))
};
// 1. Two-character time suffix: "ms"
if let Some(prefix) = word.strip_suffix("ms") {
let n = parse_u64_prefix(prefix)?;
return Ok(Some(Token::Time(n * TIME_MS, line)));
}
// 2. Single-character size suffixes: k, m, g
if let Some(prefix) = word.strip_suffix('k') {
let n = parse_u64_prefix(prefix)?;
return Ok(Some(Token::Size(n * SIZE_K, line)));
}
if let Some(prefix) = word.strip_suffix('m') {
let n = parse_u64_prefix(prefix)?;
return Ok(Some(Token::Size(n * SIZE_M, line)));
}
if let Some(prefix) = word.strip_suffix('g') {
let n = parse_u64_prefix(prefix)?;
return Ok(Some(Token::Size(n * SIZE_G, line)));
}
// 3. Single-character time suffixes: s, h, d
if let Some(prefix) = word.strip_suffix('s') {
let n = parse_u64_prefix(prefix)?;
return Ok(Some(Token::Time(n * TIME_S, line)));
}
if let Some(prefix) = word.strip_suffix('h') {
let n = parse_u64_prefix(prefix)?;
return Ok(Some(Token::Time(n * TIME_H, line)));
}
if let Some(prefix) = word.strip_suffix('d') {
let n = parse_u64_prefix(prefix)?;
return Ok(Some(Token::Time(n * TIME_D, line)));
}
// 4. No suffix -- try plain integer or float.
// Use i128 during parsing so we can detect overflow gracefully, then
// narrow to i64.
if let Ok(n) = word.parse::<i64>() {
return Ok(Some(Token::Integer(n, line)));
}
if let Ok(f) = word.parse::<f64>() {
// Reject values that are not finite (NaN, Inf).
if f.is_finite() {
return Ok(Some(Token::Float(f, line)));
}
return Err(ConfigError::new(
line,
format!("non-finite float literal: '{}'", word),
));
}
// Not a number at all -- the caller will treat it as a Word.
Ok(None)
}
/// Convenience: convert a raw word string directly into a [`Value`].
///
/// This is used during parsing when a directive argument arrives as a plain
/// `Word` token (e.g. `on`, `off`, or a suffixed number that the lexer did
/// not pre-classify -- though currently the lexer handles all numeric forms).
///
/// Falls back to [`Value::String`] when no numeric or boolean interpretation
/// applies.
fn word_to_value(word: &str, line: usize) -> Result<Value, ConfigError> {
// Boolean keywords.
match word {
"on" | "true" | "yes" => return Ok(Value::Bool(true)),
"off" | "false" | "no" => return Ok(Value::Bool(false)),
_ => {}
}
// Delegate to the same suffix logic used by the lexer.
if let Some(token) = try_parse_number(word, line)? {
return match token {
Token::Integer(n, _) => Ok(Value::Number(n)),
Token::Float(f, _) => Ok(Value::Float(f)),
Token::Size(bytes, _) => Ok(Value::Size(bytes)),
Token::Time(ms, _) => Ok(Value::Time(ms)),
_ => unreachable!("try_parse_number only returns numeric token variants"),
};
}
Ok(Value::String(word.to_owned()))
}
/// Convert a [`Token`] into a [`Value`].
///
/// Quoted strings and numeric tokens map directly; `Word` tokens are resolved
/// through [`word_to_value`].
fn token_to_value(token: &Token) -> Result<Value, ConfigError> {
match token {
Token::String(s, _) => Ok(Value::String(s.clone())),
Token::Integer(n, _) => Ok(Value::Number(*n)),
Token::Float(f, _) => Ok(Value::Float(*f)),
Token::Size(bytes, _) => Ok(Value::Size(*bytes)),
Token::Time(ms, _) => Ok(Value::Time(*ms)),
Token::Word(w, line) => word_to_value(w, *line),
Token::LBrace(line) => Err(ConfigError::new(*line, "unexpected '{'")),
Token::RBrace(line) => Err(ConfigError::new(*line, "unexpected '}'")),
Token::Semicolon(line) => Err(ConfigError::new(*line, "unexpected ';'")),
}
}
// ---------------------------------------------------------------------------
// Parser
// ---------------------------------------------------------------------------
/// Recursive-descent parser that consumes a flat token stream and produces an
/// AST rooted at a [`Block`].
struct Parser {
tokens: Vec<Token>,
pos: usize,
}
impl Parser {
fn new(tokens: Vec<Token>) -> Self {
Self { tokens, pos: 0 }
}
// -- helpers -----------------------------------------------------------
/// Peek at the current token without consuming it.
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.pos)
}
/// Advance past the current token and return a reference to it.
fn advance(&mut self) -> Option<&Token> {
let tok = self.tokens.get(self.pos);
self.pos += 1;
tok
}
/// Return `true` when every token has been consumed.
fn is_eof(&self) -> bool {
self.pos >= self.tokens.len()
}
/// Consume and return the current token, or produce an error with the
/// given message using the line number of the previous token (or 1 if at
/// the very start).
fn expect_advance(&mut self, what: &str) -> Result<&Token, ConfigError> {
if self.pos < self.tokens.len() {
let tok = &self.tokens[self.pos];
self.pos += 1;
Ok(tok)
} else {
let line = self.tokens.last().map(|t| t.line()).unwrap_or(1);
Err(ConfigError::new(
line,
format!("expected {}, got end of input", what),
))
}
}
// -- grammar rules -----------------------------------------------------
/// Parse the entire token stream into a top-level [`Block`].
///
/// ```text
/// config = directive*
/// ```
fn parse_config(&mut self) -> Result<Block, ConfigError> {
let mut block = Block::new();
while !self.is_eof() {
block.directives.push(self.parse_directive()?);
}
Ok(block)
}
/// Parse a single directive, which may optionally be followed by a block.
///
/// ```text
/// directive = name arg* (';' | '{' directive* '}')
/// ```
fn parse_directive(&mut self) -> Result<Directive, ConfigError> {
// -- name ----------------------------------------------------------
let name_token = self.expect_advance("directive name")?;
let (name, line) = match name_token {
Token::Word(w, l) => (w.clone(), *l),
Token::String(s, l) => (s.clone(), *l),
// A stray closing brace has no matching block to terminate.
Token::RBrace(l) => return Err(ConfigError::new(*l, "unexpected '}'")),
other => {
return Err(ConfigError::new(
other.line(),
format!(
"expected directive name, found '{}'",
token_debug_str(other)
),
))
}
};
// -- arguments (everything up to ';' or '{') -----------------------
let mut args: Vec<Value> = Vec::new();
loop {
match self.peek() {
// End of input -- implicit semicolon at EOF.
None => break,
// Semicolon terminates a simple directive (no block).
Some(Token::Semicolon(_)) => {
self.advance();
return Ok(Directive::new(name, args, line));
}
// Opening brace starts a block directive.
Some(Token::LBrace(_)) => {
break;
}
// Closing brace without a matching open is always an error.
Some(Token::RBrace(brace_line)) => {
return Err(ConfigError::new(
*brace_line,
"unexpected '}' without matching '{'",
))
}
// Anything else is an argument.
Some(tok) => {
let tok_clone = tok.clone();
self.advance();
args.push(token_to_value(&tok_clone)?);
}
}
}
// -- optional block ------------------------------------------------
match self.peek() {
Some(Token::LBrace(_)) => {
self.advance(); // consume '{'
let block = self.parse_block_body()?;
// Expect closing '}'
match self.advance() {
Some(Token::RBrace(_)) => {}
Some(other) => {
return Err(ConfigError::new(
other.line(),
format!(
"expected '}}' to close block opened on line {}, found '{}'",
line,
token_debug_str(other)
),
))
}
None => {
return Err(ConfigError::new(
line,
"unterminated block: expected '}' before end of input",
))
}
}
Ok(Directive::new(name, args, line).with_block(block))
}
_ => {
// No block -- this is a simple directive.
Ok(Directive::new(name, args, line))
}
}
}
/// Parse the interior of a `{ ... }` block (exclusive of the braces
/// themselves, which are consumed by the caller).
fn parse_block_body(&mut self) -> Result<Block, ConfigError> {
let mut block = Block::new();
while let Some(tok) = self.peek() {
// A closing brace ends the block; the caller will consume it.
if matches!(tok, Token::RBrace(_)) {
break;
}
block.directives.push(self.parse_directive()?);
}
Ok(block)
}
}
/// Produce a short human-readable description of a token for error messages.
fn token_debug_str(token: &Token) -> String {
match token {
Token::Word(w, _) => w.clone(),
Token::String(s, _) => format!("\"{}\"", s),
Token::Integer(n, _) => n.to_string(),
Token::Float(f, _) => format!("{}", f),
Token::Size(b, _) => format!("{}bytes", b),
Token::Time(ms, _) => format!("{}ms", ms),
Token::LBrace(_) => "{".to_owned(),
Token::RBrace(_) => "}".to_owned(),
Token::Semicolon(_) => ";".to_owned(),
}
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Parse an nginx-style configuration string into an AST [`Block`].
///
/// # Errors
///
/// Returns a [`ConfigError`] if the input contains syntax errors such as
/// unterminated strings, unmatched braces, or invalid number literals.
///
/// # Examples
///
/// ```
/// use veld::config::parser::parse;
///
/// let config = parse(r#"
/// worker_processes 4;
/// http {
/// server {
/// listen 80;
/// location / {
/// root /var/www/html;
/// }
/// }
/// }
/// "#).expect("valid config");
///
/// // Numeric values are parsed as integers; read them with `get_i64`.
/// assert_eq!(config.get_i64("worker_processes"), Some(4));
/// ```
pub fn parse(input: &str) -> Result<Block, ConfigError> {
let tokens = Lexer::new(input).tokenize()?;
Parser::new(tokens).parse_config()
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// -- lexer / tokenizer tests -------------------------------------------
#[test]
fn test_empty_input() {
let block = parse("").unwrap();
assert!(block.is_empty());
}
#[test]
fn test_simple_directive() {
let block = parse("worker_processes 4;").unwrap();
assert_eq!(block.directives.len(), 1);
let d = &block.directives[0];
assert_eq!(d.name, "worker_processes");
assert_eq!(d.args, vec![Value::Number(4)]);
}
#[test]
fn test_multiple_directives() {
let block = parse("a 1; b 2; c 3;").unwrap();
assert_eq!(block.directives.len(), 3);
assert_eq!(block.directives[0].name, "a");
assert_eq!(block.directives[1].name, "b");
assert_eq!(block.directives[2].name, "c");
}
#[test]
fn test_quoted_string() {
let block = parse(r#"server_name "example.com";"#).unwrap();
let d = &block.directives[0];
assert_eq!(d.name, "server_name");
assert_eq!(d.args, vec![Value::String("example.com".into())]);
}
#[test]
fn test_string_escapes() {
let block = parse(r#"msg "hello\nworld\t\"end\\" ;"#).unwrap();
let val = block.directives[0].args[0].as_str();
assert_eq!(val, "hello\nworld\t\"end\\");
}
#[test]
fn test_nested_blocks() {
let input = r#"
http {
server {
listen 80;
location / {
root /var/www;
}
}
}
"#;
let block = parse(input).unwrap();
let http = block.get("http").unwrap();
let server = http.block.as_ref().unwrap().get("server").unwrap();
let server_block = server.block.as_ref().unwrap();
// `listen 80` is parsed as a numeric value; read it as an integer.
assert_eq!(server_block.get_i64("listen"), Some(80));
let loc = server_block.get("location").unwrap();
assert_eq!(loc.args[0].as_str(), "/");
let loc_block = loc.block.as_ref().unwrap();
assert_eq!(loc_block.get_str("root"), Some("/var/www"));
}
#[test]
fn test_comments() {
let input = r#"
# This is a comment
worker_processes 4; # inline comment
# Another comment
"#;
let block = parse(input).unwrap();
assert_eq!(block.directives.len(), 1);
assert_eq!(block.directives[0].name, "worker_processes");
}
#[test]
fn test_size_suffixes() {
let block = parse("buf 1k; big 2m; huge 3g;").unwrap();
assert_eq!(block.directives[0].args, vec![Value::Size(1024)]);
assert_eq!(block.directives[1].args, vec![Value::Size(2 * 1024 * 1024)]);
assert_eq!(
block.directives[2].args,
vec![Value::Size(3 * 1024 * 1024 * 1024)]
);
}
#[test]
fn test_time_suffixes() {
let block = parse("t1 500ms; t2 30s; t3 2h; t4 1d;").unwrap();
assert_eq!(block.directives[0].args, vec![Value::Time(500)]);
assert_eq!(block.directives[1].args, vec![Value::Time(30_000)]);
assert_eq!(block.directives[2].args, vec![Value::Time(7_200_000)]);
assert_eq!(block.directives[3].args, vec![Value::Time(86_400_000)]);
}
#[test]
fn test_size_m_means_megabytes() {
let block = parse("limit 10m;").unwrap();
assert_eq!(
block.directives[0].args,
vec![Value::Size(10 * 1024 * 1024)]
);
}
#[test]
fn test_float_literal() {
let block = parse("ratio 2.5;").unwrap();
assert_eq!(block.directives[0].args, vec![Value::Float(2.5)]);
}
#[test]
fn test_multiple_args() {
let block = parse("proxy_pass http localhost 8080;").unwrap();
let d = &block.directives[0];
assert_eq!(d.name, "proxy_pass");
assert_eq!(d.args.len(), 3);
assert_eq!(d.args[0].as_str(), "http");
assert_eq!(d.args[1].as_str(), "localhost");
assert_eq!(d.args[2], Value::Number(8080));
}
#[test]
fn test_bool_keywords() {
let block = parse("gzip on; ssl off;").unwrap();
assert_eq!(block.directives[0].args, vec![Value::Bool(true)]);
assert_eq!(block.directives[1].args, vec![Value::Bool(false)]);
}
#[test]
fn test_line_tracking() {
let input = r#"
foo 1;
bar 2;
baz 3;
"#;
let block = parse(input).unwrap();
// Line 1 is the empty first line; "foo" is on line 2, etc.
assert_eq!(block.directives[0].line, 2);
assert_eq!(block.directives[1].line, 3);
assert_eq!(block.directives[2].line, 4);
}
// -- error cases -------------------------------------------------------
#[test]
fn test_unterminated_string() {
let result = parse(r#"name "unclosed;"#);
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("unterminated"));
}
#[test]
fn test_unmatched_brace_close() {
let result = parse("}");
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("unexpected"));
}
#[test]
fn test_unterminated_block() {
let result = parse("server { listen 80;");
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("unterminated"));
}
#[test]
fn test_invalid_number() {
let result = parse("val 99999999999999999999k;");
assert!(result.is_err());
assert!(result.unwrap_err().message.contains("invalid number"));
}
#[test]
fn test_directive_with_block_and_args() {
let input = "location /api {\n proxy_pass http://backend;\n}";
let block = parse(input).unwrap();
let loc = &block.directives[0];
assert_eq!(loc.name, "location");
assert_eq!(loc.args, vec![Value::String("/api".into())]);
let inner = loc.block.as_ref().unwrap();
assert_eq!(inner.directives.len(), 1);
assert_eq!(inner.directives[0].name, "proxy_pass");
}
#[test]
fn test_realistic_config() {
let input = r#"
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
"#;
let block = parse(input).unwrap();
assert_eq!(block.get_str("worker_processes"), Some("auto"));
assert_eq!(block.get_str("error_log"), Some("/var/log/nginx/error.log"));
let events = block.get("events").unwrap().block.as_ref().unwrap();
assert_eq!(events.get_i64("worker_connections"), Some(1024));
let http = block.get("http").unwrap().block.as_ref().unwrap();
assert_eq!(http.get_bool("sendfile"), Some(true));
assert_eq!(http.get_i64("keepalive_timeout"), Some(65));
let servers = http.get_all("server");
assert_eq!(servers.len(), 1);
let server_block = servers[0].block.as_ref().unwrap();
assert_eq!(server_block.get_i64("listen"), Some(80));
}
}