syncable_cli/analyzer/dclint/rules/
dcl009.rs

1//! DCL009: service-container-name-regex
2//!
3//! Container names must match a specified regex pattern.
4
5use 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
15// Default pattern: lowercase letters, numbers, hyphens, underscores
16const 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    // Use default pattern (in a real implementation, this could be configurable)
34    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}