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