Skip to main content

bwq_linter/
lib.rs

1pub mod ast;
2pub mod error;
3pub mod lexer;
4pub mod parser;
5pub mod validation;
6pub mod validator;
7
8use error::{LintError, LintReport, LintResult};
9use lexer::Lexer;
10use parser::Parser;
11use validator::Validator;
12
13pub struct BrandwatchLinter {
14    validator: Validator,
15}
16
17impl BrandwatchLinter {
18    pub fn new() -> Self {
19        Self {
20            validator: Validator::new(),
21        }
22    }
23
24    pub fn lint(&mut self, query: &str) -> LintResult<LintReport> {
25        let mut lexer = Lexer::new(query);
26        let tokens = lexer.tokenize()?;
27
28        let mut parser = Parser::new(tokens)?;
29        let parse_result = parser.parse()?;
30
31        let mut report = self.validator.validate(&parse_result.query);
32        report.warnings.extend(parse_result.warnings);
33
34        Ok(report)
35    }
36
37    pub fn analyze(&mut self, query: &str) -> AnalysisResult {
38        match self.lint(query) {
39            Ok(report) => AnalysisResult {
40                is_valid: !report.has_errors(),
41                errors: report.errors,
42                warnings: report.warnings,
43                query: Some(query.to_string()),
44            },
45            Err(error) => AnalysisResult {
46                is_valid: false,
47                errors: vec![error],
48                warnings: vec![],
49                query: Some(query.to_string()),
50            },
51        }
52    }
53
54    pub fn analyze_and_skip_empty(&mut self, query: &str) -> AnalysisResult {
55        if query.trim().is_empty() {
56            return AnalysisResult {
57                is_valid: true,
58                errors: Vec::new(),
59                warnings: Vec::new(),
60                query: Some(query.to_string()),
61            };
62        }
63
64        self.analyze(query)
65    }
66}
67
68impl Default for BrandwatchLinter {
69    fn default() -> Self {
70        Self::new()
71    }
72}
73
74#[derive(Debug, Clone)]
75pub struct AnalysisResult {
76    pub is_valid: bool,
77    pub errors: Vec<LintError>,
78    pub warnings: Vec<error::LintWarning>,
79    pub query: Option<String>,
80}
81
82impl AnalysisResult {
83    pub fn has_issues(&self) -> bool {
84        !self.errors.is_empty() || !self.warnings.is_empty()
85    }
86
87    pub fn summary(&self) -> String {
88        if self.is_valid && self.warnings.is_empty() {
89            "Query is valid with no issues".to_string()
90        } else {
91            let error_count = self.errors.len();
92            let warning_count = self.warnings.len();
93
94            match (error_count, warning_count) {
95                (0, 0) => "Query is valid with no issues".to_string(),
96                (0, w) => format!(
97                    "Query is valid with {} warning{}",
98                    w,
99                    if w == 1 { "" } else { "s" }
100                ),
101                (e, 0) => format!("Query has {} error{}", e, if e == 1 { "" } else { "s" }),
102                (e, w) => format!(
103                    "Query has {} error{} and {} warning{}",
104                    e,
105                    if e == 1 { "" } else { "s" },
106                    w,
107                    if w == 1 { "" } else { "s" }
108                ),
109            }
110        }
111    }
112
113    pub fn format_issues(&self) -> String {
114        let mut output = String::new();
115
116        if !self.errors.is_empty() {
117            output.push_str("Errors:\n");
118            for (i, error) in self.errors.iter().enumerate() {
119                output.push_str(&format!("  {}. {}\n", i + 1, error));
120            }
121        }
122
123        if !self.warnings.is_empty() {
124            if !output.is_empty() {
125                output.push('\n');
126            }
127            output.push_str("Warnings:\n");
128            for (i, warning) in self.warnings.iter().enumerate() {
129                output.push_str(&format!("  {}. {:?}\n", i + 1, warning));
130            }
131        }
132
133        output
134    }
135}
136
137pub fn lint_query(query: &str) -> LintResult<LintReport> {
138    let mut linter = BrandwatchLinter::new();
139    linter.lint(query)
140}
141
142pub fn analyze_query(query: &str) -> AnalysisResult {
143    let mut linter = BrandwatchLinter::new();
144    linter.analyze(query)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_basic_linting() {
153        let mut linter = BrandwatchLinter::new();
154        let report = linter.lint("apple AND juice").unwrap();
155        assert!(!report.has_errors());
156    }
157
158    #[test]
159    fn test_invalid_query() {
160        let mut linter = BrandwatchLinter::new();
161        let report = linter.lint("rating:6").unwrap();
162        assert!(report.has_errors());
163    }
164
165    #[test]
166    fn test_complex_query() {
167        let query = r#"(apple OR orange) AND "fruit juice" NOT bitter"#;
168        let mut linter = BrandwatchLinter::new();
169        let report = linter.lint(query).unwrap();
170        assert!(!report.has_errors());
171    }
172
173    #[test]
174    fn test_field_query() {
175        let query = r#"title:"apple juice" AND site:twitter.com"#;
176        let mut linter = BrandwatchLinter::new();
177        let report = linter.lint(query).unwrap();
178        assert!(!report.has_errors());
179    }
180
181    #[test]
182    fn test_proximity_query() {
183        let mut linter = BrandwatchLinter::new();
184
185        let query1 = r#"apple NEAR/3 juice"#;
186        let report1 = linter.lint(query1).unwrap();
187        assert!(!report1.has_errors());
188
189        let query2 = r#""apple juice"~5"#;
190        let report2 = linter.lint(query2).unwrap();
191        assert!(!report2.has_errors());
192    }
193
194    #[test]
195    fn test_analysis_result_summary() {
196        let analysis = analyze_query("apple AND juice");
197        assert_eq!(analysis.summary(), "Query is valid with no issues");
198
199        let analysis = analyze_query("*invalid");
200        assert!(analysis.summary().contains("error"));
201    }
202}