initiative_core/world/command/
parse.rs1use crate::utils::{capitalize, quoted_words, CaseInsensitiveStr};
2use crate::world::command::ParsedThing;
3use crate::world::npc::NpcData;
4use crate::world::place::PlaceData;
5use crate::world::Field;
6use std::str::FromStr;
7
8fn split_name(input: &str) -> Option<(&str, &str)> {
9 let (named, comma) = quoted_words(input).fold((None, None), |(named, comma), word| {
10 if named.is_none() && word.as_str().in_ci(&["named", "called"]) {
11 (Some(word), comma)
12 } else if word.as_str().ends_with(',') {
13 (named, Some(word))
14 } else {
15 (named, comma)
16 }
17 });
18
19 let (name, description) = if let Some(word) = named {
20 (&input[word.range().end..], &input[..word.range().start])
22 } else if let Some(word) = comma {
23 (
25 input[..word.range().end].trim_end_matches(','),
26 &input[word.range().end..],
27 )
28 } else {
29 return None;
30 };
31
32 if let (Some(name_start), Some(name_end)) =
33 quoted_words(name).fold((None, None), |(name_start, _), word| {
34 (
35 name_start.or_else(|| Some(word.range().start)),
36 Some(word.range().end),
37 )
38 })
39 {
40 let name = &name[name_start..name_end];
41 if let Some(name_stripped) = name.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
42 Some((name_stripped, description))
43 } else {
44 Some((name, description))
45 }
46 } else {
47 None
48 }
49}
50
51impl FromStr for ParsedThing<PlaceData> {
52 type Err = ();
53
54 fn from_str(input: &str) -> Result<Self, Self::Err> {
55 let mut place = PlaceData::default();
56 let mut unknown_words = Vec::new();
57 let mut word_count = 0;
58
59 let description = if let Some((name, description)) = split_name(input) {
60 place.name = Field::new(capitalize(name));
61 description
62 } else {
63 input
64 };
65
66 for word in quoted_words(description) {
67 let word_str = &word.as_str();
68 word_count += 1;
69
70 if word_str.in_ci(&["a", "an"]) {
71 word_count -= 1;
72 } else if let Ok(place_type) = word_str.parse() {
73 place.subtype = Field::new(place_type);
74 } else {
75 unknown_words.push(word.range().to_owned());
76 }
77 }
78
79 if unknown_words.is_empty() || unknown_words.len() <= word_count / 2 {
80 Ok(ParsedThing {
81 thing_data: place,
82 unknown_words,
83 word_count,
84 })
85 } else {
86 Err(())
87 }
88 }
89}
90
91impl FromStr for ParsedThing<NpcData> {
92 type Err = ();
93
94 fn from_str(input: &str) -> Result<Self, Self::Err> {
95 let mut npc = NpcData::default();
96 let mut unknown_words = Vec::new();
97 let mut word_count = 0;
98
99 let description = if let Some((name, description)) = split_name(input) {
100 npc.name = Field::new(capitalize(name));
101 description
102 } else {
103 input
104 };
105
106 for word in quoted_words(description) {
107 let word_str = &word.as_str();
108 word_count += 1;
109
110 if word_str.in_ci(&["a", "an"]) {
111 word_count -= 1;
112 } else if word_str.in_ci(&["character", "npc", "person"]) {
113 } else if let Ok(gender) = word_str.parse() {
115 npc.gender = Field::new(gender);
116
117 if let Ok(age) = word_str.parse() {
118 npc.age.replace(age);
122 npc.age.lock();
123 }
124 } else if let Ok(age) = word_str.parse() {
125 npc.age = Field::new(age);
126 } else if let Ok(species) = word_str.parse() {
127 npc.species = Field::new(species);
128
129 if let Ok(ethnicity) = word_str.parse() {
130 npc.ethnicity.replace(ethnicity);
131 npc.ethnicity.lock();
132 }
133 } else if let Ok(ethnicity) = word_str.parse() {
134 npc.ethnicity = Field::new(ethnicity);
135 } else if let Some(Ok(age_years)) =
136 word_str.strip_suffix_ci("-year-old").map(|s| s.parse())
137 {
138 npc.age_years = Field::new(age_years);
139 } else {
140 unknown_words.push(word.range().to_owned());
141 }
142 }
143
144 if unknown_words.is_empty() || unknown_words.len() <= word_count / 2 {
145 Ok(ParsedThing {
146 thing_data: npc,
147 unknown_words,
148 word_count,
149 })
150 } else {
151 Err(())
152 }
153 }
154}
155
156#[cfg(test)]
157mod test {
158 use super::*;
159 use crate::world::npc::{Age, Gender, Species};
160 use crate::world::place::PlaceType;
161
162 #[test]
163 fn place_from_str_test() {
164 {
165 let place: ParsedThing<PlaceData> = "inn".parse().unwrap();
166 assert_eq!(
167 Field::Locked("inn".parse::<PlaceType>().ok()),
168 place.thing_data.subtype,
169 );
170 assert_eq!(0, place.unknown_words.len());
171 assert_eq!(1, place.word_count);
172 }
173
174 {
175 let place = "building named foo bar"
176 .parse::<ParsedThing<PlaceData>>()
177 .unwrap();
178 assert_eq!(
179 Some("Foo bar"),
180 place.thing_data.name.value().map(|s| s.as_str()),
181 );
182 assert_eq!(0, place.unknown_words.len());
183 assert_eq!(1, place.word_count);
184 }
185
186 {
187 let place: ParsedThing<PlaceData> = "The Prancing Pony, an inn".parse().unwrap();
188 assert_eq!(
189 Field::Locked(Some("The Prancing Pony".to_string())),
190 place.thing_data.name,
191 );
192 assert_eq!(
193 Field::Locked("inn".parse::<PlaceType>().ok()),
194 place.thing_data.subtype,
195 );
196 assert_eq!(0, place.unknown_words.len());
197 assert_eq!(1, place.word_count);
198 }
199
200 {
201 let place: ParsedThing<PlaceData> = "\"The Prancing Pony\", an inn".parse().unwrap();
202 assert_eq!(
203 Field::Locked(Some("The Prancing Pony".to_string())),
204 place.thing_data.name,
205 );
206 assert_eq!(
207 Field::Locked("inn".parse::<PlaceType>().ok()),
208 place.thing_data.subtype,
209 );
210 assert_eq!(0, place.unknown_words.len());
211 assert_eq!(1, place.word_count);
212 }
213
214 {
215 let place: ParsedThing<PlaceData> = "a place called home".parse().unwrap();
216 assert_eq!(
217 Field::Locked(Some("Home".to_string())),
218 place.thing_data.name
219 );
220 assert_eq!(Some(&PlaceType::Any), place.thing_data.subtype.value());
221 assert_eq!(0, place.unknown_words.len());
222 assert_eq!(1, place.word_count);
223 }
224 }
225
226 #[test]
227 fn npc_from_str_test() {
228 {
229 let npc: ParsedThing<NpcData> = "npc".parse().unwrap();
230 assert_eq!(NpcData::default(), npc.thing_data);
231 assert_eq!(0, npc.unknown_words.len());
232 assert_eq!(1, npc.word_count);
233 }
234 assert_eq!(
235 "npc".parse::<ParsedThing<NpcData>>().unwrap(),
236 "NPC".parse::<ParsedThing<NpcData>>().unwrap(),
237 );
238
239 {
240 let npc: ParsedThing<NpcData> = "elf".parse().unwrap();
241 assert_eq!(Field::Locked(Some(Species::Elf)), npc.thing_data.species);
242 assert_eq!(0, npc.unknown_words.len());
243 assert_eq!(1, npc.word_count);
244 }
245 assert_eq!(
246 "elf".parse::<ParsedThing<NpcData>>().unwrap(),
247 "ELF".parse::<ParsedThing<NpcData>>().unwrap(),
248 );
249
250 {
251 let npc: ParsedThing<NpcData> = "Potato Johnson, a non-binary elf".parse().unwrap();
252 assert_eq!(
253 Field::Locked(Some("Potato Johnson".to_string())),
254 npc.thing_data.name,
255 );
256 assert_eq!(Field::Locked(Some(Species::Elf)), npc.thing_data.species);
257 assert_eq!(
258 Field::Locked(Some(Gender::NonBinaryThey)),
259 npc.thing_data.gender
260 );
261 assert_eq!(0, npc.unknown_words.len());
262 assert_eq!(2, npc.word_count);
263 }
264 assert_eq!(
265 "Potato Johnson, a non-binary elf"
266 .parse::<ParsedThing<NpcData>>()
267 .unwrap(),
268 "Potato Johnson, A NON-BINARY ELF"
269 .parse::<ParsedThing<NpcData>>()
270 .unwrap(),
271 );
272
273 {
274 let npc: ParsedThing<NpcData> = "37-year-old boy named sue".parse().unwrap();
275 assert_eq!(Field::Locked(Some("Sue".to_string())), npc.thing_data.name);
276 assert_eq!(
277 Field::Locked(Some(Gender::Masculine)),
278 npc.thing_data.gender
279 );
280 assert_eq!(Field::Locked(Some(Age::Child)), npc.thing_data.age);
281 assert_eq!(Field::Locked(Some(37)), npc.thing_data.age_years);
282 assert_eq!(0, npc.unknown_words.len());
283 assert_eq!(2, npc.word_count);
284 }
285 assert_eq!(
286 "37-year-old boy named sue"
287 .parse::<ParsedThing<NpcData>>()
288 .unwrap(),
289 "37-YEAR-OLD BOY NAMED sue"
290 .parse::<ParsedThing<NpcData>>()
291 .unwrap(),
292 );
293
294 {
295 assert!("potato".parse::<ParsedThing<NpcData>>().is_err());
296 }
297 }
298}