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}