Skip to main content

deepwoken_reqparse/parse/
reqfile.rs

1use 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
22/// A full reqfile line.
23/// Note a required requirement cannot have optional prereqs.
24enum ReqfileLine {
25    /// The regular requirement line.
26    Unspecified(BaseReqfileLine),
27    /// A line with the prefix '+', that forces it and its dependents to all be required. 
28    /// Used to force a prereq of an optional req to be required. 
29    ForceRequired(BaseReqfileLine),
30    /// A line with the prefix 'n ;', where n is an integer from 0-5. Marks the req as optional
31    /// and assigns n as the weight. Recursively marks all prereqs as optional and ties their obtainment
32    /// to each other.  
33    Optional { base: BaseReqfileLine, weight: i64 }
34}
35
36/// The parsed representation of a reqfile
37#[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
87// optional_line = weight ';' base_reqfile_line
88fn 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
99// force_reqfile_line = '+' base_reqfile_line
100fn 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
106// base_reqfile_line = dependency_with_identifier | requirement
107fn 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
117// dependency_with_identifier = identifier (',' identifier)* '=>' identifier eof
118// links prereqs to an existing named requirement (no inline definition)
119fn 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                // TODO! DependencyWithId should actually be a top level enum variant.
170                // since its not affected by required, forced, unmarked semantics
171                // so yea for now we error if the user misuses the api (FOR NOW)
172                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            // only lf anon reqs
209            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                    // if any one of them has prereqs, we want to raise this err
217                    && (!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    // a required req cannot have an optional prereq
310    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
458// TODO! this should really be the only entry point to create a Reqfile, 
459// since it also validates if the payload will be semantically correct
460pub 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
496/// Parse '.req' files into a Reqfile struct
497pub 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
505/// Generate a reqfile string from a Reqfile struct. This is outdated and
506/// does not preserve optional groups or forced required annotations.
507pub 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    // remove spaces from names
514    //
515    // we also give anonymous reqs with prereqs an identifier
516    // (we don't assign names to potentially unnammed prereqs bc
517    // it is a requirement that prereqs are already named)
518
519    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}