deepwoken_reqparse/parse/
reqfile.rs1use std::collections::{HashMap, HashSet};
2use std::path::Path;
3
4use crate::model::req::{ReqVecExt, 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, separated};
10use winnow::prelude::*;
11
12use super::req::{identifier, requirement};
13
14enum BaseReqfileLine {
15 Requirement(Requirement),
16 DepedencyWithIdentifier {
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_statement,
112 requirement.map(BaseReqfileLine::Requirement),
113 ))
114 .parse_next(input)
115}
116
117fn dependency_statement(input: &mut &str) -> ModalResult<BaseReqfileLine> {
119 let prereqs: Vec<String> =
120 separated(1.., identifier, (multispace0, ',', multispace0)).parse_next(input)?;
121
122 let _ = multispace0.parse_next(input)?;
123 let _ = "=>".parse_next(input)?;
124 let _ = multispace0.parse_next(input)?;
125
126 alt((
127 |i: &mut &str| {
128 let mut req = requirement.parse_next(i)?;
129 req.prereqs = prereqs.clone();
130 Ok(BaseReqfileLine::Requirement(req))
131 },
132 |i: &mut &str| {
133 let dependent = identifier.parse_next(i)?;
134 Ok(BaseReqfileLine::DepedencyWithIdentifier {
135 prereqs: prereqs.clone(),
136 dependent,
137 })
138 },
139 ))
140 .parse_next(input)
141}
142
143struct ParsedLine {
144 rf_line: ReqfileLine,
145 line_num: usize,
146 timing: Timing
147}
148
149fn validate_and_transform(mut lines: Vec<ParsedLine>) -> Result<Reqfile> {
150 let mut named: HashMap<String, usize> = HashMap::new();
152
153 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 let mut dependent_ids: Vec<(Vec<String>, String, u64)> = vec![];
165
166 for (vec_idx, line) in lines.iter().enumerate() {
169 let line_num = line.line_num;
170
171 let base = line.rf_line.base();
172
173 match base {
174 BaseReqfileLine::DepedencyWithIdentifier { prereqs, dependent }
175 => {
176 if let ReqfileLine::Unspecified(_) = &line.rf_line {
180
181 } else {
182 return Err(ReqparseError::Reqfile {
183 line: line.line_num,
184 message: "Optional annotations '+' or ';' must be used \
185 at the requirement definition, not in a dependency statement, unless \
186 the definition is in the dependency statement itself.".into()
187 })
188 };
189
190 dependent_ids.push(
191 (prereqs.clone(), dependent.clone(), line_num as u64)
192 );
193 },
194 BaseReqfileLine::Requirement(req) => {
195 if let Some(name) = &req.name {
197 if named.insert(name.clone(), vec_idx).is_some() {
199 return Err(ReqparseError::Reqfile {
200 line: (line_num + 1) as usize,
201 message: format!("Duplicate identifier: {}", name),
202 });
203 }
204 }
205 }
206 };
207 }
208
209 for line in &lines {
213 let base = line.rf_line.base();
214
215 if let BaseReqfileLine::Requirement(req) = base {
216 if req.name.is_some() { continue }
218
219 let other_anon = lines.iter().map(|line| line.rf_line.base())
220 .find(|other| {
221 if let BaseReqfileLine::Requirement(other_req) = other {
222 other_req.name.is_none()
223 && other_req.name_or_default() == req.name_or_default()
224 && (!other_req.prereqs.is_empty() || !req.prereqs.is_empty())
226 && other_req != req
227 } else {
228 false
229 }
230 });
231
232 if other_anon.is_some() {
233 return Err(ReqparseError::Reqfile {
234 line: line.line_num,
235 message: format!(
236 "You may not have duplicate anonymous requirements if either of them have prerequisites: {}",
237 req.name_or_default()
238 )
239 })
240 }
241 }
242 }
243
244 for (prereqs, name, line_num) in dependent_ids {
246 match named.get(&name) {
247 Some(vec_idx) => {
248 for prereq in &prereqs {
251 if !named.contains_key(prereq) {
252 return Err(ReqparseError::Reqfile {
253 line: line_num as usize,
254 message: format!("Prerequisite: no variable named '{name}'.")
255 })
256 }
257 }
258
259 let line = &mut lines[*vec_idx];
260
261 let base: &mut BaseReqfileLine = line.rf_line.base_mut();
262
263 match base {
264 BaseReqfileLine::Requirement(req) => {
265 if !req.prereqs.is_empty() {
266 return Err(ReqparseError::Reqfile {
267 line: line_num as usize,
268 message: format!("'{name}' has multiple prerequisite assignments.")
269 })
270 }
271
272 req.prereqs = prereqs;
273 },
274 _ => {}
275 };
276 },
277 None => {
278 return Err(ReqparseError::Reqfile {
279 line: line_num as usize,
280 message: format!("Dependent: no variable named '{name}'.")
281 })
282 }
283 }
284 }
285
286 let mut tree = ReqTree::new();
287
288 for line in &lines {
290 let base = line.rf_line.base();
291
292 match base {
293 BaseReqfileLine::Requirement(req) => {
294 tree.insert(req.clone());
295 },
296 _ => {}
297 }
298 }
299
300 if let Some(cycle) = tree.find_cycle() {
302 return Err(ReqparseError::Reqfile {
303 line: 0,
304 message: format!(
305 "Prereqs cannot be dependent on each other. Found cycle: {}",
306 cycle.join(" => ")
307 )
308 })
309 }
310
311 let check_opt_prereq = || {
313 for line in &lines {
314 match &line.rf_line {
315 ReqfileLine::Optional { base, .. } => {
319 if let BaseReqfileLine::Requirement(req) = base {
320 if let Some(name) = &req.name {
321 for dependent in tree.all_dependents(name) {
322 let vec_idx = str_to_idx[&dependent];
323
324 let dependent_line = &lines[vec_idx];
325
326 if !dependent_line.rf_line.is_explicit_optional() {
327 return Err(ReqparseError::Reqfile {
328 line: line.line_num,
329 message: format!(
330 "'{}' was declared as optional, however one of its \
331 dependents are required: '{} at line {}'.\n\
332 Try marking '{}' as optional instead.",
333 name,
334 dependent,
335 dependent_line.line_num,
336 dependent
337 )
338 })
339 }
340 }
341 }
342 }
343 },
344 _ => {}
345 }
346 }
347
348 Ok(())
349 };
350
351 check_opt_prereq()?;
352
353 let mut optional: Vec<OptionalGroup> = vec![];
358
359 let mut marked_opt: HashSet<String> = HashSet::new();
360
361 for line in &lines {
362 match &line.rf_line {
363 ReqfileLine::Optional { base, weight } => {
364 if let BaseReqfileLine::Requirement(req) = base {
367 let mut group = OptionalGroup {
368 general: HashSet::new(),
369 post: HashSet::new(),
370 weight: *weight,
371 };
372
373 for req in tree
376 .all_prereqs(&req.name_or_default())
377 .iter().chain(&[req.name_or_default()]) {
378
379 let vec_idx = str_to_idx[req];
380
381 let req_line = &lines[vec_idx];
382
383 match req_line.rf_line.base() {
384 BaseReqfileLine::Requirement(req) => {
385 group.get_set(req_line.timing)
386 .insert(req.clone());
387 },
388 _ => {}
389 }
390
391 marked_opt.insert(req.clone());
392 }
393
394 optional.push(group)
395 }
396 },
397 _ => {}
398 }
399 }
400
401 for line in &lines {
404 match &line.rf_line {
405 ReqfileLine::ForceRequired(base) => {
406 if let BaseReqfileLine::Requirement(req) = base {
407 for req in tree
411 .all_prereqs(&req.name_or_default())
412 .iter().chain(&[req.name_or_default()]) {
413
414 let vec_idx = str_to_idx[req];
415
416 let req_line = &lines[vec_idx];
417
418 match req_line.rf_line.base() {
419 BaseReqfileLine::Requirement(req) => {
420 for group in &mut optional {
421 group.get_set(req_line.timing).remove(req);
422 }
423 },
424 _ => {}
425 }
426
427 marked_opt.remove(req);
429 }
430 }
431 },
432 _ => {}
433 }
434 }
435
436 let mut general: Vec<Requirement> = vec![];
438 let mut post: Vec<Requirement> = vec![];
439
440 for line in &lines {
441 let base = line.rf_line.base();
444 if let BaseReqfileLine::Requirement(req) = base {
445 if marked_opt.contains(&req.name_or_default()) { continue }
446
447 match line.timing {
448 Timing::Free => general.push(req.clone()),
449 Timing::Post => post.push(req.clone()),
450 }
451 }
452 }
453
454 Ok( Reqfile {
455 general,
456 post,
457 optional
458 })
459}
460
461pub fn parse_reqfile_str(content: &String) -> Result<Reqfile> {
464 let mut lines: Vec<ParsedLine> = vec![];
465
466 let mut current = Timing::Free;
467
468 for (i, line) in content.lines().enumerate() {
469 let line = line.trim();
470 if line.is_empty() || line.starts_with('#') || line.starts_with("//") {
471 continue;
472 }
473
474 if line.to_uppercase().starts_with("FREE") {
475 current = Timing::Free;
476 continue;
477 }
478
479 if line.to_uppercase().starts_with("POST") {
480 current = Timing::Post;
481 continue;
482 }
483
484 let parsed = parse_reqfile_line(&line).map_err(|e| ReqparseError::Reqfile {
485 line: i + 1,
486 message: e.to_string(),
487 })?;
488
489 lines.push(ParsedLine {
490 rf_line: parsed,
491 line_num: i,
492 timing: current
493 });
494 }
495
496 validate_and_transform(lines)
497}
498
499pub fn parse_reqfile(path: &Path) -> Result<Reqfile> {
501 use std::fs;
502
503 let content = fs::read_to_string(path)?;
504
505 parse_reqfile_str(&content)
506}
507
508pub fn gen_reqfile(payload: &Reqfile) -> String {
509 let mut output = String::new();
510
511 output.push_str("# Auto-generated reqfile\n\n");
512 output.push_str("Free:\n");
513
514 let clean_name = |name: &str| {
521 name.replace(" ", "_")
522 .replace("[", "")
523 .replace("]", "")
524 .replace("'", "")
525 .replace(":", "")
526 .replace("(", "")
527 .replace(")", "")
528 };
529
530 let mut i = 0;
531
532 let mut general = payload
533 .general
534 .iter()
535 .map(|req: &Requirement| {
536 i += 1;
537
538 let mut req = req.clone();
539
540 req.name = req.name.clone().or_else(|| {
541 if !req.prereqs.is_empty() {
542 Some(format!("id_{}", i))
543 } else {
544 None
545 }
546 });
547
548 req
549 })
550 .collect::<Vec<_>>();
551
552 let mut post = payload
553 .post
554 .iter()
555 .map(|req: &Requirement| {
556 i += 1;
557
558 let mut req = req.clone();
559
560 req.name = req.name.clone().or_else(|| {
561 if !req.prereqs.is_empty() {
562 Some(format!("id_{}", i))
563 } else {
564 None
565 }
566 });
567
568 req
569 })
570 .collect::<Vec<_>>();
571
572 general.map_names(clean_name);
573
574 post.map_names(clean_name);
575
576 for req in &general {
577 output.push_str(&format!("{}\n", req));
578 }
579
580 if !post.is_empty() {
581 output.push_str("\nPost:\n");
582
583 for req in &post {
584 output.push_str(&format!("{}\n", req));
585 }
586 }
587
588 output
589}