|
|
|
|
@@ -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<T,StringCheckContext<'a,Str>>);
|
|
|
|
|
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<Str>,
|
|
|
|
|
{
|
|
|
|
|
/// Compute the StringCheck, passing through the provided value on success.
|
|
|
|
|
fn check<T>(self,value:T)->StringCheck<'a,T,Str>{
|
|
|
|
|
fn check<T>(self,value:T)->Result<T,Self>{
|
|
|
|
|
if self.observed==self.expected{
|
|
|
|
|
StringCheck(Ok(value))
|
|
|
|
|
Ok(value)
|
|
|
|
|
}else{
|
|
|
|
|
StringCheck(Err(self))
|
|
|
|
|
Err(self)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl<Str:std::fmt::Display> std::fmt::Display for StringCheckContext<'_,Str>{
|
|
|
|
|
impl<Str:std::fmt::Display> 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<MapInfo<'_>> 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<display_name.len(){
|
|
|
|
|
return Err(DisplayNameError::TooLong(display_name.len()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check title case
|
|
|
|
|
let display_name=StringEquality{
|
|
|
|
|
observed:display_name,
|
|
|
|
|
expected:display_name.to_title_case(),
|
|
|
|
|
}.check(display_name).map_err(DisplayNameError::TitleCase)?;
|
|
|
|
|
|
|
|
|
|
Ok(display_name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enum CreatorError{
|
|
|
|
|
Empty(StringEmpty),
|
|
|
|
|
TooLong(usize),
|
|
|
|
|
StringValue(StringValueError),
|
|
|
|
|
}
|
|
|
|
|
fn check_creator<'a>(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<creator.len(){
|
|
|
|
|
return Err(CreatorError::TooLong(creator.len()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(creator)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The result of every map check.
|
|
|
|
|
struct MapCheck<'a>{
|
|
|
|
|
// === 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<Result<StringCheck<'a,&'a str,String>,StringEmpty>,StringValueError>,
|
|
|
|
|
display_name:Result<&'a str,DisplayNameError<'a>>,
|
|
|
|
|
// Map must have a StringValue named Creator.
|
|
|
|
|
// Value must not be empty.
|
|
|
|
|
creator:Result<Result<&'a str,StringEmpty>,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<GameID,ParseGameIDError>,
|
|
|
|
|
@@ -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<MapInfoOwned,Result<MapCheckList,serde_json::Error>>{
|
|
|
|
|
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<MapCheckList,serde_json::Error>{
|
|
|
|
|
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"),
|
|
|
|
|
|