deepwoken_reqparse/parse/
req.rs1use 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
10pub 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
36pub(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
53fn 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
67fn 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
77pub(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
85fn bare_requirement(input: &mut &str) -> ModalResult<Requirement> {
87 let clauses: Vec<Clause> = alt((
88 ('(', multispace0, ')').map(|_| Vec::new()),
90 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
102fn clause(input: &mut &str) -> ModalResult<Clause> {
107 let _ = multispace0.parse_next(input)?;
108
109 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 let atom = first.into_atom(false);
132 Ok(Clause::and().atom(atom))
133 } else {
134 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
145struct 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 Reducability::Reducible
158 } else if self.stats.len() > 1 {
159 Reducability::Reducible
161 } else {
162 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
185fn 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, single_expr_prefix, ))
195 .parse_next(input)?;
196
197 let _ = multispace0.parse_next(input)?;
198
199 Ok(result)
200}
201
202fn 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
227fn 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
255fn 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
271fn 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 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 for (i, req) in parsed.iter().enumerate() {
348 assert_eq!(req.clauses.len(), 2, "variant {} should have 2 clauses", i);
349 }
350
351 for i in 1..parsed.len() {
353 assert_eq!(parsed[0], parsed[i], "variant 0 should equal variant {}", i);
354 }
355
356 let req = &parsed[0];
358
359 let clause1 = &req.clauses[0];
361 assert_eq!(clause1.clause_type, ClauseType::Or);
362 assert_eq!(clause1.atoms.len(), 2);
363
364 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 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 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 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}