deepwoken_reqparse/parse/
reqfile.rs1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3use crate::model::reqfile::Reqfile;
4use crate::util::traits::{ReqIterExt, ReqVecExt};
5use crate::model::req::{Requirement, Timing};
6use crate::util::reqtree::ReqTree;
7use crate::error::{Result, ReqparseError};
8use crate::model::{opt::OptionalGroup};
9use winnow::ascii::{digit1, multispace0};
10use winnow::combinator::{alt, eof, separated};
11use winnow::prelude::*;
12
13use super::req::{identifier, requirement};
14
15enum BaseReqfileLine {
16 Requirement(Requirement),
17 DependencyWithIdentifier {
18 prereqs: Vec<String>,
19 dependent: String,
20 },
21}
22
23enum ReqfileLine {
26 Unspecified(BaseReqfileLine),
28 ForceRequired(BaseReqfileLine),
31 Optional { base: BaseReqfileLine, weight: i64 }
35}
36
37impl ReqfileLine {
38 pub fn base(&self) -> &BaseReqfileLine {
39 match self {
40 ReqfileLine::Unspecified(base)
41 | ReqfileLine::ForceRequired(base)
42 | ReqfileLine::Optional { base, .. } => base,
43 }
44 }
45
46 pub fn base_mut(&mut self) -> &mut BaseReqfileLine {
47 match self {
48 ReqfileLine::Unspecified(base)
49 | ReqfileLine::ForceRequired(base)
50 | ReqfileLine::Optional { base, .. } => base,
51 }
52 }
53
54 pub fn is_explicit_optional(&self) -> bool {
55 match self {
56 ReqfileLine::Optional { .. } => true,
57 _ => false
58 }
59 }
60}
61
62fn parse_reqfile_line(input: &str) -> std::result::Result<ReqfileLine, String> {
63 let input = input.trim();
64 reqfile_line
65 .parse(&input)
66 .map_err(|e| format!("Parse error: {}", e))
67}
68
69fn reqfile_line(input: &mut &str) -> ModalResult<ReqfileLine> {
70 let _ = multispace0.parse_next(input)?;
71 alt((
72 optional_line,
73 force_required_line,
74 base_reqfile_line.map(ReqfileLine::Unspecified),
75 ))
76 .parse_next(input)
77}
78
79fn optional_line(input: &mut &str) -> ModalResult<ReqfileLine> {
81 let weight =
82 digit1.try_map(|s: &str| s.parse::<i64>())
83 .verify(|&n| (1..=20).contains(&n))
84 .parse_next(input)?;
85
86 let _ = (multispace0, ';', multispace0).parse_next(input)?;
87 let base = base_reqfile_line.parse_next(input)?;
88 Ok(ReqfileLine::Optional { base, weight })
89}
90
91fn force_required_line(input: &mut &str) -> ModalResult<ReqfileLine> {
93 let _ = ('+', multispace0).parse_next(input)?;
94 let base = base_reqfile_line.parse_next(input)?;
95 Ok(ReqfileLine::ForceRequired(base))
96}
97
98fn base_reqfile_line(input: &mut &str) -> ModalResult<BaseReqfileLine> {
100 let _ = multispace0.parse_next(input)?;
101
102 alt((
103 dependency_with_identifier,
104 requirement.map(BaseReqfileLine::Requirement),
105 ))
106 .parse_next(input)
107}
108
109fn dependency_with_identifier(input: &mut &str) -> ModalResult<BaseReqfileLine> {
112 let prereqs: Vec<String> =
113 separated(1.., identifier, (multispace0, ',', multispace0)).parse_next(input)?;
114
115 let _ = multispace0.parse_next(input)?;
116 let _ = "=>".parse_next(input)?;
117 let _ = multispace0.parse_next(input)?;
118
119 let dependent = identifier.parse_next(input)?;
120
121 let _ = multispace0.parse_next(input)?;
122 eof.parse_next(input)?;
123
124 Ok(BaseReqfileLine::DependencyWithIdentifier {
125 prereqs,
126 dependent,
127 })
128}
129
130struct ParsedLine {
131 rf_line: ReqfileLine,
132 line_num: usize,
133 timing: Timing
134}
135
136struct ReqfileIndex {
137 named: HashMap<String, usize>,
138 str_to_idx: HashMap<String, usize>,
139 dependency_statements: Vec<(Vec<String>, String, u64)>,
140}
141
142fn build_index(lines: &[ParsedLine]) -> Result<ReqfileIndex> {
143 let mut named: HashMap<String, usize> = HashMap::new();
144 let mut dependency_statements: Vec<(Vec<String>, String, u64)> = vec![];
145
146 let str_to_idx: HashMap<String, usize> = lines.iter().enumerate()
147 .filter_map(|(i, l)| {
148 match l.rf_line.base() {
149 BaseReqfileLine::Requirement(req)
150 => Some((req.name_or_default(), i)),
151 _ => None
152 }
153 }).collect();
154
155 for (vec_idx, line) in lines.iter().enumerate() {
156 let base = line.rf_line.base();
157
158 match base {
159 BaseReqfileLine::DependencyWithIdentifier { prereqs, dependent }
160 => {
161 if let ReqfileLine::Unspecified(_) = &line.rf_line {
165
166 } else {
167 return Err(ReqparseError::Reqfile {
168 line: line.line_num,
169 message: "Optional annotations '+' or ';' must be used \
170 at the requirement definition, not in a dependency statement, unless \
171 the definition is in the dependency statement itself.".into()
172 })
173 };
174
175 dependency_statements.push(
176 (prereqs.clone(), dependent.clone(), line.line_num as u64)
177 );
178 },
179 BaseReqfileLine::Requirement(req) => {
180 if let Some(name) = &req.name {
181 if named.insert(name.clone(), vec_idx).is_some() {
182 return Err(ReqparseError::Reqfile {
183 line: (line.line_num + 1) as usize,
184 message: format!("Duplicate identifier: {}", name),
185 });
186 }
187 }
188 }
189 };
190 }
191
192 Ok(ReqfileIndex { named, str_to_idx, dependency_statements })
193}
194
195fn validate_no_ambiguous_anonymous(lines: &[ParsedLine]) -> Result<()> {
196 for line in lines {
197 let base = line.rf_line.base();
198
199 if let BaseReqfileLine::Requirement(req) = base {
200 if req.name.is_some() { continue }
202
203 let other_anon = lines.iter().map(|line| line.rf_line.base())
204 .find(|other| {
205 if let BaseReqfileLine::Requirement(other_req) = other {
206 other_req.name.is_none()
207 && other_req.name_or_default() == req.name_or_default()
208 && (!other_req.prereqs.is_empty() || !req.prereqs.is_empty())
210 && other_req != req
211 } else {
212 false
213 }
214 });
215
216 if other_anon.is_some() {
217 return Err(ReqparseError::Reqfile {
218 line: line.line_num,
219 message: format!(
220 "You may not have duplicate anonymous requirements if either of them have prerequisites: {}",
221 req.name_or_default()
222 )
223 })
224 }
225 }
226 }
227
228 Ok(())
229}
230
231fn resolve_dependencies(lines: &mut [ParsedLine], index: &ReqfileIndex) -> Result<()> {
232 for (prereqs, name, line_num) in &index.dependency_statements {
233 match index.named.get(name) {
234 Some(vec_idx) => {
235 for prereq in prereqs {
236 if !index.named.contains_key(prereq) {
237 return Err(ReqparseError::Reqfile {
238 line: *line_num as usize,
239 message: format!("Prerequisite: no variable named '{name}'.")
240 })
241 }
242 }
243
244 let line = &mut lines[*vec_idx];
245
246 let base: &mut BaseReqfileLine = line.rf_line.base_mut();
247
248 match base {
249 BaseReqfileLine::Requirement(req) => {
250 if !req.prereqs.is_empty() {
251 return Err(ReqparseError::Reqfile {
252 line: *line_num as usize,
253 message: format!("'{name}' has multiple prerequisite assignments.")
254 })
255 }
256
257 req.prereqs = prereqs.clone();
258 },
259 _ => {}
260 };
261 },
262 None => {
263 return Err(ReqparseError::Reqfile {
264 line: *line_num as usize,
265 message: format!("Dependent: no variable named '{name}'.")
266 })
267 }
268 }
269 }
270
271 Ok(())
272}
273
274fn build_req_tree(lines: &[ParsedLine]) -> ReqTree {
275 let mut tree = ReqTree::new();
276
277 for line in lines {
278 if let BaseReqfileLine::Requirement(req) = line.rf_line.base() {
279 tree.insert(req.clone());
280 }
281 }
282
283 tree
284}
285
286fn validate_tree(
287 lines: &[ParsedLine],
288 tree: &ReqTree,
289 str_to_idx: &HashMap<String, usize>
290) -> Result<()> {
291 if let Some(cycle) = tree.find_cycle() {
292 return Err(ReqparseError::Reqfile {
293 line: 0,
294 message: format!(
295 "Prereqs cannot be dependent on each other. Found cycle: {}",
296 cycle.join(" => ")
297 )
298 })
299 }
300
301 for line in lines {
303 match &line.rf_line {
304 ReqfileLine::Optional { base, .. } => {
305 if let BaseReqfileLine::Requirement(req) = base {
306 if let Some(name) = &req.name {
307 for dependent in tree.all_dependents(name) {
308 let vec_idx = str_to_idx[&dependent];
309 let dependent_line = &lines[vec_idx];
310
311 if !dependent_line.rf_line.is_explicit_optional() {
312 return Err(ReqparseError::Reqfile {
313 line: line.line_num,
314 message: format!(
315 "'{}' was declared as optional, however one of its \
316 dependents are required: '{} at line {}'.\n\
317 Try marking '{}' as optional instead.",
318 name,
319 dependent,
320 dependent_line.line_num,
321 dependent
322 )
323 })
324 }
325 }
326 }
327 }
328 },
329 _ => {}
330 }
331 }
332
333 Ok(())
334}
335
336fn build_optional_groups(
337 lines: &[ParsedLine],
338 tree: &ReqTree,
339 str_to_idx: &HashMap<String, usize>,
340) -> (Vec<OptionalGroup>, HashSet<String>) {
341 let mut optional: Vec<OptionalGroup> = vec![];
342 let mut marked_opt: HashSet<String> = HashSet::new();
343
344 for line in lines {
345 match &line.rf_line {
346 ReqfileLine::Optional { base, weight } => {
347 if let BaseReqfileLine::Requirement(req) = base {
348 let mut group = OptionalGroup {
349 general: HashSet::new(),
350 post: HashSet::new(),
351 weight: *weight,
352 };
353
354 for req in tree
355 .all_prereqs(&req.name_or_default())
356 .iter().chain(&[req.name_or_default()]) {
357
358 let vec_idx = str_to_idx[req];
359 let req_line = &lines[vec_idx];
360
361 if let BaseReqfileLine::Requirement(req) = req_line.rf_line.base() {
362 group.get_set(req_line.timing).insert(req.clone());
363 }
364
365 marked_opt.insert(req.clone());
366 }
367
368 optional.push(group)
369 }
370 },
371 _ => {}
372 }
373 }
374
375 (optional, marked_opt)
376}
377
378fn apply_force_required(
379 lines: &[ParsedLine],
380 tree: &ReqTree,
381 str_to_idx: &HashMap<String, usize>,
382 optional: &mut Vec<OptionalGroup>,
383 marked_opt: &mut HashSet<String>,
384) {
385 for line in lines {
386 match &line.rf_line {
387 ReqfileLine::ForceRequired(base) => {
388 if let BaseReqfileLine::Requirement(req) = base {
389 for req in tree
390 .all_prereqs(&req.name_or_default())
391 .iter().chain(&[req.name_or_default()]) {
392
393 let vec_idx = str_to_idx[req];
394 let req_line = &lines[vec_idx];
395
396 if let BaseReqfileLine::Requirement(req) = req_line.rf_line.base() {
397 for group in optional.iter_mut() {
398 group.get_set(req_line.timing).remove(req);
399 }
400 }
401
402 marked_opt.remove(req);
403 }
404 }
405 },
406 _ => {}
407 }
408 }
409}
410
411fn collect_required_reqs(
412 lines: &[ParsedLine],
413 marked_opt: &HashSet<String>
414) -> (Vec<Requirement>, Vec<Requirement>) {
415 let mut general: Vec<Requirement> = vec![];
416 let mut post: Vec<Requirement> = vec![];
417
418 for line in lines {
419 let base = line.rf_line.base();
420 if let BaseReqfileLine::Requirement(req) = base {
421 if marked_opt.contains(&req.name_or_default()) { continue }
422
423 match line.timing {
424 Timing::Free => general.push(req.clone()),
425 Timing::Post => post.push(req.clone()),
426 }
427 }
428 }
429
430 (general, post)
431}
432
433fn validate_and_transform(mut lines: Vec<ParsedLine>) -> Result<Reqfile> {
434 let index = build_index(&lines)?;
435 validate_no_ambiguous_anonymous(&lines)?;
436 resolve_dependencies(&mut lines, &index)?;
437
438 let tree = build_req_tree(&lines);
439 validate_tree(&lines, &tree, &index.str_to_idx)?;
440
441 let (mut optional, mut marked_opt) =
442 build_optional_groups(&lines, &tree, &index.str_to_idx);
443 apply_force_required(&lines, &tree, &index.str_to_idx, &mut optional, &mut marked_opt);
444
445 let (general, post) = collect_required_reqs(&lines, &marked_opt);
446
447 Ok(Reqfile { general, post, optional })
448}
449
450pub fn parse_reqfile_str(content: &str) -> Result<Reqfile> {
453 let mut lines: Vec<ParsedLine> = vec![];
454
455 let mut current = Timing::Free;
456
457 for (i, line) in content.lines().enumerate() {
458 let line = line.trim();
459 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
460 continue;
461 }
462
463 if line.to_uppercase().starts_with("FREE") {
464 current = Timing::Free;
465 continue;
466 }
467
468 if line.to_uppercase().starts_with("POST") {
469 current = Timing::Post;
470 continue;
471 }
472
473 let parsed = parse_reqfile_line(&line).map_err(|e| ReqparseError::Reqfile {
474 line: i + 1,
475 message: e.to_string(),
476 })?;
477
478 lines.push(ParsedLine {
479 rf_line: parsed,
480 line_num: i,
481 timing: current
482 });
483 }
484
485 validate_and_transform(lines)
486}
487
488pub fn parse_reqfile(path: &Path) -> Result<Reqfile> {
490 use std::fs;
491
492 let content = fs::read_to_string(path)?;
493
494 parse_reqfile_str(&content)
495}
496
497pub fn gen_reqfile(payload: &Reqfile) -> String {
500 let mut output = String::new();
501
502 output.push_str("# Auto-generated reqfile\n\n");
503 output.push_str("Free:\n");
504
505 let clean_name = |name: &str| {
512 name.replace(" ", "_")
513 .replace("[", "")
514 .replace("]", "")
515 .replace("'", "")
516 .replace(":", "")
517 .replace("(", "")
518 .replace(")", "")
519 };
520
521 let mut i = 0;
522
523 let mut general = payload
524 .general
525 .iter()
526 .map(|req: &Requirement| {
527 i += 1;
528
529 let mut req = req.clone();
530
531 req.name = req.name.clone().or_else(|| {
532 if !req.prereqs.is_empty() {
533 Some(format!("id_{}", i))
534 } else {
535 None
536 }
537 });
538
539 req
540 })
541 .collect::<Vec<_>>();
542
543 let mut post = payload
544 .post
545 .iter()
546 .map(|req: &Requirement| {
547 i += 1;
548
549 let mut req = req.clone();
550
551 req.name = req.name.clone().or_else(|| {
552 if !req.prereqs.is_empty() {
553 Some(format!("id_{}", i))
554 } else {
555 None
556 }
557 });
558
559 req
560 })
561 .collect::<Vec<_>>();
562
563 general.map_names(clean_name);
564
565 post.map_names(clean_name);
566
567 for req in &general {
568 output.push_str(&format!("{}\n", req));
569 }
570
571 if !post.is_empty() {
572 output.push_str("\nPost:\n");
573
574 for req in &post {
575 output.push_str(&format!("{}\n", req));
576 }
577 }
578
579 output
580}