use crate::config::directions::Directions;
use crate::config::{Item, State, Subject, Verb};
use regex::Regex;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq)]
pub enum ActionType {
Verb,
VerbSubject,
VerbItem,
VerbItemSubject,
Invalid,
Movement,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct Action {
pub verb: Option<Verb>,
pub subject: Option<Subject>,
pub item: Option<Item>,
pub movement: Option<Directions>,
pub command_tokens: Vec<String>,
pub input: String,
}
impl Action {
pub fn is_valid(&self) -> bool {
if self.verb.is_some() {
self.item.is_some()
|| self.subject.is_some()
|| self.movement.is_some()
|| self.verb.is_some()
} else {
self.movement.is_some()
}
}
pub fn action_type(&self) -> ActionType {
if self.is_valid() && self.verb.is_some() && self.item.is_some() && self.subject.is_some() {
ActionType::VerbItemSubject
} else if self.is_valid() && self.verb.is_some() && self.subject.is_some() {
ActionType::VerbSubject
} else if self.is_valid() && self.verb.is_some() && self.item.is_some() {
ActionType::VerbItem
} else if self.is_valid() && self.verb.is_some() {
ActionType::Verb
} else if self.is_valid() && self.movement.is_some() {
ActionType::Movement
} else {
ActionType::Invalid
}
}
pub fn parse(state: &State, input: &str) -> Action {
let prepositions = state.config.allowed_prepositions.clone().prepositions;
let determiners = state.config.allowed_determiners.clone().determiners;
let command_tokens: Vec<String> = input
.split(' ')
.collect::<Vec<&str>>()
.iter()
.filter(|w| {
let word: String = w.to_string().to_lowercase();
!prepositions.contains(&word) && !determiners.contains(&word)
})
.map(|word| word.to_string())
.collect::<Vec<String>>();
if command_tokens.is_empty() {
Action {
item: None,
movement: None,
subject: None,
verb: None,
command_tokens: vec!["".to_string()],
input: input.to_string(),
}
} else {
parse_action(state, command_tokens, input)
}
}
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if !self.is_valid() {
return write!(f, "Invalid action: {}", self.command_tokens.join(" "));
}
match (&self.verb, &self.item, &self.subject, &self.movement) {
(Some(verb), Some(item), Some(subject), _) => {
write!(f, "{} {} {}", verb, item, subject.name)
}
(Some(verb), None, Some(subject), _) => write!(f, "{} {}", verb, subject.name),
(Some(verb), Some(item), None, _) => write!(f, "{} {}", verb, item),
(Some(verb), None, None, _) => write!(f, "{}", verb),
(None, _, _, Some(movement)) => write!(f, "{}", movement),
_ => write!(f, "Invalid action: {}", self.command_tokens.join(" ")),
}
}
}
impl From<Action> for String {
fn from(action: Action) -> String {
action.to_string()
}
}
impl From<&Action> for String {
fn from(action: &Action) -> String {
action.to_string()
}
}
fn parse_action(state: &State, command_tokens: Vec<String>, input: &str) -> Action {
let verb = extract_verb(state, &command_tokens);
let movement = extract_movement(state, &command_tokens);
let subject = extract_subject(state, &command_tokens);
let item = extract_item(state, &command_tokens, input);
Action {
verb,
movement,
item,
subject,
command_tokens,
input: input.to_string(),
}
}
fn extract_verb(state: &State, command_tokens: &[String]) -> Option<Verb> {
let verbs = state.config.allowed_verbs.clone();
verbs
.iter()
.find(|v| v.names.contains(&command_tokens[0]))
.cloned()
}
fn extract_item(state: &State, command_tokens: &[String], input: &str) -> Option<Item> {
let subjects = state.config.subjects.clone();
let items_string: String = state
.config
.items
.iter()
.map(|item| regex::escape(&item.name))
.collect::<Vec<String>>()
.join("|");
let items_regex_match = format!("({})", items_string);
let re = Regex::new(&items_regex_match)
.expect("item names are regex-escaped, so the pattern is always valid");
if command_tokens.len() > 1 && !subjects.iter().any(|s| s.name == command_tokens[1]) {
re.captures(input)
.and_then(|capture| capture.get(1))
.and_then(|matched| {
state
.config
.items
.iter()
.find(|item| item.name == matched.as_str())
.cloned()
})
} else {
None
}
}
fn extract_subject(state: &State, command_tokens: &[String]) -> Option<Subject> {
let subjects = state.config.subjects.clone();
match &command_tokens.len() {
0 | 1 => None,
2 => subjects
.iter()
.find(|s| s.name == command_tokens[1])
.cloned(),
_ => subjects
.iter()
.find(|s| s.name == command_tokens[&command_tokens.len() - 1])
.cloned(),
}
}
fn extract_movement(state: &State, command_tokens: &[String]) -> Option<Directions> {
let movements = state.config.allowed_movements.movements.clone();
let directions = state.config.allowed_directions.directions.clone();
match command_tokens.len() {
1 => match &command_tokens[0][..] {
"north" | "n" => Some(Directions::North),
"south" | "s" => Some(Directions::South),
"east" | "e" => Some(Directions::East),
"west" | "w" => Some(Directions::West),
_ => None,
},
2 => {
if movements.contains(&command_tokens[0]) && directions.contains(&command_tokens[1]) {
match &command_tokens[1][..] {
"north" | "n" => Some(Directions::North),
"south" | "s" => Some(Directions::South),
"east" | "e" => Some(Directions::East),
"west" | "w" => Some(Directions::West),
_ => None,
}
} else {
None
}
}
_ => None,
}
}
#[cfg(test)]
#[path = "action_tests.rs"]
mod action_tests;