diff --git a/validation/src/check.rs b/validation/src/check.rs index 98952b0..afcc810 100644 --- a/validation/src/check.rs +++ b/validation/src/check.rs @@ -324,25 +324,24 @@ pub fn get_model_info<'a>(dom:&'a rbx_dom_weak::WeakDom,model_instance:&'a rbx_d } // check if an observed string matches an expected string -pub struct StringCheck<'a,T,Str>(Result>); -pub struct StringCheckContext<'a,Str>{ +pub struct StringEquality<'a,Str>{ observed:&'a str, expected:Str, } -impl<'a,Str> StringCheckContext<'a,Str> +impl<'a,Str> StringEquality<'a,Str> where &'a str:PartialEq, { /// Compute the StringCheck, passing through the provided value on success. - fn check(self,value:T)->StringCheck<'a,T,Str>{ + fn check(self,value:T)->Result{ if self.observed==self.expected{ - StringCheck(Ok(value)) + Ok(value) }else{ - StringCheck(Err(self)) + Err(self) } } } -impl std::fmt::Display for StringCheckContext<'_,Str>{ +impl std::fmt::Display for StringEquality<'_,Str>{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ write!(f,"expected: {}, observed: {}",self.expected,self.observed) } @@ -464,19 +463,66 @@ impl TryFrom> for MapInfoOwned{ struct Exists; struct Absent; +enum DisplayNameError<'a>{ + TitleCase(StringEquality<'a,String>), + Empty(StringEmpty), + TooLong(usize), + StringValue(StringValueError), +} +fn check_display_name<'a>(display_name:Result<&'a str,StringValueError>)->Result<&'a str,DisplayNameError<'a>>{ + // DisplayName StringValue can be missing or whatever + let display_name=display_name.map_err(DisplayNameError::StringValue)?; + + // DisplayName cannot be "" + let display_name=check_empty(display_name).map_err(DisplayNameError::Empty)?; + + // DisplayName cannot exceed 50 characters + if 50(creator:Result<&'a str,StringValueError>)->Result<&'a str,CreatorError>{ + // Creator StringValue can be missing or whatever + let creator=creator.map_err(CreatorError::StringValue)?; + + // Creator cannot be "" + let creator=check_empty(creator).map_err(CreatorError::Empty)?; + + // Creator cannot exceed 50 characters + if 50{ // === METADATA CHECKS === // The root must be of class Model - model_class:StringCheck<'a,(),&'static str>, + model_class:Result<(),StringEquality<'a,&'static str>>, // Model's name must be in snake case - model_name:StringCheck<'a,(),String>, + model_name:Result<(),StringEquality<'a,String>>, // Map must have a StringValue named DisplayName. // Value must not be empty, must be in title case. - display_name:Result,StringEmpty>,StringValueError>, + display_name:Result<&'a str,DisplayNameError<'a>>, // Map must have a StringValue named Creator. // Value must not be empty. - creator:Result,StringValueError>, + creator:Result<&'a str,CreatorError>, // The prefix of the model's name must match the game it was submitted for. // bhop_ for bhop, and surf_ for surf game_id:Result, @@ -511,27 +557,22 @@ struct MapCheck<'a>{ impl<'a> ModelInfo<'a>{ fn check(self)->MapCheck<'a>{ // Check class is exactly "Model" - let model_class=StringCheckContext{ + let model_class=StringEquality{ observed:self.model_class, expected:"Model", }.check(()); // Check model name is snake case - let model_name=StringCheckContext{ + let model_name=StringEquality{ observed:self.model_name, expected:self.model_name.to_snake_case(), }.check(()); // Check display name is not empty and has title case - let display_name=self.map_info.display_name.map(|display_name|{ - check_empty(display_name).map(|display_name|StringCheckContext{ - observed:display_name, - expected:display_name.to_title_case(), - }.check(display_name)) - }); + let display_name=check_display_name(self.map_info.display_name); // Check Creator is not empty - let creator=self.map_info.creator.map(check_empty); + let creator=check_creator(self.map_info.creator); // Check GameID (model name was prefixed with bhop_ surf_ etc) let game_id=self.map_info.game_id; @@ -630,10 +671,10 @@ impl MapCheck<'_>{ fn result(self)->Result>{ match self{ MapCheck{ - model_class:StringCheck(Ok(())), - model_name:StringCheck(Ok(())), - display_name:Ok(Ok(StringCheck(Ok(display_name)))), - creator:Ok(Ok(creator)), + model_class:Ok(()), + model_name:Ok(()), + display_name:Ok(display_name), + creator:Ok(creator), game_id:Ok(game_id), mapstart:Ok(Exists), mode_start_counts:DuplicateCheck(Ok(())), @@ -737,27 +778,25 @@ macro_rules! summary_format{ impl MapCheck<'_>{ fn itemize(&self)->Result{ let model_class=match &self.model_class{ - StringCheck(Ok(()))=>passed!("ModelClass"), - StringCheck(Err(context))=>summary_format!("ModelClass","Invalid model class: {context}"), + Ok(())=>passed!("ModelClass"), + Err(context)=>summary_format!("ModelClass","Invalid model class: {context}"), }; let model_name=match &self.model_name{ - StringCheck(Ok(()))=>passed!("ModelName"), - StringCheck(Err(context))=>summary_format!("ModelName","Model name must have snake_case: {context}"), + Ok(())=>passed!("ModelName"), + Err(context)=>summary_format!("ModelName","Model name must have snake_case: {context}"), }; let display_name=match &self.display_name{ - Ok(Ok(StringCheck(Ok(_))))=>passed!("DisplayName"), - Ok(Ok(StringCheck(Err(context))))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"), - Ok(Err(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"), - Err(StringValueError::ObjectNotFound)=>summary!("DisplayName","Missing DisplayName StringValue".to_owned()), - Err(StringValueError::ValueNotSet)=>summary!("DisplayName","DisplayName Value not set".to_owned()), - Err(StringValueError::NonStringValue)=>summary!("DisplayName","DisplayName Value is not a String".to_owned()), + Ok(_)=>passed!("DisplayName"), + Err(DisplayNameError::TitleCase(context))=>summary_format!("DisplayName","DisplayName must have Title Case: {context}"), + Err(DisplayNameError::Empty(context))=>summary_format!("DisplayName","Invalid DisplayName: {context}"), + Err(DisplayNameError::TooLong(context))=>summary_format!("DisplayName","DisplayName is too long: {context} characters (50 characters max)"), + Err(DisplayNameError::StringValue(context))=>summary_format!("DisplayName","DisplayName StringValue: {context}"), }; let creator=match &self.creator{ - Ok(Ok(_))=>passed!("Creator"), - Ok(Err(context))=>summary_format!("Creator","Invalid Creator: {context}"), - Err(StringValueError::ObjectNotFound)=>summary!("Creator","Missing Creator StringValue".to_owned()), - Err(StringValueError::ValueNotSet)=>summary!("Creator","Creator Value not set".to_owned()), - Err(StringValueError::NonStringValue)=>summary!("Creator","Creator Value is not a String".to_owned()), + Ok(_)=>passed!("Creator"), + Err(CreatorError::Empty(context))=>summary_format!("Creator","Invalid Creator: {context}"), + Err(CreatorError::TooLong(context))=>summary_format!("Creator","Creator is too long: {context} characters (50 characters max)"), + Err(CreatorError::StringValue(context))=>summary_format!("Creator","Creator StringValue: {context}"), }; let game_id=match &self.game_id{ Ok(_)=>passed!("GameID"), diff --git a/validation/src/rbx_util.rs b/validation/src/rbx_util.rs index dbfc086..39287f3 100644 --- a/validation/src/rbx_util.rs +++ b/validation/src/rbx_util.rs @@ -79,6 +79,15 @@ pub enum StringValueError{ ValueNotSet, NonStringValue, } +impl std::fmt::Display for StringValueError{ + fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ + match self{ + StringValueError::ObjectNotFound=>write!(f,"Missing StringValue"), + StringValueError::ValueNotSet=>write!(f,"Value not set"), + StringValueError::NonStringValue=>write!(f,"Value is not a String"), + } + } +} fn string_value(instance:Option<&rbx_dom_weak::Instance>)->Result<&str,StringValueError>{ let instance=instance.ok_or(StringValueError::ObjectNotFound)?;