Skip to main content

variable_core/
validate.rs

1#![allow(unused_assignments)]
2
3use std::collections::HashMap;
4
5use miette::{Diagnostic, SourceSpan};
6use thiserror::Error;
7
8use crate::ast::VarFile;
9use crate::lexer::Span;
10
11#[derive(Error, Debug, Diagnostic)]
12pub enum ValidationError {
13    #[error("duplicate feature name: `{name}`")]
14    DuplicateFeature {
15        name: String,
16        #[label("first defined here")]
17        first: SourceSpan,
18        #[label("duplicate defined here")]
19        duplicate: SourceSpan,
20    },
21
22    #[error("duplicate feature id `{id}`")]
23    DuplicateFeatureId {
24        id: u32,
25        #[label("first defined here")]
26        first: SourceSpan,
27        #[label("duplicate defined here")]
28        duplicate: SourceSpan,
29    },
30
31    #[error("duplicate variable name `{name}` in feature `{feature}`")]
32    DuplicateVariable {
33        feature: String,
34        name: String,
35        #[label("first defined here")]
36        first: SourceSpan,
37        #[label("duplicate defined here")]
38        duplicate: SourceSpan,
39    },
40
41    #[error("duplicate variable id `{id}` in feature `{feature}`")]
42    DuplicateVariableId {
43        feature: String,
44        id: u32,
45        #[label("first defined here")]
46        first: SourceSpan,
47        #[label("duplicate defined here")]
48        duplicate: SourceSpan,
49    },
50}
51
52fn span_to_source_span(span: &Span) -> SourceSpan {
53    SourceSpan::from(span.offset)
54}
55
56pub fn validate(var_file: &VarFile) -> Result<(), Vec<ValidationError>> {
57    let mut errors = Vec::new();
58
59    // Check for duplicate feature names and IDs
60    let mut feature_names: HashMap<&str, &Span> = HashMap::new();
61    let mut feature_ids: HashMap<u32, &Span> = HashMap::new();
62    for feature in &var_file.features {
63        if let Some(first_span) = feature_names.get(feature.name.as_str()) {
64            errors.push(ValidationError::DuplicateFeature {
65                name: feature.name.clone(),
66                first: span_to_source_span(first_span),
67                duplicate: span_to_source_span(&feature.span),
68            });
69        } else {
70            feature_names.insert(&feature.name, &feature.span);
71        }
72
73        if let Some(first_span) = feature_ids.get(&feature.id) {
74            errors.push(ValidationError::DuplicateFeatureId {
75                id: feature.id,
76                first: span_to_source_span(first_span),
77                duplicate: span_to_source_span(&feature.span),
78            });
79        } else {
80            feature_ids.insert(feature.id, &feature.span);
81        }
82    }
83
84    // Check for duplicate variable names and IDs within each feature
85    for feature in &var_file.features {
86        let mut var_names: HashMap<&str, &Span> = HashMap::new();
87        let mut var_ids: HashMap<u32, &Span> = HashMap::new();
88        for variable in &feature.variables {
89            if let Some(first_span) = var_names.get(variable.name.as_str()) {
90                errors.push(ValidationError::DuplicateVariable {
91                    feature: feature.name.clone(),
92                    name: variable.name.clone(),
93                    first: span_to_source_span(first_span),
94                    duplicate: span_to_source_span(&variable.span),
95                });
96            } else {
97                var_names.insert(&variable.name, &variable.span);
98            }
99
100            if let Some(first_span) = var_ids.get(&variable.id) {
101                errors.push(ValidationError::DuplicateVariableId {
102                    feature: feature.name.clone(),
103                    id: variable.id,
104                    first: span_to_source_span(first_span),
105                    duplicate: span_to_source_span(&variable.span),
106                });
107            } else {
108                var_ids.insert(variable.id, &variable.span);
109            }
110        }
111    }
112
113    if errors.is_empty() {
114        Ok(())
115    } else {
116        Err(errors)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::lexer::lex;
124    use crate::parser::parse;
125
126    fn parse_and_validate(input: &str) -> Result<VarFile, Vec<ValidationError>> {
127        let tokens = lex(input).expect("lex failed");
128        let var_file = parse(tokens).expect("parse failed");
129        validate(&var_file)?;
130        Ok(var_file)
131    }
132
133    #[test]
134    fn valid_file_passes() {
135        let input = r#"1: Feature Checkout = {
136    1: Variable enabled Boolean = true
137    2: Variable max_items Number = 50
138}
139
1402: Feature Search = {
141    1: Variable query String = "default"
142}"#;
143        assert!(parse_and_validate(input).is_ok());
144    }
145
146    #[test]
147    fn duplicate_feature_name_error() {
148        let input = r#"1: Feature Checkout = {
149    1: Variable enabled Boolean = true
150}
151
1522: Feature Checkout = {
153    1: Variable max_items Number = 50
154}"#;
155        let err = parse_and_validate(input).unwrap_err();
156        assert_eq!(err.len(), 1);
157        match &err[0] {
158            ValidationError::DuplicateFeature { name, .. } => {
159                assert_eq!(name, "Checkout");
160            }
161            _ => panic!("expected DuplicateFeature error"),
162        }
163    }
164
165    #[test]
166    fn duplicate_variable_name_error() {
167        let input = r#"1: Feature Checkout = {
168    1: Variable enabled Boolean = true
169    2: Variable enabled Boolean = false
170}"#;
171        let err = parse_and_validate(input).unwrap_err();
172        assert_eq!(err.len(), 1);
173        match &err[0] {
174            ValidationError::DuplicateVariable { feature, name, .. } => {
175                assert_eq!(feature, "Checkout");
176                assert_eq!(name, "enabled");
177            }
178            _ => panic!("expected DuplicateVariable error"),
179        }
180    }
181
182    #[test]
183    fn error_has_correct_line_info() {
184        let input = r#"1: Feature Checkout = {
185    1: Variable enabled Boolean = true
186}
187
1882: Feature Checkout = {
189    1: Variable max_items Number = 50
190}"#;
191        let err = parse_and_validate(input).unwrap_err();
192        match &err[0] {
193            ValidationError::DuplicateFeature {
194                first, duplicate, ..
195            } => {
196                // First "Feature" is at offset 0
197                assert_eq!(first.offset(), 0);
198                // Duplicate "Feature" is on line 5
199                assert!(duplicate.offset() > 0);
200            }
201            _ => panic!("expected DuplicateFeature error"),
202        }
203    }
204
205    #[test]
206    fn duplicate_feature_id_error() {
207        let input = r#"1: Feature Checkout = {
208    1: Variable enabled Boolean = true
209}
210
2111: Feature Search = {
212    1: Variable query String = "default"
213}"#;
214        let err = parse_and_validate(input).unwrap_err();
215        assert_eq!(err.len(), 1);
216        match &err[0] {
217            ValidationError::DuplicateFeatureId { id, .. } => {
218                assert_eq!(*id, 1);
219            }
220            _ => panic!("expected DuplicateFeatureId error"),
221        }
222    }
223
224    #[test]
225    fn duplicate_variable_id_error() {
226        let input = r#"1: Feature Checkout = {
227    1: Variable enabled Boolean = true
228    1: Variable max_items Number = 50
229}"#;
230        let err = parse_and_validate(input).unwrap_err();
231        assert_eq!(err.len(), 1);
232        match &err[0] {
233            ValidationError::DuplicateVariableId { feature, id, .. } => {
234                assert_eq!(feature, "Checkout");
235                assert_eq!(*id, 1);
236            }
237            _ => panic!("expected DuplicateVariableId error"),
238        }
239    }
240}