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