Skip to main content

deepwoken_reqparse/parse/
reqfile.rs

1use 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
23/// A full reqfile line.
24/// Note a required requirement cannot have optional prereqs.
25enum ReqfileLine {
26    /// The regular requirement line.
27    Unspecified(BaseReqfileLine),
28    /// A line with the prefix '+', that forces it and its dependents to all be required. 
29    /// Used to force a prereq of an optional req to be required. 
30    ForceRequired(BaseReqfileLine),
31    /// A line with the prefix 'n ;', where n is an integer from 0-5. Marks the req as optional
32    /// and assigns n as the weight. Recursively marks all prereqs as optional and ties their obtainment
33    /// to each other.  
34    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
79// optional_line = weight ';' base_reqfile_line
80fn 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
91// force_reqfile_line = '+' base_reqfile_line
92fn 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
98// base_reqfile_line = dependency_with_identifier | requirement
99fn 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
109// dependency_with_identifier = identifier (',' identifier)* '=>' identifier eof
110// links prereqs to an existing named requirement (no inline definition)
111fn 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                // TODO! DependencyWithId should actually be a top level enum variant.
162                // since its not affected by required, forced, unmarked semantics
163                // so yea for now we error if the user misuses the api (FOR NOW)
164                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            // only lf anon reqs
201            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                    // if any one of them has prereqs, we want to raise this err
209                    && (!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    // a required req cannot have an optional prereq
302    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
450// TODO! this should really be the only entry point to create a Reqfile, 
451// since it also validates if the payload will be semantically correct
452pub 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
488/// Parse '.req' files into a Reqfile struct
489pub 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
497/// Generate a reqfile string from a Reqfile struct. This is outdated and
498/// does not preserve optional groups or forced required annotations.
499pub 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    // remove spaces from names
506    //
507    // we also give anonymous reqs with prereqs an identifier
508    // (we don't assign names to potentially unnammed prereqs bc
509    // it is a requirement that prereqs are already named)
510
511    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}