Skip to main content

deepwoken_reqparse/parse/
req.rs

1use crate::model::req::{Atom, Clause, Reducability, Requirement};
2use crate::error::{ReqparseError, Result };
3use crate::Stat;
4use log::warn;
5use winnow::ascii::{alpha1, digit1, multispace0, Caseless};
6use winnow::combinator::{alt, delimited, opt, preceded, repeat, separated};
7use winnow::prelude::*;
8use winnow::token::one_of;
9
10/// Parse a string into a Requirement
11///
12/// If reducibility is unspecified:
13/// - Unspecified atoms in OR clauses are reducible
14/// - Unspecified atoms in AND clauses are strict iff they have a singular stat.
15/// - Unspecified atoms in AND clauses are reducible if they are SUM types
16///
17/// Examples:
18/// - "90 FTD" -> AND clause with strict 90 Fortitude
19/// - "FTD = 90" -> Same thing but diff syntax, "ftd=90", "90ftd" also are valid
20/// - "25 STR OR 25 AGL" -> OR clause with reducible atoms
21/// - "25S STR OR 25 AGL" -> OR clause with asymmetric reducability
22/// - "(LHT + MED + HVY = 90)" -> AND clause with sum atom (reducible by default)
23/// - "(LHT + MED + HVY = 90S)" -> Any stat that make up the sum cannot be reduced
24/// - "25S STR" -> strict atom
25/// - "25R STR" -> reducible atom
26/// - "reinforced = 90 FTD" -> named requirement (assignment syntax)
27/// - "base, armor => reinforced := 90 FTD" -> named requirement with prerequisites
28/// - "base => 90 FTD" -> anonymous requirement with a prerequisite
29pub fn parse_req(input: &str) -> Result<Requirement> {
30    let input = input.trim();
31    requirement
32        .parse(&input)
33        .map_err(|e| ReqparseError::Req(e.to_string()))
34}
35
36// requirement = prefix? bare_requirement
37// prefix = prereq_prefix | name_prefix
38pub(crate) fn requirement(input: &mut &str) -> ModalResult<Requirement> {
39    let _ = multispace0.parse_next(input)?;
40
41    let prefix = opt(alt((prereq_prefix, name_prefix))).parse_next(input)?;
42
43    let mut req = bare_requirement.parse_next(input)?;
44
45    if let Some((prereqs, name)) = prefix {
46        req.prereqs = prereqs;
47        req.name = name;
48    }
49
50    Ok(req)
51}
52
53// prereq_prefix = identifier (',' identifier)* '=>' (identifier ':=')?
54fn prereq_prefix(input: &mut &str) -> ModalResult<(Vec<String>, Option<String>)> {
55    let prereqs: Vec<String> =
56        separated(1.., identifier, (multispace0, ',', multispace0)).parse_next(input)?;
57
58    let _ = multispace0.parse_next(input)?;
59    let _ = "=>".parse_next(input)?;
60    let _ = multispace0.parse_next(input)?;
61
62    let name = opt((identifier, multispace0, ":=", multispace0)).parse_next(input)?;
63
64    Ok((prereqs, name.map(|(n, _, _, _)| n)))
65}
66
67// name_prefix = identifier ':='
68fn name_prefix(input: &mut &str) -> ModalResult<(Vec<String>, Option<String>)> {
69    let name = identifier.parse_next(input)?;
70    let _ = multispace0.parse_next(input)?;
71    let _ = ":=".parse_next(input)?;
72    let _ = multispace0.parse_next(input)?;
73
74    Ok((Vec::new(), Some(name)))
75}
76
77// identifier = alpha (alpha | digit | '_')*
78pub(crate) fn identifier(input: &mut &str) -> ModalResult<String> {
79    let first: char = one_of(('A'..='Z', 'a'..='z', '_')).parse_next(input)?;
80    let rest: String =
81        repeat(0.., one_of(('A'..='Z', 'a'..='z', '0'..='9', '_'))).parse_next(input)?;
82    Ok(format!("{}{}", first, rest))
83}
84
85// requirement = '(' ')' | clause (',' clause)*
86fn bare_requirement(input: &mut &str) -> ModalResult<Requirement> {
87    let clauses: Vec<Clause> = alt((
88        // if () then its an empty req
89        ('(', multispace0, ')').map(|_| Vec::new()),
90        // Normal: 1+ clauses (clauses can have their own parens)
91        separated(1.., clause, (multispace0, ',', multispace0)),
92    ))
93    .parse_next(input)?;
94
95    Ok(Requirement {
96        name: None,
97        prereqs: Vec::new(),
98        clauses,
99    })
100}
101
102// clause = '(' clause_inner ')' | clause_inner
103// clause_inner = atom ('OR' atom)*
104// TODO! this is lacking an explicit 'AND', though you
105// can just implicitly create new ANDs by making a new single atom clause!
106fn clause(input: &mut &str) -> ModalResult<Clause> {
107    let _ = multispace0.parse_next(input)?;
108
109    // try (clause) first
110    let result = alt((
111        delimited(('(', multispace0), clause_inner, (multispace0, ')')),
112        clause_inner,
113    ))
114    .parse_next(input)?;
115
116    let _ = multispace0.parse_next(input)?;
117
118    Ok(result)
119}
120
121fn clause_inner(input: &mut &str) -> ModalResult<Clause> {
122    let first = atom.parse_next(input)?;
123    let rest: Vec<ParsedAtom> = repeat(
124        0..,
125        preceded((multispace0, Caseless("OR"), multispace0), atom),
126    )
127    .parse_next(input)?;
128
129    if rest.is_empty() {
130        // single atom -> AND clause
131        let atom = first.into_atom(false);
132        Ok(Clause::and().atom(atom))
133    } else {
134        // multiple atoms -> OR clause (no AND support YET..)
135        let mut clause = Clause::or();
136        clause = clause.atom(first.into_atom(true));
137        for parsed in rest {
138            clause = clause.atom(parsed.into_atom(true));
139        }
140
141        Ok(clause)
142    }
143}
144
145// intermediate atom structure
146struct ParsedAtom {
147    stats: Vec<Stat>,
148    value: i64,
149    reducability: Option<Reducability>,
150}
151
152impl ParsedAtom {
153    fn into_atom(self, is_or: bool) -> Atom {
154        let reducability = self.reducability.unwrap_or_else(|| {
155            if is_or {
156                // OR clause atoms default to reducible
157                Reducability::Reducible
158            } else if self.stats.len() > 1 {
159                // multi-stat (sum) AND atoms default to reducible
160                Reducability::Reducible
161            } else {
162                // single stat AND atoms default to strict
163                Reducability::Strict
164            }
165        });
166
167        if reducability == Reducability::Strict && self.stats.len() > 1 {
168            warn!(
169                "You have specified a strict SUM requirement, please note that \
170                strict SUM requirements' semantics are not properly defined currently. \
171                You probably don't need it anyways."
172            )
173        }
174
175        let mut atom = Atom::new(reducability).value(self.value);
176
177        for stat in self.stats {
178            atom.add_stat(stat);
179        }
180
181        atom
182    }
183}
184
185// atom = sum_expr | single_expr
186fn atom(input: &mut &str) -> ModalResult<ParsedAtom> {
187    let _ = multispace0.parse_next(input)?;
188
189    let result = alt((
190        sum_expr_parens,
191        sum_expr_no_parens,
192        single_expr_eq,     // stat '=' value reducability?
193        single_expr_prefix, // value reducability? stat
194    ))
195    .parse_next(input)?;
196
197    let _ = multispace0.parse_next(input)?;
198
199    Ok(result)
200}
201
202// sum_expr_parens = '(' stat ('+' stat)* '=' value reducability? ')'
203fn sum_expr_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
204    let _ = '('.parse_next(input)?;
205    let _ = multispace0.parse_next(input)?;
206
207    let stats: Vec<Stat> =
208        separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
209
210    let _ = multispace0.parse_next(input)?;
211    let _ = '='.parse_next(input)?;
212    let _ = multispace0.parse_next(input)?;
213
214    let value = number.parse_next(input)?;
215    let reducability = opt(reducability_marker).parse_next(input)?;
216
217    let _ = multispace0.parse_next(input)?;
218    let _ = ')'.parse_next(input)?;
219
220    Ok(ParsedAtom {
221        stats,
222        value,
223        reducability,
224    })
225}
226
227// sum_expr_no_parens = stat '+' stat ('+' stat)* '=' value reducability?
228// needs 2 or more stats
229fn sum_expr_no_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
230    let first = stat.parse_next(input)?;
231    let _ = multispace0.parse_next(input)?;
232    let _ = '+'.parse_next(input)?;
233    let _ = multispace0.parse_next(input)?;
234
235    let rest: Vec<Stat> =
236        separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
237
238    let _ = multispace0.parse_next(input)?;
239    let _ = '='.parse_next(input)?;
240    let _ = multispace0.parse_next(input)?;
241
242    let value = number.parse_next(input)?;
243    let reducability = opt(reducability_marker).parse_next(input)?;
244
245    let mut stats = vec![first];
246    stats.extend(rest);
247
248    Ok(ParsedAtom {
249        stats,
250        value,
251        reducability,
252    })
253}
254
255// single_expr_eq = stat '=' value reducability?
256fn single_expr_eq(input: &mut &str) -> ModalResult<ParsedAtom> {
257    let s = stat.parse_next(input)?;
258    let _ = multispace0.parse_next(input)?;
259    let _ = '='.parse_next(input)?;
260    let _ = multispace0.parse_next(input)?;
261    let value = number.parse_next(input)?;
262    let reducability = opt(reducability_marker).parse_next(input)?;
263
264    Ok(ParsedAtom {
265        stats: vec![s],
266        value,
267        reducability,
268    })
269}
270
271// single_expr_prefix = value reducability? stat
272fn single_expr_prefix(input: &mut &str) -> ModalResult<ParsedAtom> {
273    let value = number.parse_next(input)?;
274    let reducability = opt(reducability_marker).parse_next(input)?;
275    let _ = multispace0.parse_next(input)?;
276    let s = stat.parse_next(input)?;
277
278    Ok(ParsedAtom {
279        stats: vec![s],
280        value,
281        reducability,
282    })
283}
284
285fn reducability_marker(input: &mut &str) -> ModalResult<Reducability> {
286    let c = one_of(['S', 'R', 's', 'r']).parse_next(input)?;
287    Ok(match c {
288        'S' | 's' => Reducability::Strict,
289        'R' | 'r' => Reducability::Reducible,
290        _ => unreachable!(),
291    })
292}
293
294fn number(input: &mut &str) -> ModalResult<i64> {
295    digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
296}
297
298fn stat(input: &mut &str) -> ModalResult<Stat> {
299    alpha1
300        .verify_map(|s: &str| {
301            let upper = s.to_uppercase();
302            Stat::from_short_name(&upper)
303        })
304        .parse_next(input)
305}
306
307#[cfg(test)]
308mod tests {
309    use crate::model::req::ClauseType;
310
311    use super::*;
312
313    #[test]
314    fn reinforced_armor() {
315        let req = parse_req("90 FTD").unwrap();
316        assert_eq!(req.clauses.len(), 1);
317
318        let clause = &req.clauses[0];
319        assert_eq!(clause.clause_type, ClauseType::And);
320        assert_eq!(clause.atoms.len(), 1);
321
322        let atom = clause.atoms.iter().next().unwrap();
323        assert!(atom.stats.contains(&Stat::Fortitude));
324        assert_eq!(atom.value, 90);
325        assert_eq!(atom.reducability, Reducability::Strict);
326    }
327
328    #[test]
329    fn bladeharper_variants() {
330        // all valid representations of bladeharper requirements
331        // for testing syntax stuff
332        let variants = [
333            "25 STR OR 25 AGL, 75 MED OR (LHT + MED + HVY = 90)",
334            "(25 STR OR 25 AGL), (75 MED OR (LHT + MED + HVY = 90))",
335            "STR = 25 OR AGL = 25, 75 MED OR (LHT + MED + HVY = 90)",
336            "(STR = 25 OR AGL = 25), (75 MED OR (LHT + MED + HVY = 90))",
337            "(STR = 25 OR AGL = 25),(75 MED OR (LHT + MED + HVY = 90))",
338            "STR=25 OR AGL= 25,med=75 OR (lht + MED +hvy = 90)",
339        ];
340
341        let parsed: Vec<Requirement> = variants
342            .iter()
343            .map(|s| parse_req(s).expect(&format!("Failed to parse: {}", s)))
344            .collect();
345
346        // verify all parse successfully and are equal
347        for (i, req) in parsed.iter().enumerate() {
348            assert_eq!(req.clauses.len(), 2, "variant {} should have 2 clauses", i);
349        }
350
351        // all variants should be equal to each other
352        for i in 1..parsed.len() {
353            assert_eq!(parsed[0], parsed[i], "variant 0 should equal variant {}", i);
354        }
355
356        // verify structure of one of them (then they all are correct)
357        let req = &parsed[0];
358
359        // first clause: 25 STR OR 25 AGL
360        let clause1 = &req.clauses[0];
361        assert_eq!(clause1.clause_type, ClauseType::Or);
362        assert_eq!(clause1.atoms.len(), 2);
363
364        // second clause: 75 MED OR (LHT + MED + HVY = 90)
365        let clause2 = &req.clauses[1];
366        assert_eq!(clause2.clause_type, ClauseType::Or);
367        assert_eq!(clause2.atoms.len(), 2);
368    }
369
370    #[test]
371    fn bunch_of_random_stuff() {
372        // silentheart reqs
373        parse_req("25R STR, LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
374        parse_req("(25R STR), LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
375        parse_req("silentheart := str=25r,lht+med+hvy=75,25CHA OR agl=25r").unwrap();
376        parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").unwrap();
377        assert!(parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").is_ok());
378
379        // neuro reqs
380        assert!(parse_req("35cha OR 35wll OR 35int").is_ok());
381        assert!(parse_req("35 cha OR 35 wll OR 35 int").is_ok());
382        assert!(parse_req("()").unwrap().is_empty());
383
384        // INVALID BAD REQ
385        assert!(parse_req("(35 cha").is_err());
386        assert!(parse_req("35 SBF").is_err());
387        assert!(parse_req("35CHAOR35WLL").is_err());
388    }
389
390    #[test]
391    fn explicit_reducability() {
392        let req = parse_req("25S STR").unwrap();
393        let atom = req.clauses[0].atoms.iter().next().unwrap();
394        assert_eq!(atom.reducability, Reducability::Strict);
395
396        let req = parse_req("25R STR").unwrap();
397        let atom = req.clauses[0].atoms.iter().next().unwrap();
398        assert_eq!(atom.reducability, Reducability::Reducible);
399
400        let req = parse_req("25S STR OR 25R AGL").unwrap();
401        assert_eq!(req.clauses[0].clause_type, ClauseType::Or);
402    }
403
404    #[test]
405    fn prereq_prefix_parsing() {
406        let req = parse_req("base, armor => reinforced := 90 FTD").unwrap();
407        assert_eq!(req.prereqs, vec!["base", "armor"]);
408        assert_eq!(req.name, Some("reinforced".to_string()));
409        assert_eq!(req.clauses.len(), 1);
410
411        let req = parse_req("base => 90 FTD").unwrap();
412        assert_eq!(req.prereqs, vec!["base"]);
413        assert!(req.name.is_none());
414
415        let req = parse_req("base, armor => 50 INT, 25 STR OR 25 AGL").unwrap();
416        assert_eq!(req.prereqs, vec!["base", "armor"]);
417        assert_eq!(req.clauses.len(), 2);
418    }
419
420    #[test]
421    fn casing_and_compactness() {
422        let req1 = parse_req("25 str or 25 agl").unwrap();
423        let req2 = parse_req("25 STR or 25 AGL").unwrap();
424        assert_eq!(req1, req2);
425
426        assert!(parse_req("25 Str OR 25 AgL").is_ok());
427
428        assert!(parse_req("lht+hvy=90").is_ok());
429        assert!(parse_req("lht+med+hvy=90").is_ok());
430        assert!(parse_req("25 STR OR AGL=25,75S MED OR (LHT+MED+HVY=90)").is_ok());
431
432        let compact = parse_req("str=25 OR agl=25").unwrap();
433        let spaced = parse_req("STR = 25 OR AGL = 25").unwrap();
434        assert_eq!(compact, spaced);
435    }
436}