Skip to main content

variable_core/
validate.rs

1use std::collections::HashMap;
2
3use miette::{Diagnostic, SourceSpan};
4use thiserror::Error;
5
6use crate::ast::VarFile;
7use crate::lexer::Span;
8
9#[derive(Error, Debug, Diagnostic)]
10pub enum ValidationError {
11    #[error("duplicate feature name: `{name}`")]
12    DuplicateFeature {
13        name: String,
14        #[label("first defined here")]
15        first: SourceSpan,
16        #[label("duplicate defined here")]
17        duplicate: SourceSpan,
18    },
19
20    #[error("duplicate variable name `{name}` in feature `{feature}`")]
21    DuplicateVariable {
22        feature: String,
23        name: String,
24        #[label("first defined here")]
25        first: SourceSpan,
26        #[label("duplicate defined here")]
27        duplicate: SourceSpan,
28    },
29}
30
31fn span_to_source_span(span: &Span) -> SourceSpan {
32    SourceSpan::from(span.offset)
33}
34
35pub fn validate(var_file: &VarFile) -> Result<(), Vec<ValidationError>> {
36    let mut errors = Vec::new();
37
38    // Check for duplicate feature names
39    let mut feature_names: HashMap<&str, &Span> = HashMap::new();
40    for feature in &var_file.features {
41        if let Some(first_span) = feature_names.get(feature.name.as_str()) {
42            errors.push(ValidationError::DuplicateFeature {
43                name: feature.name.clone(),
44                first: span_to_source_span(first_span),
45                duplicate: span_to_source_span(&feature.span),
46            });
47        } else {
48            feature_names.insert(&feature.name, &feature.span);
49        }
50    }
51
52    // Check for duplicate variable names within each feature
53    for feature in &var_file.features {
54        let mut var_names: HashMap<&str, &Span> = HashMap::new();
55        for variable in &feature.variables {
56            if let Some(first_span) = var_names.get(variable.name.as_str()) {
57                errors.push(ValidationError::DuplicateVariable {
58                    feature: feature.name.clone(),
59                    name: variable.name.clone(),
60                    first: span_to_source_span(first_span),
61                    duplicate: span_to_source_span(&variable.span),
62                });
63            } else {
64                var_names.insert(&variable.name, &variable.span);
65            }
66        }
67    }
68
69    if errors.is_empty() {
70        Ok(())
71    } else {
72        Err(errors)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::lexer::lex;
80    use crate::parser::parse;
81
82    fn parse_and_validate(input: &str) -> Result<VarFile, Vec<ValidationError>> {
83        let tokens = lex(input).expect("lex failed");
84        let var_file = parse(tokens).expect("parse failed");
85        validate(&var_file)?;
86        Ok(var_file)
87    }
88
89    #[test]
90    fn valid_file_passes() {
91        let input = r#"Feature Checkout {
92    Variable enabled Boolean = true
93    Variable max_items Number = 50
94}
95
96Feature Search {
97    Variable query String = "default"
98}"#;
99        assert!(parse_and_validate(input).is_ok());
100    }
101
102    #[test]
103    fn duplicate_feature_name_error() {
104        let input = r#"Feature Checkout {
105    Variable enabled Boolean = true
106}
107
108Feature Checkout {
109    Variable max_items Number = 50
110}"#;
111        let err = parse_and_validate(input).unwrap_err();
112        assert_eq!(err.len(), 1);
113        match &err[0] {
114            ValidationError::DuplicateFeature { name, .. } => {
115                assert_eq!(name, "Checkout");
116            }
117            _ => panic!("expected DuplicateFeature error"),
118        }
119    }
120
121    #[test]
122    fn duplicate_variable_name_error() {
123        let input = r#"Feature Checkout {
124    Variable enabled Boolean = true
125    Variable enabled Boolean = false
126}"#;
127        let err = parse_and_validate(input).unwrap_err();
128        assert_eq!(err.len(), 1);
129        match &err[0] {
130            ValidationError::DuplicateVariable {
131                feature, name, ..
132            } => {
133                assert_eq!(feature, "Checkout");
134                assert_eq!(name, "enabled");
135            }
136            _ => panic!("expected DuplicateVariable error"),
137        }
138    }
139
140    #[test]
141    fn error_has_correct_line_info() {
142        let input = r#"Feature Checkout {
143    Variable enabled Boolean = true
144}
145
146Feature Checkout {
147    Variable max_items Number = 50
148}"#;
149        let err = parse_and_validate(input).unwrap_err();
150        match &err[0] {
151            ValidationError::DuplicateFeature {
152                first, duplicate, ..
153            } => {
154                // First "Feature" is at offset 0
155                assert_eq!(first.offset(), 0);
156                // Duplicate "Feature" is on line 5
157                assert!(duplicate.offset() > 0);
158            }
159            _ => panic!("expected DuplicateFeature error"),
160        }
161    }
162}