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> {
28 let input = input.trim();
29 requirement
30 .parse(&input)
31 .map_err(|e| ReqparseError::Req(e.to_string()))
32}
33
34pub(crate) fn requirement(input: &mut &str) -> ModalResult<Requirement> {
37 let _ = multispace0.parse_next(input)?;
38
39 let name = opt((identifier, multispace0, ":=", multispace0)).parse_next(input)?;
41
42 let mut req = bare_requirement.parse_next(input)?;
44
45 if let Some((name, _, _, _)) = name {
46 req.name = Some(name);
47 }
48
49 Ok(req)
50}
51
52pub(crate) fn identifier(input: &mut &str) -> ModalResult<String> {
54 let first: char = one_of(('A'..='Z', 'a'..='z', '_')).parse_next(input)?;
55 let rest: String =
56 repeat(0.., one_of(('A'..='Z', 'a'..='z', '0'..='9', '_'))).parse_next(input)?;
57 Ok(format!("{}{}", first, rest))
58}
59
60fn bare_requirement(input: &mut &str) -> ModalResult<Requirement> {
62 let clauses: Vec<Clause> = alt((
63 ('(', multispace0, ')').map(|_| Vec::new()),
65 separated(1.., clause, (multispace0, ',', multispace0)),
67 ))
68 .parse_next(input)?;
69
70 Ok(Requirement {
71 name: None,
72 prereqs: Vec::new(),
73 clauses,
74 })
75}
76
77fn clause(input: &mut &str) -> ModalResult<Clause> {
82 let _ = multispace0.parse_next(input)?;
83
84 let result = alt((
86 delimited(('(', multispace0), clause_inner, (multispace0, ')')),
87 clause_inner,
88 ))
89 .parse_next(input)?;
90
91 let _ = multispace0.parse_next(input)?;
92
93 Ok(result)
94}
95
96fn clause_inner(input: &mut &str) -> ModalResult<Clause> {
97 let first = atom.parse_next(input)?;
98 let rest: Vec<ParsedAtom> = repeat(
99 0..,
100 preceded((multispace0, Caseless("OR"), multispace0), atom),
101 )
102 .parse_next(input)?;
103
104 if rest.is_empty() {
105 let atom = first.into_atom(false);
107 Ok(Clause::and().atom(atom))
108 } else {
109 let mut clause = Clause::or();
111 clause = clause.atom(first.into_atom(true));
112 for parsed in rest {
113 clause = clause.atom(parsed.into_atom(true));
114 }
115
116 Ok(clause)
117 }
118}
119
120struct ParsedAtom {
122 stats: Vec<Stat>,
123 value: i64,
124 reducability: Option<Reducability>,
125}
126
127impl ParsedAtom {
128 fn into_atom(self, is_or: bool) -> Atom {
129 let reducability = self.reducability.unwrap_or_else(|| {
130 if is_or {
131 Reducability::Reducible
133 } else if self.stats.len() > 1 {
134 Reducability::Reducible
136 } else {
137 Reducability::Strict
139 }
140 });
141
142 if reducability == Reducability::Strict && self.stats.len() > 1 {
143 warn!(
144 "You have specified a strict SUM requirement, please note that \
145 there is a BUG regarding this in the solver code that makes that atom \
146 restrict SOM usage incorrectly. I am \
147 too lazy to fix atm bc nothing will ever need it. \
148 You probably don't need it anyways."
149 )
150 }
151
152 let mut atom = Atom::new(reducability).value(self.value);
153
154 for stat in self.stats {
155 atom.add_stat(stat);
156 }
157
158 atom
159 }
160}
161
162fn atom(input: &mut &str) -> ModalResult<ParsedAtom> {
164 let _ = multispace0.parse_next(input)?;
165
166 let result = alt((
167 sum_expr_parens,
168 sum_expr_no_parens,
169 single_expr_eq, single_expr_prefix, ))
172 .parse_next(input)?;
173
174 let _ = multispace0.parse_next(input)?;
175
176 Ok(result)
177}
178
179fn sum_expr_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
181 let _ = '('.parse_next(input)?;
182 let _ = multispace0.parse_next(input)?;
183
184 let stats: Vec<Stat> =
185 separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
186
187 let _ = multispace0.parse_next(input)?;
188 let _ = '='.parse_next(input)?;
189 let _ = multispace0.parse_next(input)?;
190
191 let value = number.parse_next(input)?;
192 let reducability = opt(reducability_marker).parse_next(input)?;
193
194 let _ = multispace0.parse_next(input)?;
195 let _ = ')'.parse_next(input)?;
196
197 Ok(ParsedAtom {
198 stats,
199 value,
200 reducability,
201 })
202}
203
204fn sum_expr_no_parens(input: &mut &str) -> ModalResult<ParsedAtom> {
207 let first = stat.parse_next(input)?;
208 let _ = multispace0.parse_next(input)?;
209 let _ = '+'.parse_next(input)?;
210 let _ = multispace0.parse_next(input)?;
211
212 let rest: Vec<Stat> =
213 separated(1.., stat, (multispace0, '+', multispace0)).parse_next(input)?;
214
215 let _ = multispace0.parse_next(input)?;
216 let _ = '='.parse_next(input)?;
217 let _ = multispace0.parse_next(input)?;
218
219 let value = number.parse_next(input)?;
220 let reducability = opt(reducability_marker).parse_next(input)?;
221
222 let mut stats = vec![first];
223 stats.extend(rest);
224
225 Ok(ParsedAtom {
226 stats,
227 value,
228 reducability,
229 })
230}
231
232fn single_expr_eq(input: &mut &str) -> ModalResult<ParsedAtom> {
234 let s = stat.parse_next(input)?;
235 let _ = multispace0.parse_next(input)?;
236 let _ = '='.parse_next(input)?;
237 let _ = multispace0.parse_next(input)?;
238 let value = number.parse_next(input)?;
239 let reducability = opt(reducability_marker).parse_next(input)?;
240
241 Ok(ParsedAtom {
242 stats: vec![s],
243 value,
244 reducability,
245 })
246}
247
248fn single_expr_prefix(input: &mut &str) -> ModalResult<ParsedAtom> {
250 let value = number.parse_next(input)?;
251 let reducability = opt(reducability_marker).parse_next(input)?;
252 let _ = multispace0.parse_next(input)?;
253 let s = stat.parse_next(input)?;
254
255 Ok(ParsedAtom {
256 stats: vec![s],
257 value,
258 reducability,
259 })
260}
261
262fn reducability_marker(input: &mut &str) -> ModalResult<Reducability> {
263 let c = one_of(['S', 'R', 's', 'r']).parse_next(input)?;
264 Ok(match c {
265 'S' | 's' => Reducability::Strict,
266 'R' | 'r' => Reducability::Reducible,
267 _ => unreachable!(),
268 })
269}
270
271fn number(input: &mut &str) -> ModalResult<i64> {
272 digit1.try_map(|s: &str| s.parse::<i64>()).parse_next(input)
273}
274
275fn stat(input: &mut &str) -> ModalResult<Stat> {
276 alpha1
277 .verify_map(|s: &str| {
278 let upper = s.to_uppercase();
279 Stat::from_short_name(&upper)
280 })
281 .parse_next(input)
282}
283
284#[cfg(test)]
285mod tests {
286 use crate::model::req::ClauseType;
287
288 use super::*;
289
290 #[test]
291 fn reinforced_armor() {
292 let req = parse_req("90 FTD").unwrap();
293 assert_eq!(req.clauses.len(), 1);
294
295 let clause = &req.clauses[0];
296 assert_eq!(clause.clause_type, ClauseType::And);
297 assert_eq!(clause.atoms.len(), 1);
298
299 let atom = clause.atoms.iter().next().unwrap();
300 assert!(atom.stats.contains(&Stat::Fortitude));
301 assert_eq!(atom.value, 90);
302 assert_eq!(atom.reducability, Reducability::Strict);
303 }
304
305 #[test]
306 fn bladeharper_variants() {
307 let variants = [
310 "25 STR OR 25 AGL, 75 MED OR (LHT + MED + HVY = 90)",
311 "(25 STR OR 25 AGL), (75 MED OR (LHT + MED + HVY = 90))",
312 "STR = 25 OR AGL = 25, 75 MED OR (LHT + MED + HVY = 90)",
313 "(STR = 25 OR AGL = 25), (75 MED OR (LHT + MED + HVY = 90))",
314 "(STR = 25 OR AGL = 25),(75 MED OR (LHT + MED + HVY = 90))",
315 "STR=25 OR AGL= 25,med=75 OR (lht + MED +hvy = 90)",
316 ];
317
318 let parsed: Vec<Requirement> = variants
319 .iter()
320 .map(|s| parse_req(s).expect(&format!("Failed to parse: {}", s)))
321 .collect();
322
323 for (i, req) in parsed.iter().enumerate() {
325 assert_eq!(req.clauses.len(), 2, "variant {} should have 2 clauses", i);
326 }
327
328 for i in 1..parsed.len() {
330 assert_eq!(parsed[0], parsed[i], "variant 0 should equal variant {}", i);
331 }
332
333 let req = &parsed[0];
335
336 let clause1 = &req.clauses[0];
338 assert_eq!(clause1.clause_type, ClauseType::Or);
339 assert_eq!(clause1.atoms.len(), 2);
340
341 let clause2 = &req.clauses[1];
343 assert_eq!(clause2.clause_type, ClauseType::Or);
344 assert_eq!(clause2.atoms.len(), 2);
345 }
346
347 #[test]
348 fn bunch_of_random_stuff() {
349 parse_req("25R STR, LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
351 parse_req("(25R STR), LHT + MED + HVY = 75, 25 CHA OR 25 AGL").unwrap();
352 parse_req("silentheart := str=25r,lht+med+hvy=75,25CHA OR agl=25r").unwrap();
353 parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").unwrap();
354 assert!(parse_req("silentheart := (str=25r),lht+med+hvy=75,25CHA OR agl=25r").is_ok());
355 assert!(parse_req("35cha OR 35wll OR 35int").is_ok());
360 assert!(parse_req("35 cha OR 35 wll OR 35 int").is_ok());
361 assert!(parse_req("()").unwrap().is_empty());
362
363 assert!(parse_req("(35 cha").is_err());
365 assert!(parse_req("35 SBF").is_err());
366 assert!(parse_req("35CHAOR35WLL").is_err());
367 }
368
369 #[test]
370 fn explicit_reducability() {
371 let req = parse_req("25S STR").unwrap();
372 let atom = req.clauses[0].atoms.iter().next().unwrap();
373 assert_eq!(atom.reducability, Reducability::Strict);
374
375 let req = parse_req("25R STR").unwrap();
376 let atom = req.clauses[0].atoms.iter().next().unwrap();
377 assert_eq!(atom.reducability, Reducability::Reducible);
378
379 let req = parse_req("25S STR OR 25R AGL").unwrap();
380 assert_eq!(req.clauses[0].clause_type, ClauseType::Or);
381 }
382
383 #[test]
384 fn casing_and_compactness() {
385 let req1 = parse_req("25 str or 25 agl").unwrap();
386 let req2 = parse_req("25 STR or 25 AGL").unwrap();
387 assert_eq!(req1, req2);
388
389 assert!(parse_req("25 Str OR 25 AgL").is_ok());
390
391 assert!(parse_req("lht+hvy=90").is_ok());
392 assert!(parse_req("lht+med+hvy=90").is_ok());
393 assert!(parse_req("25 STR OR AGL=25,75S MED OR (LHT+MED+HVY=90)").is_ok());
394
395 let compact = parse_req("str=25 OR agl=25").unwrap();
396 let spaced = parse_req("STR = 25 OR AGL = 25").unwrap();
397 assert_eq!(compact, spaced);
398 }
399}