This commit is contained in:
Quaternions 2025-04-11 16:29:42 -07:00
parent 81e7c7afdf
commit 582693fc8f
Signed by: Quaternions
GPG Key ID: D0DF5964F79AC131

@ -12,6 +12,7 @@ pub enum Error{
CreatorTypeMustBeUser, CreatorTypeMustBeUser,
Download(crate::download::Error), Download(crate::download::Error),
ModelFileDecode(ReadDomError), ModelFileDecode(ReadDomError),
GetRootInstance(GetRootInstanceError),
} }
impl std::fmt::Display for Error{ impl std::fmt::Display for Error{
fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{
@ -155,38 +156,29 @@ pub fn get_model_info(dom:&rbx_dom_weak::WeakDom)->Result<ModelInfo,GetRootInsta
}) })
} }
pub enum Check<Context>{
Pass,
Fail(Context),
}
// check if an observed string matches and expected string // check if an observed string matches and expected string
pub struct StringCheck<'a>(Check<StringCheckContext<'a>>); pub struct StringCheck<'a,T>(Result<T,StringCheckContext<'a>>);
pub struct StringCheckContext<'a>{ pub struct StringCheckContext<'a>{
observed:&'a str, observed:&'a str,
expected:Cow<'a,str>, expected:Cow<'a,str>,
} }
impl<'a> StringCheckContext<'a>{ impl<'a> StringCheckContext<'a>{
fn check(self)->StringCheck<'a>{ fn check<T>(self,value:T)->StringCheck<'a,T>{
if self.observed==self.expected{ if self.observed==self.expected{
StringCheck(Check::Pass) StringCheck(Ok(value))
}else{ }else{
StringCheck(Check::Fail(self)) StringCheck(Err(self))
} }
} }
} }
// check if a string is empty // check if a string is empty
pub enum StringEmptyCheck<Context>{ pub struct StringEmpty;
Empty, pub struct StringEmptyCheck<Context>(Result<Context,StringEmpty>);
Passed(Context),
}
// check for duplicate objects // check for duplicate objects
pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>); pub struct DuplicateCheckContext<ID>(HashMap<ID,u64>);
pub struct DuplicateCheck<ID>(Check<DuplicateCheckContext<ID>>); pub struct DuplicateCheck<ID>(Result<(),DuplicateCheckContext<ID>>);
impl<ID> DuplicateCheckContext<ID>{ impl<ID> DuplicateCheckContext<ID>{
fn check(self)->DuplicateCheck<ID>{ fn check(self)->DuplicateCheck<ID>{
let Self(mut set)=self; let Self(mut set)=self;
@ -194,9 +186,9 @@ impl<ID> DuplicateCheckContext<ID>{
set.retain(|_,&mut c|c!=1); set.retain(|_,&mut c|c!=1);
// if any entries remain, they are incorrect // if any entries remain, they are incorrect
if set.is_empty(){ if set.is_empty(){
DuplicateCheck(Check::Pass) DuplicateCheck(Ok(()))
}else{ }else{
DuplicateCheck(Check::Fail(Self(set))) DuplicateCheck(Err(Self(set)))
} }
} }
} }
@ -206,7 +198,7 @@ pub struct AtLeastOneMatchingAndNoExtraCheckContext<ID>{
set:HashMap<ID,u64>, set:HashMap<ID,u64>,
missing:HashSet<ID>, missing:HashSet<ID>,
} }
pub struct AtLeastOneMatchingAndNoExtraCheck<ID>(Check<AtLeastOneMatchingAndNoExtraCheckContext<ID>>); pub struct AtLeastOneMatchingAndNoExtraCheck<ID>(Result<(),AtLeastOneMatchingAndNoExtraCheckContext<ID>>);
impl<ID> AtLeastOneMatchingAndNoExtraCheckContext<ID>{ impl<ID> AtLeastOneMatchingAndNoExtraCheckContext<ID>{
fn new(set:HashMap<ID,u64>)->Self{ fn new(set:HashMap<ID,u64>)->Self{
Self{ Self{
@ -227,9 +219,9 @@ impl<ID:Copy+Eq+std::hash::Hash> AtLeastOneMatchingAndNoExtraCheckContext<ID>{
} }
// if any entries remain, they are incorrect // if any entries remain, they are incorrect
if set.is_empty()&&missing.is_empty(){ if set.is_empty()&&missing.is_empty(){
AtLeastOneMatchingAndNoExtraCheck(Check::Pass) AtLeastOneMatchingAndNoExtraCheck(Ok(()))
}else{ }else{
AtLeastOneMatchingAndNoExtraCheck(Check::Fail(Self{set,missing})) AtLeastOneMatchingAndNoExtraCheck(Err(Self{set,missing}))
} }
} }
} }
@ -242,15 +234,15 @@ pub struct MapInfoOwned{
// crazy! // crazy!
pub struct MapCheck<'a>{ pub struct MapCheck<'a>{
model_class:StringCheck<'a>, model_class:StringCheck<'a,()>,
model_name:StringCheck<'a>, model_name:StringCheck<'a,()>,
display_name:Result<StringEmptyCheck<StringCheck<'a>>,StringValueError>, display_name:Result<StringEmptyCheck<StringCheck<'a,&'a str>>,StringValueError>,
creator:Result<StringEmptyCheck<()>,StringValueError>, creator:Result<StringEmptyCheck<&'a str>,StringValueError>,
game_id:Result<GameID,ParseGameIDError>, game_id:Result<GameID,ParseGameIDError>,
mapstart:Check<()>, mapstart:Result<(),()>,
mode_start_counts:DuplicateCheck<ModeID>, mode_start_counts:DuplicateCheck<ModeID>,
mode_finish_counts:AtLeastOneMatchingAndNoExtraCheck<ModeID>, mode_finish_counts:AtLeastOneMatchingAndNoExtraCheck<ModeID>,
spawn1:Check<()>, spawn1:Result<(),()>,
spawn_counts:DuplicateCheck<SpawnID>, spawn_counts:DuplicateCheck<SpawnID>,
wormhole_out_counts:DuplicateCheck<WormholeOutID>, wormhole_out_counts:DuplicateCheck<WormholeOutID>,
} }
@ -260,32 +252,32 @@ impl<'a> ModelInfo<'a>{
let model_class=StringCheckContext{ let model_class=StringCheckContext{
observed:self.model_class, observed:self.model_class,
expected:Cow::Borrowed("Model"), expected:Cow::Borrowed("Model"),
}.check(); }.check(());
let model_name=StringCheckContext{ let model_name=StringCheckContext{
observed:self.model_name, observed:self.model_name,
expected:Cow::Owned(self.model_name.to_snake_case()), expected:Cow::Owned(self.model_name.to_snake_case()),
}.check(); }.check(());
// check display name // check display name
let display_name=self.map_info.display_name.map(|display_name|{ let display_name=self.map_info.display_name.map(|display_name|{
if display_name.is_empty(){ if display_name.is_empty(){
StringEmptyCheck::Empty StringEmptyCheck(Err(StringEmpty))
}else{ }else{
let display_name=StringCheckContext{ let display_name=StringCheckContext{
observed:display_name, observed:display_name,
expected:Cow::Owned(display_name.to_title_case()), expected:Cow::Owned(display_name.to_title_case()),
}.check(); }.check(display_name);
StringEmptyCheck::Passed(display_name) StringEmptyCheck(Ok(display_name))
} }
}); });
// check Creator // check Creator
let creator=self.map_info.creator.map(|creator|{ let creator=self.map_info.creator.map(|creator|{
if creator.is_empty(){ if creator.is_empty(){
StringEmptyCheck::Empty StringEmptyCheck(Err(StringEmpty))
}else{ }else{
StringEmptyCheck::Passed(()) StringEmptyCheck(Ok(creator))
} }
}); });
@ -294,16 +286,16 @@ impl<'a> ModelInfo<'a>{
// MapStart must exist // MapStart must exist
let mapstart=if self.counts.mode_start_counts.get(&ModeID::MAIN).is_some(){ let mapstart=if self.counts.mode_start_counts.get(&ModeID::MAIN).is_some(){
Check::Pass Ok(())
}else{ }else{
Check::Fail(()) Err(())
}; };
// Spawn1 must exist // Spawn1 must exist
let spawn1=if self.counts.spawn_counts.get(&SpawnID::FIRST).is_some(){ let spawn1=if self.counts.spawn_counts.get(&SpawnID::FIRST).is_some(){
Check::Pass Ok(())
}else{ }else{
Check::Fail(()) Err(())
}; };
// check that at least one end zone exists for each start zone. // check that at least one end zone exists for each start zone.
@ -338,17 +330,17 @@ impl<'a> MapCheck<'a>{
fn pass(self)->Result<MapInfoOwned,Self>{ fn pass(self)->Result<MapInfoOwned,Self>{
match self{ match self{
MapCheck{ MapCheck{
model_class:StringCheck(Check::Pass), model_class:StringCheck(Ok(())),
model_name:StringCheck(Check::Pass), model_name:StringCheck(Ok(())),
display_name:Ok(StringEmptyCheck::Passed(StringCheck(Check::Pass))), display_name:Ok(StringEmptyCheck(Ok(StringCheck(Ok(display_name))))),
creator:Ok(StringEmptyCheck::Passed(())), creator:Ok(StringEmptyCheck(Ok(creator))),
game_id:Ok(game_id), game_id:Ok(game_id),
mapstart, mapstart:Ok(()),
mode_start_counts, mode_start_counts:DuplicateCheck(Ok(())),
mode_finish_counts, mode_finish_counts:AtLeastOneMatchingAndNoExtraCheck(Ok(())),
spawn1, spawn1:Ok(()),
spawn_counts, spawn_counts:DuplicateCheck(Ok(())),
wormhole_out_counts, wormhole_out_counts:DuplicateCheck(Ok(())),
}=>{ }=>{
Ok(MapInfoOwned{ Ok(MapInfoOwned{
display_name:display_name.to_owned(), display_name:display_name.to_owned(),
@ -362,7 +354,7 @@ impl<'a> MapCheck<'a>{
} }
pub struct CheckReportAndVersion{ pub struct CheckReportAndVersion{
pub status:CheckStatus, pub status:Result<MapInfoOwned,String>,
pub version:u64, pub version:u64,
} }
@ -389,7 +381,14 @@ impl crate::message_handler::MessageHandler{
// decode dom (slow!) // decode dom (slow!)
let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?; let dom=maybe_gzip.read_with(read_dom,read_dom).map_err(Error::ModelFileDecode)?;
let status=check(&dom); // extract information from the model
let model_info=get_model_info(&dom).map_err(Error::GetRootInstance)?;
// convert the model information into a structured report
let map_check=model_info.check();
// check the report, generate an error message if it fails the check
let status=map_check.pass().map_err(|e|e.to_string());
Ok(CheckReportAndVersion{status,version}) Ok(CheckReportAndVersion{status,version})
} }