use crate::{ db::Database, models::commands::{ CommandEvent, AiCommand, EventCoherenceFailure, EventConversionError, EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution, }, }; use anyhow::Result; use futures::stream::{self, StreamExt, TryStreamExt}; use itertools::{Either, Itertools}; use std::convert::TryFrom; use strum::VariantNames; type EventConversionResult = std::result::Result; impl CommandEvent { pub fn new(raw_event: RawCommandEvent) -> EventConversionResult { let event_name = raw_event.event_name.as_str().to_lowercase(); if Self::VARIANTS.contains(&event_name.as_str()) { deserialize_recognized_event(raw_event) } else { Err(EventConversionError::UnrecognizedEvent(raw_event)) } } } impl TryFrom for CommandEvent { type Error = EventConversionError; fn try_from(raw_event: RawCommandEvent) -> Result { CommandEvent::new(raw_event) } } /// Internal struct to hold the narrative parts of the /// RawCommandExecution to minimize clones. struct Narrative { valid: bool, reason: Option, narration: String, } fn from_raw_success(raw: Narrative, events: Vec) -> AiCommand { AiCommand { events, valid: raw.valid, reason: match &raw.reason { Some(reason) if !raw.valid && reason.is_empty() => { Some("invalid for unknown reason".to_string()) } Some(_) if !raw.valid => raw.reason.clone(), _ => None, }, narration: raw.narration.clone(), } } pub async fn convert_raw_execution( mut raw_exec: RawCommandExecution, db: &Database, ) -> ExecutionConversionResult { if !raw_exec.valid { return ExecutionConversionResult::Success(AiCommand::from_raw_invalid(raw_exec)); } let narrative = Narrative { valid: raw_exec.valid, reason: raw_exec.reason.take(), narration: std::mem::take(&mut raw_exec.narration), }; let conversions: Vec<_> = raw_exec .events .into_iter() .map(|raw_event| CommandEvent::new(raw_event)) .collect(); let (converted, conversion_failures): (Vec<_>, Vec<_>) = conversions.into_iter().partition_map(|res| match res { Ok(converted_event) => Either::Left(converted_event), Err(err) => Either::Right(err), }); // Coherence validation of converted events. let (successes, incoherent_events): (Vec<_>, Vec<_>) = stream::iter(converted.into_iter()) .then(|event| validate_event_coherence(db, event)) .collect::>() .await .into_iter() .partition_map(|res| match res { Ok(event) => Either::Left(event), Err(err) => Either::Right(err), }); let failure_len = conversion_failures.len() + incoherent_events.len(); if successes.len() > 0 && failure_len == 0 { ExecutionConversionResult::Success(from_raw_success(narrative, successes)) } else if successes.len() > 0 && failure_len > 0 { let converted_execution = from_raw_success(narrative, successes); let failures = EventConversionFailures::from_failures(conversion_failures, incoherent_events); ExecutionConversionResult::PartialSuccess(converted_execution, failures) } else { ExecutionConversionResult::Failure(EventConversionFailures::from_failures( conversion_failures, incoherent_events, )) } } fn deserialize_recognized_event( raw_event: RawCommandEvent, ) -> Result { let event_name = raw_event.event_name.as_str().to_lowercase(); let event_name = event_name.as_str(); match event_name { // scene-related "change_scene" => Ok(CommandEvent::ChangeScene { scene_key: raw_event .parameter .strip_prefix("scenes/") // Mini coherence check .map(String::from) .unwrap_or(raw_event.parameter) .clone(), }), // bodily position-related "stand" => Ok(CommandEvent::Stand { target: raw_event.applies_to, }), "sit" => Ok(CommandEvent::Sit { target: raw_event.applies_to, }), "prone" => Ok(CommandEvent::Prone { target: raw_event.applies_to, }), "crouch" => Ok(CommandEvent::Crouch { target: raw_event.applies_to, }), // combat-related "take_damage" => deserialize_take_damage(raw_event), // miscellaneous "narration" => Ok(CommandEvent::Narration(raw_event.parameter)), // unrecognized _ => Err(EventConversionError::UnrecognizedEvent(raw_event)), } } fn deserialize_take_damage( raw_event: RawCommandEvent, ) -> Result { match raw_event.parameter.parse::() { Ok(dmg) => Ok(CommandEvent::TakeDamage { target: raw_event.applies_to, amount: dmg, }), Err(_) => Err(EventConversionError::InvalidParameter(raw_event)), } } async fn validate_event_coherence<'a>( db: &Database, event: CommandEvent, ) -> std::result::Result { match event { CommandEvent::ChangeScene { ref scene_key } => match db.stage_exists(&scene_key).await { Ok(exists) => match exists { true => Ok(event), false => Err(invalid_converted_event(event).unwrap()), }, Err(err) => Err(invalid_converted_event_because_err(event, err)), }, _ => Ok(event), } } /// The event was converted from the raw response properly, but the /// information contained in the response is not valid. fn invalid_converted_event(event: CommandEvent) -> Option { match event { CommandEvent::ChangeScene { .. } => Some(EventCoherenceFailure::TargetDoesNotExist(event)), _ => None, } } /// The event was converted from the raw response properly, but /// something went wrong with attempting to check the coherence of the /// converted event. fn invalid_converted_event_because_err( event: CommandEvent, err: anyhow::Error, ) -> EventCoherenceFailure { EventCoherenceFailure::OtherError(event, format!("{}", err)) }