1pub mod ast;
83pub mod context;
84pub mod error;
85pub mod syntax_kind;
86
87pub mod lexer_rowan;
88pub mod line_index;
89pub mod parser;
90pub mod rowan_to_ast;
91
92#[cfg(feature = "wasm")]
93mod wasm;
94
95use syntax_kind::SyntaxNode;
96
97pub fn parse_string_rowan(source: &str) -> (SyntaxNode, Vec<parser::SyntaxError>) {
109 let tokens = lexer_rowan::tokenize(source);
110 let (green, errors) = parser::parse(tokens);
111 (SyntaxNode::new_root(green), errors)
112}
113
114use ast::Config;
115use error::{ParseError, ParseResult};
116use std::fs;
117use std::path::Path;
118
119pub fn parse_config(path: &Path) -> ParseResult<Config> {
121 let content = fs::read_to_string(path).map_err(|e| ParseError::IoError(e.to_string()))?;
122 parse_string(&content)
123}
124
125pub fn parse_string(source: &str) -> ParseResult<Config> {
130 let (root, errors) = parse_string_rowan(source);
131 if let Some(err) = errors.first() {
132 return Err(ParseError::UnexpectedToken {
133 expected: "valid syntax".to_string(),
134 found: err.message.clone(),
135 position: line_index::LineIndex::new(source).position(err.offset),
136 });
137 }
138 Ok(rowan_to_ast::convert(&root, source))
139}
140
141pub fn parse_string_with_errors(source: &str) -> (Config, Vec<parser::SyntaxError>) {
147 let (root, errors) = parse_string_rowan(source);
148 let config = rowan_to_ast::convert(&root, source);
149 (config, errors)
150}
151
152pub fn is_raw_block_directive(name: &str) -> bool {
166 name.ends_with("_by_lua_block")
169}
170
171const BLOCK_DIRECTIVES: &[&str] = &[
173 "http",
175 "server",
176 "location",
177 "upstream",
178 "events",
179 "stream",
180 "mail",
181 "types",
182 "if",
184 "limit_except",
185 "geo",
186 "map",
187 "split_clients",
188 "match",
189];
190
191pub fn is_block_directive(name: &str) -> bool {
202 BLOCK_DIRECTIVES.contains(&name) || is_raw_block_directive(name)
203}
204
205pub fn is_block_directive_with_extras(name: &str, additional: &[String]) -> bool {
219 is_block_directive(name) || additional.iter().any(|s| s == name)
220}
221
222pub fn is_raw_block_cst_node(block: &SyntaxNode) -> bool {
239 use syntax_kind::SyntaxKind;
240
241 let directive = match block.parent() {
242 Some(p) if p.kind() == SyntaxKind::DIRECTIVE => p,
243 _ => return false,
244 };
245 for child in directive.children_with_tokens() {
246 if let Some(t) = child.as_token()
247 && t.kind() == SyntaxKind::IDENT
248 {
249 return is_raw_block_directive(t.text());
250 }
251 }
252 false
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use ast::ConfigItem;
259
260 #[test]
261 fn test_simple_directive() {
262 let config = parse_string("worker_processes auto;").unwrap();
263 let directives: Vec<_> = config.directives().collect();
264 assert_eq!(directives.len(), 1);
265 assert_eq!(directives[0].name, "worker_processes");
266 assert_eq!(directives[0].first_arg(), Some("auto"));
267 }
268
269 #[test]
270 fn test_block_directive() {
271 let config = parse_string("http {\n server {\n listen 80;\n }\n}").unwrap();
272 let directives: Vec<_> = config.directives().collect();
273 assert_eq!(directives.len(), 1);
274 assert_eq!(directives[0].name, "http");
275 assert!(directives[0].block.is_some());
276
277 let all_directives: Vec<_> = config.all_directives().collect();
278 assert_eq!(all_directives.len(), 3);
279 assert_eq!(all_directives[0].name, "http");
280 assert_eq!(all_directives[1].name, "server");
281 assert_eq!(all_directives[2].name, "listen");
282 }
283
284 #[test]
285 fn test_extension_directive() {
286 let config = parse_string(r#"more_set_headers "Server: Custom";"#).unwrap();
287 let directives: Vec<_> = config.directives().collect();
288 assert_eq!(directives.len(), 1);
289 assert_eq!(directives[0].name, "more_set_headers");
290 assert_eq!(directives[0].first_arg(), Some("Server: Custom"));
291 }
292
293 #[test]
294 fn test_ssl_protocols() {
295 let config = parse_string("ssl_protocols TLSv1.2 TLSv1.3;").unwrap();
296 let directives: Vec<_> = config.directives().collect();
297 assert_eq!(directives.len(), 1);
298 assert_eq!(directives[0].name, "ssl_protocols");
299 assert_eq!(directives[0].args.len(), 2);
300 assert_eq!(directives[0].args[0].as_str(), "TLSv1.2");
301 assert_eq!(directives[0].args[1].as_str(), "TLSv1.3");
302 }
303
304 #[test]
305 fn test_autoindex() {
306 let config = parse_string("autoindex on;").unwrap();
307 let directives: Vec<_> = config.directives().collect();
308 assert_eq!(directives.len(), 1);
309 assert_eq!(directives[0].name, "autoindex");
310 assert!(directives[0].args[0].is_on());
311 }
312
313 #[test]
314 fn test_comment() {
315 let config = parse_string("# This is a comment\nworker_processes auto;").unwrap();
316 assert_eq!(config.items.len(), 2);
317 match &config.items[0] {
318 ConfigItem::Comment(c) => assert_eq!(c.text, "# This is a comment"),
319 _ => panic!("Expected comment"),
320 }
321 }
322
323 #[test]
324 fn test_full_config() {
325 let source = r#"
326# Good nginx configuration
327worker_processes auto;
328error_log /var/log/nginx/error.log;
329
330http {
331 server_tokens off;
332 gzip on;
333
334 server {
335 listen 80;
336 server_name example.com;
337
338 location / {
339 root /var/www/html;
340 index index.html;
341 }
342 }
343}
344"#;
345 let config = parse_string(source).unwrap();
346
347 let all_directives: Vec<_> = config.all_directives().collect();
348 let names: Vec<&str> = all_directives.iter().map(|d| d.name.as_str()).collect();
349
350 assert!(names.contains(&"worker_processes"));
351 assert!(names.contains(&"error_log"));
352 assert!(names.contains(&"server_tokens"));
353 assert!(names.contains(&"gzip"));
354 assert!(names.contains(&"listen"));
355 assert!(names.contains(&"server_name"));
356 assert!(names.contains(&"root"));
357 assert!(names.contains(&"index"));
358 }
359
360 #[test]
361 fn test_server_tokens_on() {
362 let config = parse_string("server_tokens on;").unwrap();
363 let directive = config.directives().next().unwrap();
364 assert_eq!(directive.name, "server_tokens");
365 assert!(directive.first_arg_is("on"));
366 assert!(directive.args[0].is_on());
367 }
368
369 #[test]
370 fn test_gzip_on() {
371 let config = parse_string("gzip on;").unwrap();
372 let directive = config.directives().next().unwrap();
373 assert_eq!(directive.name, "gzip");
374 assert!(directive.first_arg_is("on"));
375 }
376
377 #[test]
378 fn test_position_tracking() {
379 let config = parse_string("http {\n listen 80;\n}").unwrap();
380 let all_directives: Vec<_> = config.all_directives().collect();
381
382 assert_eq!(all_directives[0].span.start.line, 1);
384
385 assert_eq!(all_directives[1].span.start.line, 2);
387 }
388
389 #[test]
390 fn test_error_unmatched_brace() {
391 let result = parse_string("http {\n listen 80;\n");
392 assert!(result.is_err());
393 match result.unwrap_err() {
394 ParseError::UnclosedBlock { .. } | ParseError::UnexpectedToken { .. } => {}
395 e => panic!(
396 "Expected UnclosedBlock or UnexpectedToken error, got {:?}",
397 e
398 ),
399 }
400 }
401
402 #[test]
403 fn test_error_missing_semicolon() {
404 let result = parse_string("listen 80\n}");
405 assert!(result.is_err());
406 }
407
408 #[test]
409 fn test_roundtrip() {
410 let source = "worker_processes auto;\nhttp {\n listen 80;\n}\n";
411 let config = parse_string(source).unwrap();
412 let output = config.to_source();
413
414 let reparsed = parse_string(&output).unwrap();
416 let names1: Vec<&str> = config.all_directives().map(|d| d.name.as_str()).collect();
417 let names2: Vec<&str> = reparsed.all_directives().map(|d| d.name.as_str()).collect();
418 assert_eq!(names1, names2);
419 }
420
421 #[test]
422 fn test_lua_directive() {
423 let config = parse_string("lua_code_cache on;").unwrap();
424 let directive = config.directives().next().unwrap();
425 assert_eq!(directive.name, "lua_code_cache");
426 assert!(directive.first_arg_is("on"));
427 }
428
429 #[test]
430 fn test_gzip_types() {
431 let config = parse_string("gzip_types text/plain text/css application/json;").unwrap();
432 let directive = config.directives().next().unwrap();
433 assert_eq!(directive.name, "gzip_types");
434 assert_eq!(directive.args.len(), 3);
435 }
436
437 #[test]
438 fn test_lua_block_directive() {
439 let config = parse_string(
440 r#"content_by_lua_block {
441 local cjson = require "cjson"
442 ngx.say(cjson.encode({status = "ok"}))
443}"#,
444 )
445 .unwrap();
446 let directive = config.directives().next().unwrap();
447 assert_eq!(directive.name, "content_by_lua_block");
448 assert!(directive.block.is_some());
449
450 let block = directive.block.as_ref().unwrap();
451 assert!(block.is_raw());
452 assert!(block.raw_content.is_some());
453
454 let content = block.raw_content.as_ref().unwrap();
455 assert!(content.contains("local cjson = require"));
456 assert!(content.contains("ngx.say"));
457 }
458
459 #[test]
460 fn test_map_with_empty_string_key() {
461 let config = parse_string(
462 r#"map $http_upgrade $connection_upgrade {
463 default upgrade;
464 '' close;
465}"#,
466 )
467 .unwrap();
468 let directive = config.directives().next().unwrap();
469 assert_eq!(directive.name, "map");
470 assert!(directive.block.is_some());
471
472 let block = directive.block.as_ref().unwrap();
473 let directives: Vec<_> = block.directives().collect();
474 assert_eq!(directives.len(), 2);
475 assert_eq!(directives[0].name, "default");
476 assert_eq!(directives[1].name, ""); }
478
479 #[test]
480 fn test_init_by_lua_block() {
481 let config = parse_string(
482 r#"init_by_lua_block {
483 require "resty.core"
484 cjson = require "cjson"
485}"#,
486 )
487 .unwrap();
488 let directive = config.directives().next().unwrap();
489 assert_eq!(directive.name, "init_by_lua_block");
490 assert!(directive.block.is_some());
491
492 let block = directive.block.as_ref().unwrap();
493 assert!(block.is_raw());
494
495 let content = block.raw_content.as_ref().unwrap();
496 assert!(content.contains("require \"resty.core\""));
497 }
498
499 #[test]
500 fn test_whitespace_capture() {
501 let config = parse_string("http {\n listen 80;\n}").unwrap();
502 let all_directives: Vec<_> = config.all_directives().collect();
503
504 assert_eq!(all_directives[0].leading_whitespace, "");
506 assert_eq!(all_directives[0].space_before_terminator, " ");
508
509 assert_eq!(all_directives[1].leading_whitespace, " ");
511 assert_eq!(all_directives[1].space_before_terminator, "");
513 }
514
515 #[test]
516 fn test_comment_whitespace_capture() {
517 let config = parse_string(" # test comment\nlisten 80;").unwrap();
518
519 if let ConfigItem::Comment(comment) = &config.items[0] {
521 assert_eq!(comment.leading_whitespace, " ");
522 } else {
523 panic!("Expected comment");
524 }
525 }
526
527 #[test]
528 fn test_roundtrip_preserves_whitespace() {
529 let source = "http {\n server {\n listen 80;\n }\n}\n";
531 let config = parse_string(source).unwrap();
532 let output = config.to_source();
533
534 let reparsed = parse_string(&output).unwrap();
536 let all_directives: Vec<_> = reparsed.all_directives().collect();
537
538 assert_eq!(all_directives[0].leading_whitespace, "");
540 assert_eq!(all_directives[1].leading_whitespace, " ");
542 assert_eq!(all_directives[2].leading_whitespace, " ");
544 }
545
546 #[test]
549 fn test_variable_in_argument() {
550 let config = parse_string("set $var value;").unwrap();
551 let directive = config.directives().next().unwrap();
552 assert_eq!(directive.name, "set");
553 assert_eq!(directive.args[0].as_str(), "var");
555 assert!(directive.args[0].is_variable());
556 assert_eq!(directive.args[0].raw, "$var");
558 }
559
560 #[test]
561 fn test_variable_in_proxy_pass() {
562 let config = parse_string("proxy_pass http://$backend;").unwrap();
564 let directive = config.directives().next().unwrap();
565 assert_eq!(directive.args[0].as_str(), "http://");
567 assert!(directive.args[0].is_literal());
568 assert_eq!(directive.args[1].as_str(), "backend");
570 assert!(directive.args[1].is_variable());
571 }
572
573 #[test]
574 fn test_braced_variable() {
575 let config = parse_string(r#"add_header X-Request-Id "${request_id}";"#).unwrap();
576 let directive = config.directives().next().unwrap();
577 assert!(directive.args[1].is_quoted());
579 assert!(directive.args[1].as_str().contains("request_id"));
580 }
581
582 #[test]
585 fn test_location_exact_match() {
586 let config = parse_string("location = /exact { return 200; }").unwrap();
587 let directive = config.directives().next().unwrap();
588 assert_eq!(directive.name, "location");
589 assert_eq!(directive.args[0].as_str(), "=");
590 assert_eq!(directive.args[1].as_str(), "/exact");
591 }
592
593 #[test]
594 fn test_location_prefix_match() {
595 let config = parse_string("location ^~ /prefix { return 200; }").unwrap();
596 let directive = config.directives().next().unwrap();
597 assert_eq!(directive.args[0].as_str(), "^~");
598 assert_eq!(directive.args[1].as_str(), "/prefix");
599 }
600
601 #[test]
602 fn test_location_regex_case_sensitive() {
603 let config = parse_string(r#"location ~ \.php$ { return 200; }"#).unwrap();
604 let directive = config.directives().next().unwrap();
605 assert_eq!(directive.args[0].as_str(), "~");
606 assert_eq!(directive.args[1].as_str(), r"\.php$");
607 }
608
609 #[test]
610 fn test_location_regex_case_insensitive() {
611 let config = parse_string(r#"location ~* \.(gif|jpg|png)$ { return 200; }"#).unwrap();
612 let directive = config.directives().next().unwrap();
613 assert_eq!(directive.args[0].as_str(), "~*");
614 assert_eq!(directive.args[1].as_str(), r"\.(gif|jpg|png)$");
615 }
616
617 #[test]
618 fn test_named_location() {
619 let config = parse_string("location @backend { proxy_pass http://backend; }").unwrap();
620 let directive = config.directives().next().unwrap();
621 assert_eq!(directive.args[0].as_str(), "@backend");
622 }
623
624 #[test]
627 fn test_if_variable_check() {
628 let config = parse_string("if ($request_uri ~* /admin) { return 403; }").unwrap();
629 let directive = config.directives().next().unwrap();
630 assert_eq!(directive.name, "if");
631 assert!(directive.block.is_some());
632 }
633
634 #[test]
635 fn test_if_file_exists() {
636 let config = parse_string("if (-f $request_filename) { break; }").unwrap();
637 let directive = config.directives().next().unwrap();
638 assert_eq!(directive.name, "if");
639 assert_eq!(directive.args[0].as_str(), "(-f");
640 }
641
642 #[test]
645 fn test_upstream_basic() {
646 let config = parse_string(
647 r#"upstream backend {
648 server 127.0.0.1:8080;
649 server 127.0.0.1:8081;
650}"#,
651 )
652 .unwrap();
653 let directive = config.directives().next().unwrap();
654 assert_eq!(directive.name, "upstream");
655 assert_eq!(directive.args[0].as_str(), "backend");
656
657 let servers: Vec<_> = directive.block.as_ref().unwrap().directives().collect();
658 assert_eq!(servers.len(), 2);
659 }
660
661 #[test]
662 fn test_upstream_with_options() {
663 let config = parse_string(
664 r#"upstream backend {
665 server 127.0.0.1:8080 weight=5 max_fails=3 fail_timeout=30s;
666 keepalive 32;
667}"#,
668 )
669 .unwrap();
670 let directive = config.directives().next().unwrap();
671 let block = directive.block.as_ref().unwrap();
672 let items: Vec<_> = block.directives().collect();
673
674 assert_eq!(items[0].name, "server");
675 assert!(items[0].args.iter().any(|a| a.as_str().contains("weight")));
676 assert_eq!(items[1].name, "keepalive");
677 }
678
679 #[test]
682 fn test_geo_directive() {
683 let config = parse_string(
684 r#"geo $geo {
685 default unknown;
686 127.0.0.1 local;
687 10.0.0.0/8 internal;
688}"#,
689 )
690 .unwrap();
691 let directive = config.directives().next().unwrap();
692 assert_eq!(directive.name, "geo");
693 assert!(directive.block.is_some());
694 }
695
696 #[test]
697 fn test_map_directive() {
698 let config = parse_string(
699 r#"map $uri $new_uri {
700 default $uri;
701 /old /new;
702 ~^/api/v1/(.*) /api/v2/$1;
703}"#,
704 )
705 .unwrap();
706 let directive = config.directives().next().unwrap();
707 assert_eq!(directive.name, "map");
708 assert_eq!(directive.args.len(), 2);
709 }
710
711 #[test]
714 fn test_single_quoted_string() {
715 let config = parse_string(r#"set $var 'single quoted';"#).unwrap();
716 let directive = config.directives().next().unwrap();
717 assert_eq!(directive.args[1].as_str(), "single quoted");
718 assert!(directive.args[1].is_quoted());
719 }
720
721 #[test]
722 fn test_double_quoted_string() {
723 let config = parse_string(r#"set $var "double quoted";"#).unwrap();
724 let directive = config.directives().next().unwrap();
725 assert_eq!(directive.args[1].as_str(), "double quoted");
726 assert!(directive.args[1].is_quoted());
727 }
728
729 #[test]
730 fn test_quoted_string_with_spaces() {
731 let config = parse_string(r#"add_header X-Custom "value with spaces";"#).unwrap();
732 let directive = config.directives().next().unwrap();
733 assert_eq!(directive.args[1].as_str(), "value with spaces");
734 }
735
736 #[test]
737 fn test_escaped_quote_in_string() {
738 let config = parse_string(r#"set $var "say \"hello\"";"#).unwrap();
739 let directive = config.directives().next().unwrap();
740 let value = directive.args[1].as_str();
742 assert!(value.contains("hello"), "value was: {}", value);
743 }
744
745 #[test]
748 fn test_include_directive() {
749 let config = parse_string("include /etc/nginx/conf.d/*.conf;").unwrap();
750 let directive = config.directives().next().unwrap();
751 assert_eq!(directive.name, "include");
752 assert_eq!(directive.args[0].as_str(), "/etc/nginx/conf.d/*.conf");
753 }
754
755 #[test]
756 fn test_include_with_glob() {
757 let config = parse_string("include sites-enabled/*;").unwrap();
758 let directive = config.directives().next().unwrap();
759 assert!(directive.args[0].as_str().contains("*"));
760 }
761
762 #[test]
765 fn test_error_unexpected_closing_brace() {
766 let result = parse_string("listen 80; }");
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_error_unclosed_string() {
772 let result = parse_string(r#"set $var "unclosed;"#);
773 assert!(result.is_err());
774 }
775
776 #[test]
777 fn test_error_empty_directive_name() {
778 let result = parse_string("map $a $b { '' x; }");
780 assert!(result.is_ok());
781 }
782
783 #[test]
786 fn test_try_files_directive() {
787 let config = parse_string("try_files $uri $uri/ /index.php?$args;").unwrap();
788 let directive = config.directives().next().unwrap();
789 assert_eq!(directive.name, "try_files");
790 assert!(directive.args.len() >= 3);
793 assert!(directive.args.iter().any(|a| a.as_str() == "uri"));
794 }
795
796 #[test]
797 fn test_rewrite_directive() {
798 let config = parse_string("rewrite ^/old/(.*)$ /new/$1 permanent;").unwrap();
799 let directive = config.directives().next().unwrap();
800 assert_eq!(directive.name, "rewrite");
801 assert!(directive.args.len() >= 3);
803 assert_eq!(directive.args[0].as_str(), "^/old/(.*)$");
804 assert!(directive.args.iter().any(|a| a.as_str() == "permanent"));
805 }
806
807 #[test]
808 fn test_return_directive() {
809 let config = parse_string("return 301 https://$host$request_uri;").unwrap();
810 let directive = config.directives().next().unwrap();
811 assert_eq!(directive.name, "return");
812 assert_eq!(directive.args[0].as_str(), "301");
813 }
814
815 #[test]
816 fn test_limit_except_block() {
817 let config = parse_string(
818 r#"location / {
819 limit_except GET POST {
820 deny all;
821 }
822}"#,
823 )
824 .unwrap();
825 let all: Vec<_> = config.all_directives().collect();
826 assert!(all.iter().any(|d| d.name == "limit_except"));
827 }
828
829 #[test]
832 fn test_ssl_configuration() {
833 let config = parse_string(
834 r#"server {
835 listen 443 ssl http2;
836 ssl_certificate /etc/ssl/cert.pem;
837 ssl_certificate_key /etc/ssl/key.pem;
838 ssl_protocols TLSv1.2 TLSv1.3;
839 ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256;
840 ssl_prefer_server_ciphers on;
841}"#,
842 )
843 .unwrap();
844
845 let all: Vec<_> = config.all_directives().collect();
846 assert!(all.iter().any(|d| d.name == "ssl_certificate"));
847 assert!(all.iter().any(|d| d.name == "ssl_protocols"));
848 }
849
850 #[test]
851 fn test_proxy_configuration() {
852 let config = parse_string(
853 r#"location /api {
854 proxy_pass http://backend;
855 proxy_set_header Host $host;
856 proxy_set_header X-Real-IP $remote_addr;
857 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
858 proxy_connect_timeout 60s;
859 proxy_read_timeout 60s;
860}"#,
861 )
862 .unwrap();
863
864 let all: Vec<_> = config.all_directives().collect();
865 let proxy_headers: Vec<_> = all
866 .iter()
867 .filter(|d| d.name == "proxy_set_header")
868 .collect();
869 assert_eq!(proxy_headers.len(), 3);
870 }
871
872 #[test]
873 fn test_deeply_nested_blocks() {
874 let config = parse_string(
875 r#"http {
876 server {
877 location / {
878 if ($request_method = POST) {
879 return 405;
880 }
881 }
882 }
883}"#,
884 )
885 .unwrap();
886
887 let all: Vec<_> = config.all_directives().collect();
888 assert_eq!(all.len(), 5); }
890
891 #[test]
894 fn test_argument_is_on_off() {
895 let config = parse_string("gzip on; gzip_static off;").unwrap();
896 let directives: Vec<_> = config.directives().collect();
897
898 assert!(directives[0].args[0].is_on());
899 assert!(!directives[0].args[0].is_off());
900
901 assert!(directives[1].args[0].is_off());
902 assert!(!directives[1].args[0].is_on());
903 }
904
905 #[test]
906 fn test_argument_is_literal() {
907 let config = parse_string(r#"set $var "quoted"; set $var2 literal;"#).unwrap();
908 let directives: Vec<_> = config.directives().collect();
909
910 assert!(!directives[0].args[1].is_literal());
911 assert!(directives[1].args[1].is_literal());
912 }
913
914 #[test]
917 fn test_blank_lines_preserved() {
918 let config =
919 parse_string("worker_processes 1;\n\nerror_log /var/log/error.log;\n").unwrap();
920
921 assert_eq!(config.items.len(), 3);
923 assert!(matches!(config.items[1], ConfigItem::BlankLine(_)));
924 }
925
926 #[test]
927 fn test_multiple_blank_lines() {
928 let config = parse_string("a 1;\n\n\nb 2;\n").unwrap();
929
930 let blank_count = config
931 .items
932 .iter()
933 .filter(|i| matches!(i, ConfigItem::BlankLine(_)))
934 .count();
935 assert_eq!(blank_count, 2);
936 }
937
938 #[test]
941 fn test_events_block() {
942 let config = parse_string(
943 r#"events {
944 worker_connections 1024;
945 use epoll;
946 multi_accept on;
947}"#,
948 )
949 .unwrap();
950
951 let directive = config.directives().next().unwrap();
952 assert_eq!(directive.name, "events");
953
954 let inner: Vec<_> = directive.block.as_ref().unwrap().directives().collect();
955 assert_eq!(inner.len(), 3);
956 }
957
958 #[test]
961 fn test_stream_block() {
962 let config = parse_string(
963 r#"stream {
964 server {
965 listen 12345;
966 proxy_pass backend;
967 }
968}"#,
969 )
970 .unwrap();
971
972 let directive = config.directives().next().unwrap();
973 assert_eq!(directive.name, "stream");
974 }
975
976 #[test]
979 fn test_types_block() {
980 let config = parse_string(
981 r#"types {
982 text/html html htm;
983 text/css css;
984 application/javascript js;
985}"#,
986 )
987 .unwrap();
988
989 let directive = config.directives().next().unwrap();
990 assert_eq!(directive.name, "types");
991
992 let inner: Vec<_> = directive.block.as_ref().unwrap().directives().collect();
993 assert_eq!(inner.len(), 3);
994 assert_eq!(inner[0].name, "text/html");
995 }
996
997 #[test]
998 fn test_utf8_comment_column_tracking() {
999 let config = parse_string("# 開発環境\nlisten 80;").unwrap();
1002 if let ast::ConfigItem::Comment(c) = &config.items[0] {
1004 assert_eq!(c.span.start.line, 1);
1005 assert_eq!(c.span.start.column, 1);
1006 assert_eq!(c.span.end.column, 15);
1008 } else {
1009 panic!("expected Comment");
1010 }
1011 let directives: Vec<_> = config.all_directives().collect();
1013 assert_eq!(directives[0].span.start.line, 2);
1014 assert_eq!(directives[0].span.start.column, 1);
1015 }
1016
1017 #[test]
1018 fn test_utf8_comment_byte_offset_tracking() {
1019 let config = parse_string("# 開発環境\nlisten 80;").unwrap();
1021 if let ast::ConfigItem::Comment(c) = &config.items[0] {
1022 assert_eq!(c.span.start.offset, 0);
1024 assert_eq!(c.span.end.offset, 14);
1025 } else {
1026 panic!("expected Comment");
1027 }
1028 let directives: Vec<_> = config.all_directives().collect();
1030 assert_eq!(directives[0].span.start.offset, 15);
1031 }
1032}