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