raz_override/parser/
smart_parser.rs1use regex::Regex;
6use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default)]
10pub struct ParsedOverrides {
11 pub env_vars: HashMap<String, String>,
13 pub options: Vec<String>,
15 pub args: Vec<String>,
17 pub raw_input: String,
19}
20
21impl ParsedOverrides {
22 pub fn is_empty(&self) -> bool {
24 self.env_vars.is_empty() && self.options.is_empty() && self.args.is_empty()
25 }
26}
27
28#[derive(Debug, Clone, PartialEq)]
30pub enum OverrideOperator {
31 Replace,
33 Add,
35 Remove,
37 Force,
39}
40
41fn parse_operator(option: &str) -> (OverrideOperator, &str) {
43 if option.starts_with("+--") {
44 (OverrideOperator::Add, &option[1..])
45 } else if option.starts_with("---") {
46 (OverrideOperator::Remove, &option[1..])
47 } else if option.starts_with("!--") {
48 (OverrideOperator::Force, &option[1..])
49 } else {
50 (OverrideOperator::Replace, option)
51 }
52}
53
54pub struct SmartOverrideParser {
56 env_regex: Regex,
58 #[allow(dead_code)]
60 subcommand: String,
61}
62
63impl SmartOverrideParser {
64 pub fn new(subcommand: impl Into<String>) -> Self {
66 Self {
67 env_regex: Regex::new(r#"^([A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)=(.+)$"#).unwrap(),
69 subcommand: subcommand.into(),
70 }
71 }
72
73 pub fn parse(&self, input: &str) -> ParsedOverrides {
75 let mut result = ParsedOverrides {
76 raw_input: input.to_string(),
77 ..Default::default()
78 };
79
80 if let Some(stripped) = input.strip_prefix("-- ") {
83 result.args = shell_words::split(stripped)
84 .unwrap_or_else(|_| stripped.split_whitespace().map(String::from).collect());
85 return result;
86 }
87
88 let parts: Vec<&str> = input.splitn(2, " -- ").collect();
89 let before_dash = parts[0];
90 let after_dash = parts.get(1);
91
92 if let Some(args_str) = after_dash {
94 result.args = shell_words::split(args_str).unwrap_or_else(|_| {
95 args_str.split_whitespace().map(String::from).collect()
97 });
98 }
99
100 let tokens = shell_words::split(before_dash).unwrap_or_else(|_| {
102 before_dash.split_whitespace().map(String::from).collect()
104 });
105
106 let mut i = 0;
107 while i < tokens.len() {
108 let token = &tokens[i];
109
110 if let Some(captures) = self.env_regex.captures(token) {
112 let key = captures.get(1).unwrap().as_str().to_string();
113 let value = captures.get(2).unwrap().as_str().to_string();
114 result.env_vars.insert(key, value);
115 i += 1;
116 continue;
117 }
118
119 let (_operator, clean_option) = parse_operator(token);
121
122 if clean_option.starts_with("-") {
123 result.options.push(clean_option.to_string());
125
126 if i + 1 < tokens.len() && !tokens[i + 1].starts_with("-") {
128 let next_token = &tokens[i + 1];
131 if !self.env_regex.is_match(next_token) {
132 result.options.push(next_token.clone());
133 i += 1;
134 }
135 }
136 i += 1;
137 } else {
138 result.options.push(token.clone());
141 i += 1;
142 }
143 }
144
145 result
146 }
147
148 pub fn parse_vscode_input(
150 &self,
151 input: &str,
152 ) -> (ParsedOverrides, HashMap<String, OverrideOperator>) {
153 let mut operators = HashMap::new();
154 let mut result = ParsedOverrides {
155 raw_input: input.to_string(),
156 ..Default::default()
157 };
158
159 let parts: Vec<&str> = input.splitn(2, " -- ").collect();
161 let before_dash = parts[0];
162 let after_dash = parts.get(1);
163
164 if let Some(args_str) = after_dash {
166 result.args = shell_words::split(args_str)
167 .unwrap_or_else(|_| args_str.split_whitespace().map(String::from).collect());
168 }
169
170 let tokens = shell_words::split(before_dash)
172 .unwrap_or_else(|_| before_dash.split_whitespace().map(String::from).collect());
173
174 let mut i = 0;
175 while i < tokens.len() {
176 let token = &tokens[i];
177
178 if let Some(captures) = self.env_regex.captures(token) {
180 let key = captures.get(1).unwrap().as_str().to_string();
181 let value = captures.get(2).unwrap().as_str().to_string();
182 result.env_vars.insert(key, value);
183 i += 1;
184 continue;
185 }
186
187 let (operator, clean_option) = parse_operator(token);
189
190 if operator != OverrideOperator::Replace {
192 operators.insert(clean_option.to_string(), operator.clone());
193 }
194
195 let final_option =
197 if operator == OverrideOperator::Remove && clean_option.starts_with("--") {
198 format!("--no-{}", &clean_option[2..])
199 } else {
200 clean_option.to_string()
201 };
202
203 result.options.push(final_option);
205
206 if i + 1 < tokens.len() && !tokens[i + 1].starts_with("-") {
208 result.options.push(tokens[i + 1].clone());
209 i += 2;
210 } else {
211 i += 1;
212 }
213 }
214
215 (result, operators)
216 }
217}
218
219pub fn overrides_to_cli_args(overrides: &ParsedOverrides) -> Vec<String> {
221 let mut args = vec![];
222
223 for (key, value) in &overrides.env_vars {
225 args.push(format!("--env={key}={value}"));
226 }
227
228 if !overrides.options.is_empty() {
230 args.push(format!("--options={}", overrides.options.join(" ")));
231 }
232
233 if !overrides.args.is_empty() {
235 args.push(format!("--args={}", overrides.args.join(" ")));
236 }
237
238 args
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn test_parse_simple_env() {
247 let parser = SmartOverrideParser::new("run");
248 let result = parser.parse("RUST_BACKTRACE=full");
249
250 assert_eq!(
251 result.env_vars.get("RUST_BACKTRACE"),
252 Some(&"full".to_string())
253 );
254 assert!(result.options.is_empty());
255 assert!(result.args.is_empty());
256 }
257
258 #[test]
259 fn test_parse_quoted_env() {
260 let parser = SmartOverrideParser::new("run");
261 let result = parser.parse(r#"RUST_FLAGS="-A warnings""#);
262
263 assert_eq!(
264 result.env_vars.get("RUST_FLAGS"),
265 Some(&"-A warnings".to_string())
266 );
267 }
268
269 #[test]
270 fn test_parse_options() {
271 let parser = SmartOverrideParser::new("run");
272 let result = parser.parse("--release --target aarch64-apple-darwin");
273
274 assert_eq!(
275 result.options,
276 vec!["--release", "--target", "aarch64-apple-darwin"]
277 );
278 assert!(result.env_vars.is_empty());
279 assert!(result.args.is_empty());
280 }
281
282 #[test]
283 fn test_parse_with_args() {
284 let parser = SmartOverrideParser::new("test");
285 let result = parser.parse("--release -- --exact --nocapture");
286
287 assert_eq!(result.options, vec!["--release"]);
288 assert_eq!(result.args, vec!["--exact", "--nocapture"]);
289 }
290
291 #[test]
292 fn test_parse_mixed() {
293 let parser = SmartOverrideParser::new("run");
294 let result = parser.parse("RUST_LOG=debug --release --features ssr -- --port 3000");
295
296 assert_eq!(result.env_vars.get("RUST_LOG"), Some(&"debug".to_string()));
297 assert_eq!(result.options, vec!["--release", "--features", "ssr"]);
298 assert_eq!(result.args, vec!["--port", "3000"]);
299 }
300
301 #[test]
302 fn test_parse_vscode_operators() {
303 let parser = SmartOverrideParser::new("run");
304 let (result, operators) = parser.parse_vscode_input("+--features new ---default-features");
305
306 assert_eq!(
307 result.options,
308 vec!["--features", "new", "--no-default-features"]
309 );
310 assert_eq!(operators.get("--features"), Some(&OverrideOperator::Add));
311 assert_eq!(
312 operators.get("--default-features"),
313 Some(&OverrideOperator::Remove)
314 );
315 }
316
317 #[test]
318 fn test_framework_options() {
319 let parser = SmartOverrideParser::new("run");
320 let result = parser.parse("--platform web --device true");
321
322 assert_eq!(
324 result.options,
325 vec!["--platform", "web", "--device", "true"]
326 );
327 }
328}