syncable_cli/analyzer/dclint/rules/
dcl009.rs1use regex::Regex;
6
7use crate::analyzer::dclint::rules::{LintContext, Rule, SimpleRule, make_failure};
8use crate::analyzer::dclint::types::{CheckFailure, RuleCategory, Severity};
9
10const CODE: &str = "DCL009";
11const NAME: &str = "service-container-name-regex";
12const DESCRIPTION: &str = "Container names must follow the naming convention.";
13const URL: &str = "https://github.com/zavoloklom/docker-compose-linter/blob/main/docs/rules/service-container-name-regex-rule.md";
14
15const DEFAULT_PATTERN: &str = r"^[a-z][a-z0-9_-]*$";
17
18pub fn rule() -> impl Rule {
19 SimpleRule::new(
20 CODE,
21 NAME,
22 Severity::Warning,
23 RuleCategory::Style,
24 DESCRIPTION,
25 URL,
26 check,
27 )
28}
29
30fn check(ctx: &LintContext) -> Vec<CheckFailure> {
31 let mut failures = Vec::new();
32
33 let pattern = Regex::new(DEFAULT_PATTERN).expect("Invalid default pattern");
35
36 for (service_name, service) in &ctx.compose.services {
37 if let Some(container_name) = &service.container_name
38 && !pattern.is_match(container_name)
39 {
40 let line = service
41 .container_name_pos
42 .map(|p| p.line)
43 .unwrap_or(service.position.line);
44
45 let message = format!(
46 "Container name \"{}\" in service \"{}\" does not match the required pattern: {}",
47 container_name, service_name, DEFAULT_PATTERN
48 );
49
50 failures.push(
51 make_failure(
52 &CODE.into(),
53 NAME,
54 Severity::Warning,
55 RuleCategory::Style,
56 message,
57 line,
58 1,
59 false,
60 )
61 .with_data("serviceName", service_name.clone())
62 .with_data("containerName", container_name.clone())
63 .with_data("pattern", DEFAULT_PATTERN.to_string()),
64 );
65 }
66 }
67
68 failures
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use crate::analyzer::dclint::parser::parse_compose;
75
76 fn check_yaml(yaml: &str) -> Vec<CheckFailure> {
77 let compose = parse_compose(yaml).unwrap();
78 let ctx = LintContext::new(&compose, yaml, "docker-compose.yml");
79 check(&ctx)
80 }
81
82 #[test]
83 fn test_no_violation_valid_name() {
84 let yaml = r#"
85services:
86 web:
87 image: nginx
88 container_name: my-web-container
89"#;
90 assert!(check_yaml(yaml).is_empty());
91 }
92
93 #[test]
94 fn test_no_violation_no_container_name() {
95 let yaml = r#"
96services:
97 web:
98 image: nginx
99"#;
100 assert!(check_yaml(yaml).is_empty());
101 }
102
103 #[test]
104 fn test_violation_uppercase() {
105 let yaml = r#"
106services:
107 web:
108 image: nginx
109 container_name: MyContainer
110"#;
111 let failures = check_yaml(yaml);
112 assert_eq!(failures.len(), 1);
113 assert!(failures[0].message.contains("MyContainer"));
114 }
115
116 #[test]
117 fn test_violation_starts_with_number() {
118 let yaml = r#"
119services:
120 web:
121 image: nginx
122 container_name: 123container
123"#;
124 let failures = check_yaml(yaml);
125 assert_eq!(failures.len(), 1);
126 }
127
128 #[test]
129 fn test_valid_names() {
130 let valid_names = ["web", "my-app", "app_v1", "a123", "web-api-v2"];
131
132 for name in valid_names {
133 let yaml = format!(
134 r#"
135services:
136 web:
137 image: nginx
138 container_name: {}
139"#,
140 name
141 );
142 assert!(
143 check_yaml(&yaml).is_empty(),
144 "Name '{}' should be valid",
145 name
146 );
147 }
148 }
149}