use std::path::PathBuf; use futures::{StreamExt, TryStreamExt}; use tokio::io::AsyncReadExt; use crate::common::{sanitize,Style,PropertiesOverride}; //holy smokes what am I doing lmao //This giant machine is supposed to search for files according to style rules //e.g. ScriptName.server.lua or init.lua //Obviously I got carried away //I could use an enum! //I could use a struct! //I could use a trait! //I could use an error! //I could use a match! //I could use a function! //eventually: #[derive(Debug)] #[allow(dead_code)]//idk why this thinks it's dead code, the errors are printed out in various places pub enum QueryResolveError{ NotFound,//0 results Ambiguous,//>1 results JoinError(tokio::task::JoinError), IO(std::io::Error), } impl std::fmt::Display for QueryResolveError{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ write!(f,"{self:?}") } } impl std::error::Error for QueryResolveError{} struct FileWithName{ file:tokio::fs::File, name:String, } async fn get_file_async(mut path:PathBuf,file_name:impl AsRef<std::path::Path>)->Result<FileWithName,QueryResolveError>{ let name=file_name.as_ref().to_str().unwrap().to_owned(); path.push(file_name); match tokio::fs::File::open(path).await{ Ok(file)=>Ok(FileWithName{file,name}), Err(e)=>match e.kind(){ std::io::ErrorKind::NotFound=>Err(QueryResolveError::NotFound), _=>Err(QueryResolveError::IO(e)), }, } } type QueryHintResult=Result<FileHint,QueryResolveError>; trait Query{ async fn resolve(self)->QueryHintResult; } type QueryHandle=tokio::task::JoinHandle<Result<FileWithName,QueryResolveError>>; struct QuerySingle{ script:QueryHandle, } impl QuerySingle{ fn rox(search_path:&PathBuf,search_name:&str)->Self{ Self{ script:tokio::spawn(get_file_async(search_path.clone(),format!("{}.lua",search_name))) } } } impl Query for QuerySingle{ async fn resolve(self)->QueryHintResult{ match self.script.await{ Ok(Ok(file))=>Ok(FileHint{file,hint:ScriptHint::ModuleScript}), Ok(Err(e))=>Err(e), Err(e)=>Err(QueryResolveError::JoinError(e)), } } } struct QueryTriple{ module:QueryHandle, server:QueryHandle, client:QueryHandle, } impl QueryTriple{ fn rox_rojo(search_path:&PathBuf,search_name:&str,search_module:bool)->Self{ //this should be implemented as constructors of Triplet and Quadruplet to fully support Trey's suggestion let module_name=if search_module{ format!("{}.module.lua",search_name) }else{ format!("{}.lua",search_name) }; Self{ module:tokio::spawn(get_file_async(search_path.clone(),module_name)), server:tokio::spawn(get_file_async(search_path.clone(),format!("{}.server.lua",search_name))), client:tokio::spawn(get_file_async(search_path.clone(),format!("{}.client.lua",search_name))), } } fn rojo(search_path:&PathBuf)->Self{ QueryTriple::rox_rojo(search_path,"init",false) } } //these functions can be achieved with macros, but I have not learned that yet fn mega_triple_join(query_triplet:(QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{ match query_triplet{ //unambiguously locate file (Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) |(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound)) |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f), //multiple files located (Ok(_),Ok(_),Err(QueryResolveError::NotFound)) |(Ok(_),Err(QueryResolveError::NotFound),Ok(_)) |(Err(QueryResolveError::NotFound),Ok(_),Ok(_)) |(Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous), //no files located (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound), //other error (Err(e),_,_) |(_,Err(e),_) |(_,_,Err(e))=>Err(e), } } //LETS GOOOOOOOOOOOOOOOO fn mega_quadruple_join(query_quad:(QueryHintResult,QueryHintResult,QueryHintResult,QueryHintResult))->QueryHintResult{ match query_quad{ //unambiguously locate file (Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) |(Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f),Err(QueryResolveError::NotFound)) |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(f))=>Ok(f), //multiple files located (Ok(_),Ok(_),Ok(_),Err(QueryResolveError::NotFound)) |(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Ok(_)) |(Ok(_),Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound)) |(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Ok(_)) |(Ok(_),Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound)) |(Ok(_),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_)) |(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Ok(_)) |(Err(QueryResolveError::NotFound),Ok(_),Ok(_),Err(QueryResolveError::NotFound)) |(Err(QueryResolveError::NotFound),Ok(_),Err(QueryResolveError::NotFound),Ok(_)) |(Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Ok(_),Ok(_)) |(Ok(_),Ok(_),Ok(_),Ok(_))=>Err(QueryResolveError::Ambiguous), //no files located (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Err(QueryResolveError::NotFound), //other error (Err(e),_,_,_) |(_,Err(e),_,_) |(_,_,Err(e),_) |(_,_,_,Err(e))=>Err(e), } } impl Query for QueryTriple{ async fn resolve(self)->QueryHintResult{ let (module,server,client)=tokio::join!(self.module,self.server,self.client); mega_triple_join(( module.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}), client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), )) } } struct QueryQuad{ module_implicit:QueryHandle, module_explicit:QueryHandle, server:QueryHandle, client:QueryHandle, } impl QueryQuad{ fn rox_rojo(search_path:&PathBuf,search_name:&str)->Self{ let fill=QueryTriple::rox_rojo(search_path,search_name,true); Self{ module_implicit:QuerySingle::rox(search_path,search_name).script,//Script.lua module_explicit:fill.module,//Script.module.lua server:fill.server, client:fill.client, } } } impl Query for QueryQuad{ async fn resolve(self)->QueryHintResult{ let (module_implicit,module_explicit,server,client)=tokio::join!(self.module_implicit,self.module_explicit,self.server,self.client); mega_quadruple_join(( module_implicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), module_explicit.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::ModuleScript}), server.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::Script}), client.map_err(|e|QueryResolveError::JoinError(e))?.map(|file|FileHint{file,hint:ScriptHint::LocalScript}), )) } } struct ScriptWithOverrides{ overrides:PropertiesOverride, source:String, } #[derive(Debug)] pub enum ScriptWithOverridesError{ UnimplementedProperty(String), } impl std::fmt::Display for ScriptWithOverridesError{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ write!(f,"{self:?}") } } impl std::error::Error for ScriptWithOverridesError{} impl ScriptWithOverrides{ fn from_source(mut source:String)->Result<Self,ScriptWithOverridesError>{ let mut overrides=PropertiesOverride::default(); let mut count=0; for line in source.lines(){ //only string type properties are supported atm if let Some(captures)=lazy_regex::regex!(r#"^\-\-\s*Properties\.([A-Za-z]\w*)\s*\=\s*"(\w+)"$"#) .captures(line){ count+=line.len(); match &captures[1]{ "Name"=>overrides.name=Some(captures[2].to_owned()), "ClassName"=>overrides.class=Some(captures[2].to_owned()), other=>Err(ScriptWithOverridesError::UnimplementedProperty(other.to_owned()))?, } }else{ break; } } Ok(ScriptWithOverrides{overrides,source:source.split_off(count)}) } } enum CompileClass{ Folder, Script(String), LocalScript(String), ModuleScript(String), Model(Vec<u8>), } struct CompileNode{ name:String, blacklist:Option<String>, class:CompileClass, } #[derive(Debug)] pub enum CompileNodeError{ IO(std::io::Error), ScriptWithOverrides(ScriptWithOverridesError), InvalidClassOrHint{ class:Option<String>, hint:ScriptHint }, QueryResolveError(QueryResolveError), /// Conversion from OsString to String failed FileName(std::ffi::OsString), ExtensionNotSupportedInStyle{ extension:String, style:Option<Style>, }, UnknownExtension, } impl std::fmt::Display for CompileNodeError{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ write!(f,"{self:?}") } } impl std::error::Error for CompileNodeError{} enum FileDiscernment{ Model, Script(ScriptHint), } impl CompileNode{ async fn script(search_name:&str,mut file:FileWithName,hint:ScriptHint)->Result<Self,CompileNodeError>{ //read entire file let mut buf=String::new(); file.file.read_to_string(&mut buf).await.map_err(CompileNodeError::IO)?; //regex script according to Properties lines at the top let script_with_overrides=ScriptWithOverrides::from_source(buf).map_err(CompileNodeError::ScriptWithOverrides)?; //script Ok(Self{ blacklist:Some(file.name), name:script_with_overrides.overrides.name.unwrap_or_else(||search_name.to_owned()), class:match (script_with_overrides.overrides.class.as_deref(),hint){ (Some("ModuleScript"),_) |(None,ScriptHint::ModuleScript)=>CompileClass::ModuleScript(script_with_overrides.source), (Some("LocalScript"),_) |(None,ScriptHint::LocalScript)=>CompileClass::LocalScript(script_with_overrides.source), (Some("Script"),_) |(None,ScriptHint::Script)=>CompileClass::Script(script_with_overrides.source), (class,hint)=>Err(CompileNodeError::InvalidClassOrHint{class:class.map(|s|s.to_owned()),hint})?, }, }) } async fn model(search_name:&str,mut file:FileWithName)->Result<Self,CompileNodeError>{ //read entire file let mut buf=Vec::new(); file.file.read_to_end(&mut buf).await.map_err(CompileNodeError::IO)?; //model Ok(Self{ blacklist:Some(file.name), name:search_name.to_owned(),//wrong but gets overwritten by internal model name class:CompileClass::Model(buf), }) } async fn from_folder(entry:&tokio::fs::DirEntry,style:Option<Style>)->Result<Self,CompileNodeError>{ let contents_folder=entry.path(); let file_name=entry.file_name(); let search_name=file_name.to_str().unwrap(); //scan inside the folder for an object to define the class of the folder let script_query=async {match style{ Some(Style::Rox)=>QuerySingle::rox(&contents_folder,search_name).resolve().await, Some(Style::RoxRojo)=>QueryQuad::rox_rojo(&contents_folder,search_name).resolve().await, Some(Style::Rojo)=>QueryTriple::rojo(&contents_folder).resolve().await, //try all three and complain if there is ambiguity None=>mega_triple_join(tokio::join!( QuerySingle::rox(&contents_folder,search_name).resolve(), //true=search for module here to avoid ambiguity with QuerySingle::rox results QueryTriple::rox_rojo(&contents_folder,search_name,true).resolve(), QueryTriple::rojo(&contents_folder).resolve(), )) }}; //model files are rox & rox-rojo only, so it's a lot less work... let model_query=get_file_async(contents_folder.clone(),format!("{}.rbxmx",search_name)); //model? script? both? Ok(match tokio::join!(script_query,model_query){ (Ok(FileHint{file,hint}),Err(QueryResolveError::NotFound))=>Self::script(search_name,file,hint).await?, (Err(QueryResolveError::NotFound),Ok(file))=>Self::model(search_name,file).await?, (Ok(_),Ok(_))=>Err(CompileNodeError::QueryResolveError(QueryResolveError::Ambiguous))?, //neither (Err(QueryResolveError::NotFound),Err(QueryResolveError::NotFound))=>Self{ name:search_name.to_owned(), blacklist:None, class:CompileClass::Folder, }, //other error (Err(e),_) |(_,Err(e))=>Err(CompileNodeError::QueryResolveError(e))? }) } async fn from_file(entry:&tokio::fs::DirEntry,style:Option<Style>)->Result<Self,CompileNodeError>{ let mut file_name=entry .file_name() .into_string() .map_err(CompileNodeError::FileName)?; //reject goobers let is_goober=match style{ Some(Style::Rojo)=>true, _=>false, }; let (ext_len,file_discernment)={ if let Some(captures)=lazy_regex::regex!(r"^.*(\.module\.lua|\.client\.lua|\.server\.lua)$") .captures(file_name.as_str()){ let ext=&captures[1]; (ext.len(),match ext{ ".module.lua"=>{ if is_goober{ Err(CompileNodeError::ExtensionNotSupportedInStyle{extension:ext.to_owned(),style})?; } FileDiscernment::Script(ScriptHint::ModuleScript) }, ".client.lua"=>FileDiscernment::Script(ScriptHint::LocalScript), ".server.lua"=>FileDiscernment::Script(ScriptHint::Script), _=>panic!("Regex failed"), }) }else if let Some(captures)=lazy_regex::regex!(r"^.*(\.rbxmx|\.lua)$") .captures(file_name.as_str()){ let ext=&captures[1]; (ext.len(),match ext{ ".rbxmx"=>{ if is_goober{ Err(CompileNodeError::ExtensionNotSupportedInStyle{extension:ext.to_owned(),style})?; } FileDiscernment::Model }, ".lua"=>FileDiscernment::Script(ScriptHint::ModuleScript), _=>panic!("Regex failed"), }) }else{ return Err(CompileNodeError::UnknownExtension); } }; file_name.truncate(file_name.len()-ext_len); let file=tokio::fs::File::open(entry.path()).await.map_err(CompileNodeError::IO)?; Ok(match file_discernment{ FileDiscernment::Model=>Self::model(file_name.as_str(),FileWithName{file,name:file_name.clone()}).await?, FileDiscernment::Script(hint)=>Self::script(file_name.as_str(),FileWithName{file,name:file_name.clone()},hint).await?, }) } } #[derive(Debug)] pub enum ScriptHint{ Script, LocalScript, ModuleScript, } struct FileHint{ file:FileWithName, hint:ScriptHint, } enum PreparedData{ Model(rbx_dom_weak::WeakDom), Builder(rbx_dom_weak::InstanceBuilder), } enum CompileStackInstruction{ TraverseReferent(rbx_dom_weak::types::Ref,Option<String>), PopFolder, } fn script_builder(class:&str,name:&str,source:String)->rbx_dom_weak::InstanceBuilder{ let mut builder=rbx_dom_weak::InstanceBuilder::new(class); builder.set_name(name); builder.add_property("Source",rbx_dom_weak::types::Variant::String(source)); builder } enum TooComplicated<T>{ Stop, Value(T), Skip, } pub struct CompileConfig{ pub input_folder:PathBuf, pub style:Option<Style>, } #[derive(Debug)] pub enum CompileError{ NullChildRef, IO(std::io::Error), CompileNode(CompileNodeError), DecodeError(rbx_xml::DecodeError), JoinError(tokio::task::JoinError), } impl std::fmt::Display for CompileError{ fn fmt(&self,f:&mut std::fmt::Formatter<'_>)->std::fmt::Result{ write!(f,"{self:?}") } } impl std::error::Error for CompileError{} pub async fn compile(config:CompileConfig,mut dom:&mut rbx_dom_weak::WeakDom)->Result<(),CompileError>{ //hack to traverse root folder as the root object dom.root_mut().name="src".to_owned(); //add in scripts and models let mut folder=config.input_folder.clone(); let mut stack:Vec<CompileStackInstruction>=vec![CompileStackInstruction::TraverseReferent(dom.root_ref(),None)]; while let Some(instruction)=stack.pop(){ match instruction{ CompileStackInstruction::TraverseReferent(item_ref,blacklist)=>{ //scope to avoid holding item ref { let item=dom.get_by_ref(item_ref).ok_or(CompileError::NullChildRef)?; let folder_name=sanitize(item.name.as_str()); folder.push(folder_name.as_ref()); //drop item } stack.push(CompileStackInstruction::PopFolder); //check if a folder exists with item.name if let Ok(dir)=tokio::fs::read_dir(folder.as_path()).await{ let mut exist_names:std::collections::HashSet<String>={ let item=dom.get_by_ref(item_ref).ok_or(CompileError::NullChildRef)?; //push existing dom children objects onto stack (unrelated to exist_names) stack.extend(item.children().into_iter().map(|&referent|CompileStackInstruction::TraverseReferent(referent,None))); //get names of existing objects item.children().into_iter().map(|&child_ref|{ let child=dom.get_by_ref(child_ref).ok_or(CompileError::NullChildRef)?; Ok::<_,CompileError>(sanitize(child.name.as_str()).to_string()) }).collect::<Result<_,CompileError>>()? }; if let Some(dont)=blacklist{ exist_names.insert(dont); } //generate children from folder contents UNLESS! item already has a child of the same name let style=config.style; let exist_names=&exist_names; futures::stream::unfold(dir,|mut dir1|async{ //thread the needle! follow the path that dir takes! let ret1={ //capture a scoped mutable reference so we can forward dir to the next call even on an error let dir2=&mut dir1; (||async move{//error catcher so I can use ? let ret2=if let Some(entry)=dir2.next_entry().await?{ //cull early even if supporting things with identical names is possible if exist_names.contains(entry.file_name().to_str().unwrap()){ TooComplicated::Skip }else{ TooComplicated::Value(entry) } }else{ TooComplicated::Stop }; Ok(ret2) })().await }; match ret1{ Ok(TooComplicated::Stop)=>None, Ok(TooComplicated::Skip)=>Some((Ok(None),dir1)), Ok(TooComplicated::Value(v))=>Some((Ok(Some(v)),dir1)), Err(e)=>Some((Err(CompileError::IO(e)),dir1)), } }) //gotta spawn off the worker threads (Model is slow) .then(|bog|async{ match bog{ Ok(Some(entry))=>tokio::spawn(async move{ let met=entry.metadata().await.map_err(CompileError::IO)?; //discern that bad boy let compile_class={ let result=match met.is_dir(){ true=>CompileNode::from_folder(&entry,style).await, false=>CompileNode::from_file(&entry,style).await, }; match result{ Ok(compile_class)=>compile_class, Err(e)=>{ println!("Ignoring file {entry:?} due to error {e}"); return Ok(None); }, } }; //prepare data structure Ok(Some((compile_class.blacklist,match compile_class.class{ CompileClass::Folder=>PreparedData::Builder(rbx_dom_weak::InstanceBuilder::new("Folder").with_name(compile_class.name.as_str())), CompileClass::Script(source)=>PreparedData::Builder(script_builder("Script",compile_class.name.as_str(),source)), CompileClass::LocalScript(source)=>PreparedData::Builder(script_builder("LocalScript",compile_class.name.as_str(),source)), CompileClass::ModuleScript(source)=>PreparedData::Builder(script_builder("ModuleScript",compile_class.name.as_str(),source)), CompileClass::Model(buf)=>PreparedData::Model(rbx_xml::from_reader_default(std::io::Cursor::new(buf)).map_err(CompileError::DecodeError)?), }))) }).await.map_err(CompileError::JoinError)?, Ok(None)=>Ok(None), Err(e)=>Err(e), } }) //is this even what I want? .map(|f|async{f}).buffer_unordered(32) //begin processing immediately //TODO: fix dom being &mut &mut inside the closure .try_fold((&mut stack,&mut dom),|(stack,dom),bog|async{ //push child objects onto dom serially as they arrive if let Some((blacklist,data))=bog{ let referent=match data{ PreparedData::Model(mut model_dom)=>{ let referent=model_dom.root().children()[0]; model_dom.transfer(referent,dom,item_ref); referent }, PreparedData::Builder(script)=>dom.insert(item_ref,script), }; //new children need to be traversed stack.push(CompileStackInstruction::TraverseReferent(referent,blacklist)); } Ok((stack,dom)) }).await?; } }, CompileStackInstruction::PopFolder=>assert!(folder.pop(),"pop folder bad"), } } Ok(()) }