diff --git a/module/move/wca/src/ca/aggregator.rs b/module/move/wca/src/ca/aggregator.rs index 63fe9d3a01..c6e8f2f2ba 100644 --- a/module/move/wca/src/ca/aggregator.rs +++ b/module/move/wca/src/ca/aggregator.rs @@ -8,7 +8,7 @@ pub( crate ) mod private ProgramParser, Command, grammar::command::private::CommandFormer, - help::{ HelpGeneratorFn, HelpGeneratorArgs, HelpVariants, dot_command }, + help::{ HelpGeneratorFn, HelpGeneratorOptions, HelpVariants }, }; use std::collections::HashSet; @@ -202,7 +202,7 @@ pub( crate ) mod private /// ``` pub fn help< HelpFunction >( mut self, func : HelpFunction ) -> Self where - HelpFunction : Fn( &Dictionary, HelpGeneratorArgs< '_ > ) -> String + 'static + HelpFunction : Fn( &Dictionary, HelpGeneratorOptions< '_ > ) -> String + 'static { self.storage.help_generator = Some( HelpGeneratorFn::new( func ) ); self @@ -255,8 +255,6 @@ pub( crate ) mod private } } - dot_command( &help_generator, &mut ca.dictionary ); - ca } diff --git a/module/move/wca/src/ca/executor/command.rs b/module/move/wca/src/ca/executor/command.rs deleted file mode 100644 index 247a1ce9d9..0000000000 --- a/module/move/wca/src/ca/executor/command.rs +++ /dev/null @@ -1,46 +0,0 @@ -pub( crate ) mod private -{ - use crate::*; - use std::collections::HashMap; - - /// Represents a command that can be executed, with a list of command subjects and a set of command options, and a callback function that defines the command logic. - /// - /// # Example: - /// - /// ``` - /// # use wca::{ ExecutableCommand_, Routine, Value }; - /// # use std::collections::HashMap; - /// ExecutableCommand_ - /// { - /// subjects : vec![ Value::String( "subject_value".to_string() ), /* ... */ ], - /// properties : HashMap::from_iter - /// ([ - /// ( "prop_name".to_string(), Value::Number( 42.0 ) ), - /// /* ... */ - /// ]), - /// routine : Routine::new( |( args, props )| Ok( () ) ) - /// }; - /// ``` - /// - #[ derive( Debug, Clone ) ] - pub struct ExecutableCommand_ - { - /// subjects values - pub subjects : Vec< Value >, - /// properties value - pub properties : HashMap< String, Value >, - /// function that will be called - pub routine : Routine, - } - // qqq : for Bohdan : rid off the structure. VerifiedCommand should be used and passed to userland. - -} - -// - -crate::mod_interface! -{ - exposed use ExecutableCommand_; -} - -// qqq : use orphan instead of exposed for ALL files in the folder, dont use prelude for structs \ No newline at end of file diff --git a/module/move/wca/src/ca/executor/context.rs b/module/move/wca/src/ca/executor/context.rs index ab0b8921ce..ea83670921 100644 --- a/module/move/wca/src/ca/executor/context.rs +++ b/module/move/wca/src/ca/executor/context.rs @@ -13,19 +13,20 @@ pub( crate ) mod private /// let ctx = Context::default(); /// /// ctx.insert( 42 ); - /// assert_eq!( 42, *ctx.get_ref().unwrap() ); + /// assert_eq!( 42, ctx.get().unwrap() ); /// ``` /// /// ``` /// # use wca::{ Routine, Context, Value, Args, Props }; + /// # use std::sync::{ Arc, Mutex }; /// let routine = Routine::new_with_ctx /// ( /// | ( args, props ), ctx | /// { /// let first_arg : i32 = args.get_owned( 0 ).unwrap_or_default(); - /// let ctx_value : &mut i32 = ctx.get_or_default(); + /// let ctx_value : Arc< Mutex< i32 > > = ctx.get_or_default(); /// - /// *ctx_value += first_arg; + /// *ctx_value.lock().unwrap() += first_arg; /// /// Ok( () ) /// } @@ -35,7 +36,7 @@ pub( crate ) mod private /// { /// callback( ( Args( vec![ Value::Number( 1.0 ) ] ), Props( Default::default() ) ), ctx.clone() ).unwrap(); /// } - /// assert_eq!( 1, *ctx.get_ref().unwrap() ); + /// assert_eq!( 1, *ctx.get::< Arc< Mutex< i32 > > >().unwrap().lock().unwrap() ); /// ``` // CloneAny needs to deep clone of Context // qqq : ? diff --git a/module/move/wca/src/ca/executor/converter.rs b/module/move/wca/src/ca/executor/converter.rs deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/module/move/wca/src/ca/executor/executor.rs b/module/move/wca/src/ca/executor/executor.rs index 68717f6762..f9c977261e 100644 --- a/module/move/wca/src/ca/executor/executor.rs +++ b/module/move/wca/src/ca/executor/executor.rs @@ -2,10 +2,9 @@ pub( crate ) mod private { use crate::*; - use ca::executor::runtime::_exec_command; use wtools::error::Result; - use std::sync::Arc; - use std::sync::atomic::Ordering; + use error_tools::return_err; + use ca::help::private::{ HelpGeneratorOptions, LevelOfDetail, generate_help_content }; // aaa : for Bohdan : how is it useful? where is it used? // aaa : `ExecutorType` has been removed @@ -25,46 +24,132 @@ pub( crate ) mod private { /// Executes a program /// - /// Setup runtimes for each namespace into program and run it with specified execution type + /// Iterates over the commands in the program and executes each command using the provided dictionary. + /// This method returns a `Result` indicating whether the execution was successful or not. + /// + /// # Arguments + /// + /// * `dictionary` - A reference to the dictionary used to look up the command routine. + /// * `program` - The program to be executed, which is a `Program` object consisting of a list of commands. + /// + /// # Returns + /// + /// A `Result` with `Ok(())` if the execution was successful, or an `Err` containing an error message if an error occurred. + /// pub fn program( &self, dictionary : &Dictionary, program : Program< VerifiedCommand > ) -> Result< () > { - let context = self.context.clone(); - let runtime = Runtime + for command in program.commands { - context, - pos : 0, - namespace : program.commands, - }; - - Self::sequential_execution_loop( dictionary, runtime )?; + self.command( dictionary, command )?; + } Ok( () ) } - /// Executes a command + /// Executes a given command using a provided dictionary and command. + /// + /// Calls the command callback with the given context if it is necessary. /// - /// Call command callback with context if it is necessary. + /// # Arguments + /// + /// * `dictionary` - A reference to the dictionary used to look up the command routine. + /// * `command` - The verified command that needs to be executed. + /// + /// # Returns + /// + /// Returns a Result indicating success or failure. If successful, returns `Ok(())`, otherwise returns an error. pub fn command( &self, dictionary : &Dictionary, command : VerifiedCommand ) -> Result< () > { - let routine = dictionary.command( &command.phrase ).unwrap().routine.clone(); - _exec_command( command, routine, self.context.clone() ) + if command.internal_command + { + _exec_internal_command( dictionary, command ) + } + else + { + let routine = dictionary.command( &command.phrase ).unwrap().routine.clone(); + _exec_command( command, routine, self.context.clone() ) + } } - - // qqq : for Bohdan : probably redundant + + // aaa : for Bohdan : probably redundant // aaa : removed `parallel_execution_loop` - - fn sequential_execution_loop( dictionary : &Dictionary, mut runtime : Runtime ) -> Result< () > + } + + fn _exec_command( command : VerifiedCommand, routine : Routine, ctx : Context ) -> Result< () > + { + match routine + { + Routine::WithoutContext( routine ) => routine(( Args( command.subjects ), Props( command.properties ) )), + Routine::WithContext( routine ) => routine( ( Args( command.subjects ), Props( command.properties ) ), ctx ), + } + } + + fn _exec_internal_command( dictionary : &Dictionary, command : VerifiedCommand ) -> Result< () > + { + match command.phrase.as_str() { - while !runtime.is_finished() + "." => { - let state = runtime.context.get_or_default::< Arc< RuntimeState > >(); - state.pos.store( runtime.pos + 1, Ordering::Release ); - runtime.r#do( &dictionary )?; - runtime.pos = runtime.context.get::< Arc< RuntimeState > >().unwrap().pos.load( Ordering::Relaxed ); + let generator_args = HelpGeneratorOptions::former() + .command_prefix( "." ) + .form(); + + let content = generate_help_content( dictionary, generator_args ); + println!( "{content}" ); } + ".?" => + { + let generator_args = HelpGeneratorOptions::former() + .description_detailing( LevelOfDetail::Simple ) + .subject_detailing( LevelOfDetail::Simple ) + .property_detailing( LevelOfDetail::Simple ) + .form(); + + let content = generate_help_content( dictionary, generator_args ); + println!( "{content}" ); + } + name if name.ends_with( '.' ) => + { + let name = name.strip_suffix( '.' ).unwrap(); + let commands = dictionary.search( name.strip_prefix( '.' ).unwrap_or( name ) ); + if commands.is_empty() + { + return_err!( "Not found command that starts with `.{}`.", name ); + } + let generator_args = HelpGeneratorOptions::former() + .command_prefix( "." ) + .for_commands( commands ) + .form(); - Ok( () ) + let content = generate_help_content( dictionary, generator_args ); + println!( "{content}" ); + } + name if name.ends_with( ".?" ) => + { + let name = name.strip_suffix( ".?" ).unwrap(); + let command = dictionary.command( &name.strip_prefix( '.' ).unwrap_or( name ).to_string() ); + if let Some( command ) = command + { + let generator_args = HelpGeneratorOptions::former() + .for_commands([ command ]) + .description_detailing( LevelOfDetail::Detailed ) + .subject_detailing( LevelOfDetail::Simple ) + .property_detailing( LevelOfDetail::Simple ) + .with_footer( true ) + .form(); + + let content = generate_help_content( dictionary, generator_args ); + println!( "{content}" ); + } + else + { + return_err!( "Not found command that starts with `.{}`.", name ); + } + } + unexpected => return_err!( "Encountered an unrecognized internal command: `.{}`.", unexpected ), } + + Ok( () ) } } diff --git a/module/move/wca/src/ca/executor/mod.rs b/module/move/wca/src/ca/executor/mod.rs index f0fd5e3997..77789544d0 100644 --- a/module/move/wca/src/ca/executor/mod.rs +++ b/module/move/wca/src/ca/executor/mod.rs @@ -1,12 +1,10 @@ crate::mod_interface! { - /// Executor that is responsible for executing the program’s commands - layer executor; - /// Represents the state of the program's runtime - layer runtime; /// Container for contexts values layer context; + /// Executor that is responsible for executing the program’s commands + layer executor; /// Command callback representation layer routine; diff --git a/module/move/wca/src/ca/executor/runtime.rs b/module/move/wca/src/ca/executor/runtime.rs deleted file mode 100644 index 80ac92f2f6..0000000000 --- a/module/move/wca/src/ca/executor/runtime.rs +++ /dev/null @@ -1,107 +0,0 @@ -pub( crate ) mod private -{ - use crate::*; - use wtools::{ error::Result, err }; - use std::sync::Arc; - use std::sync::atomic::AtomicUsize; - - /// State of a program runtime - /// - /// `RuntimeState` contains information about the current state of a running program. - /// It is used to store information that can be modified during program execution. - /// - /// Can be used to change execution position at runtime. - /// - /// # Examples - /// - /// ``` - /// # use wca::RuntimeState; - /// let mut state = RuntimeState::default(); - /// - /// state.pos = 5; // modify current execution position - /// - /// assert_eq!( state.pos, 5 ); - /// ``` - #[ derive( Debug, Default, Clone ) ] - pub struct RuntimeState - { - /// current execution position that can be changed by user - pub pos : Arc< AtomicUsize >, - } - // qqq : for Bohdan : why? how is it useful? is it? - - /// Represents the state of the program's runtime, including the current context, execution position, and namespace of executable commands. - /// - /// Cloned Runtime will work with the same context. - /// - /// It performs callbacks to commands at the current execution position and, if necessary, provides context for them. - /// - /// ``` - /// # use wca::{ Runtime, Context }; - /// let runtime = Runtime - /// { - /// context : Context::default(), - /// pos : 0, - /// namespace :vec![], - /// }; - /// - /// assert!( runtime.is_finished() ); - /// ``` - #[ derive( Debug, Clone ) ] - pub struct Runtime - { - /// context for current runtime - pub context : Context, - /// current execution position - pub pos : usize, - /// namespace which must be executed - pub namespace : Vec< VerifiedCommand >, // qqq : for Bohdan : use VerifiedCommand - } - // qqq : for Bohdan : why both Runtime and RuntimeState exist? probably one should removed - // qqq : for Bohdan : why both Runtime and Context exist? What about incapsulating Context into Runtime maybe - // qqq : for Bohdan : why both Runtime and Executor exist? rid off of Executor. Incapsulating Executor into Runtime. - - impl Runtime - { - /// returns true if execution position at the end - pub fn is_finished( &self ) -> bool - { - self.namespace.len() == self.pos - } - - /// executes current command( command at current execution position ) - pub fn r#do( &mut self, dictionary : &Dictionary ) -> Result< () > - { - self - .namespace - .get( self.pos ) - .ok_or_else( || err!( "No command here. Current execution pos was `{}`", self.pos ) ) - .and_then( | cmd | - { - let routine = dictionary.command( &cmd.phrase ).unwrap().routine.clone(); - _exec_command( cmd.clone(), routine, self.context.clone() ) - }) - } - } - - // qqq : for Bohdan : _exec_command probably should be method of Runtime. - // qqq : for Bohdan : Accept reference instead of copy. - /// executes a command - pub fn _exec_command( command : VerifiedCommand, routine : Routine, ctx : Context ) -> Result< () > - { - match routine - { - Routine::WithoutContext( routine ) => routine(( Args( command.subjects ), Props( command.properties ) )), - Routine::WithContext( routine ) => routine( ( Args( command.subjects ), Props( command.properties ) ), ctx ), - } - } -} - -// - -crate::mod_interface! -{ - exposed use RuntimeState; - exposed use Runtime; - protected use _exec_command; -} diff --git a/module/move/wca/src/ca/grammar/dictionary.rs b/module/move/wca/src/ca/grammar/dictionary.rs index ca91548186..21fc962efb 100644 --- a/module/move/wca/src/ca/grammar/dictionary.rs +++ b/module/move/wca/src/ca/grammar/dictionary.rs @@ -67,6 +67,25 @@ pub( crate ) mod private { self.commands.get( name ) } + + /// Find commands that match a given name part. + /// + /// This function accepts a `name_part` parameter which is of generic type `NamePart`. + /// The `NamePart` type must implement the `AsRef` trait. + /// + /// # Arguments + /// + /// * `name_part` - The name part to match against command phrases. + /// + /// # Returns + /// + /// A vector of references to `Command` that match the given `name_part`. + pub fn search< NamePart >( &self, name_part : NamePart ) -> Vec< &Command > + where + NamePart : AsRef< str >, + { + self.commands.values().filter( | command | command.phrase.starts_with( name_part.as_ref() ) ).collect() + } } } diff --git a/module/move/wca/src/ca/help.rs b/module/move/wca/src/ca/help.rs index 95a5a04d5b..f4b7f6c3ef 100644 --- a/module/move/wca/src/ca/help.rs +++ b/module/move/wca/src/ca/help.rs @@ -11,35 +11,10 @@ pub( crate ) mod private use std::rc::Rc; use error_tools::for_app::anyhow; use former::Former; + use ca::tool::table::format_table; // qqq : for Bohdan : it should transparent mechanist which patch list of commands, not a stand-alone mechanism - /// Generate `dot` command - pub fn dot_command( generator : &HelpGeneratorFn, dictionary : &mut Dictionary ) - { - let generator = generator.clone(); - let grammar = dictionary.clone(); - let routine = move | props : Props | - { - let prefix : String = props.get_owned( "command_prefix" ).unwrap(); - - let generator_args = HelpGeneratorArgs::former() - .command_prefix( prefix ) - .form(); - - println!( "{}", generator.exec( &grammar, generator_args ) ); - }; - - let cmd = Command::former() - .hint( "prints all available commands" ) - .phrase( "" ) - .property( "command_prefix" ).kind( Type::String ).end() - .routine( routine ) - .form(); - - dictionary.register( cmd ); - } - #[ derive( Debug, Default, Copy, Clone, PartialEq, Eq ) ] pub enum LevelOfDetail { @@ -51,13 +26,13 @@ pub( crate ) mod private /// Container for arguments passed to a help generator function. #[ derive( Debug, Former ) ] - pub struct HelpGeneratorArgs< 'a > + pub struct HelpGeneratorOptions< 'a > { /// Prefix that will be shown before command name #[ default( String::new() ) ] pub command_prefix : String, - /// Show help for the specified command - pub for_command : Option< &'a Command >, + /// Show help for the specified commands + pub for_commands : Vec< &'a Command >, /// Reresents how much information to display for the subjects /// /// - `None` - nothing @@ -81,12 +56,19 @@ pub( crate ) mod private } // qqq : for Barsik : make possible to change properties order - fn generate_help_content( dictionary : &Dictionary, args : HelpGeneratorArgs< '_ > ) -> String + pub( crate ) fn generate_help_content( dictionary : &Dictionary, o : HelpGeneratorOptions< '_ > ) -> String { + struct Row + { + name : String, + args : String, + hint : String, + footer : String, + } let for_single_command = | command : &Command | { let name = &command.phrase; - let hint = match args.description_detailing + let hint = match o.description_detailing { LevelOfDetail::None => "", _ if command.hint.is_empty() && command.long_hint.is_empty() => "", @@ -96,14 +78,14 @@ pub( crate ) mod private _ if !command.hint.is_empty() => command.hint.as_str(), _ => unreachable!(), }; - let subjects = match args.subject_detailing + let subjects = match o.subject_detailing { LevelOfDetail::None => "".into(), _ if command.subjects.is_empty() => "".into(), LevelOfDetail::Simple => "< subjects >".into(), LevelOfDetail::Detailed => command.subjects.iter().map( | v | format!( "< {}{:?} >", if v.optional { "?" } else { "" }, v.kind ) ).collect::< Vec< _ > >().join( " " ), }; - let properties = match args.property_detailing + let properties = match o.property_detailing { LevelOfDetail::None => "".into(), _ if command.subjects.is_empty() => "".into(), @@ -111,10 +93,10 @@ pub( crate ) mod private LevelOfDetail::Detailed => command.properties.iter().map( |( n, v )| format!( "< {n}:{}{:?} >", if v.optional { "?" } else { "" }, v.kind ) ).collect::< Vec< _ > >().join( " " ), }; - let footer = if args.with_footer + let footer = if o.with_footer { let full_subjects = command.subjects.iter().map( | subj | format!( "- {} [{}{:?}]", subj.hint, if subj.optional { "?" } else { "" }, subj.kind ) ).join( "\n\t" ); - let full_properties = command.properties.iter().sorted_by_key( |( name, _ )| *name ).map( |( name, value )| format!( "{name} - {} [{}{:?}]", value.hint, if value.optional { "?" } else { "" }, value.kind ) ).join( "\n\t" ); + let full_properties = format_table( command.properties.iter().sorted_by_key( |( name, _ )| *name ).map( |( name, value )| [ name.clone(), format!( "- {} [{}{:?}]", value.hint, if value.optional { "?" } else { "" }, value.kind ) ] ) ).unwrap().replace( '\n', "\n\t" ); format! ( "{}{}", @@ -123,31 +105,39 @@ pub( crate ) mod private ) } else { "".into() }; - format! - ( - "{}{name}{}{subjects}{}{properties}{}{hint}{}{footer}", - args.command_prefix, - if !subjects.is_empty() || !properties.is_empty() { " " } else { "" }, - if properties.is_empty() { "" } else { " " }, - if hint.is_empty() { "" } else { " - " }, - if footer.is_empty() { "" } else { "\n" }, - ) + Row + { + name : format!( "{}{name}", o.command_prefix ), + args : format!( "{subjects}{}{properties}", if !subjects.is_empty() || !properties.is_empty() { " " } else { "" } ), + hint : format!( "{}{hint}", if hint.is_empty() { "" } else { "- " } ), + footer, + } }; - if let Some( command ) = args.for_command + if o.for_commands.len() == 1 || !o.for_commands.is_empty() && !o.with_footer { - for_single_command( command ) + o.for_commands.into_iter().map( | command | + { + let row = for_single_command( command ); + format! + ( + "{}{}{}", + format_table([[ row.name, row.args, row.hint ]]).unwrap(), + if row.footer.is_empty() { "" } else { "\n" }, + row.footer + ) + }) + .join( "\n" ) } else { - dictionary.commands + let rows = dictionary.commands .iter() .sorted_by_key( |( name, _ )| *name ) .map( |( _, cmd )| cmd ) .map( for_single_command ) - .fold( String::new(), | acc, cmd | - { - format!( "{acc}{}{cmd}", if acc.is_empty() { "" } else { "\n" } ) - }) + .map( | row | [ row.name, row.args, row.hint ] ); + + format_table( rows ).unwrap() } } @@ -221,7 +211,8 @@ pub( crate ) mod private text = generator.exec ( &grammar, - HelpGeneratorArgs::former() + HelpGeneratorOptions::former() + .command_prefix( "." ) .description_detailing( LevelOfDetail::Simple ) .subject_detailing( LevelOfDetail::Simple ) .property_detailing( LevelOfDetail::Simple ) @@ -269,8 +260,9 @@ pub( crate ) mod private let command = args.get_owned::< String >( 0 ).unwrap(); let cmd = grammar.commands.get( &command ).ok_or_else( || anyhow!( "Can not found help for command `{command}`" ) )?; - let args = HelpGeneratorArgs::former() - .for_command( cmd ) + let args = HelpGeneratorOptions::former() + .command_prefix( "." ) + .for_commands([ cmd ]) .description_detailing( LevelOfDetail::Detailed ) .subject_detailing( LevelOfDetail::Simple ) .property_detailing( LevelOfDetail::Simple ) @@ -350,15 +342,15 @@ pub( crate ) mod private // } } - type HelpFunctionFn = Rc< dyn Fn( &Dictionary, HelpGeneratorArgs< '_ > ) -> String >; + type HelpFunctionFn = Rc< dyn Fn( &Dictionary, HelpGeneratorOptions< '_ > ) -> String >; /// Container for function that generates help string for any command /// /// ``` - /// # use wca::ca::help::{ HelpGeneratorArgs, HelpGeneratorFn }; + /// # use wca::ca::help::{ HelpGeneratorOptions, HelpGeneratorFn }; /// use wca::{ Command, Dictionary }; /// - /// fn my_help_generator( grammar : &Dictionary, command : Option< &Command > ) -> String + /// fn my_help_generator( dictionary : &Dictionary, args : HelpGeneratorOptions< '_ > ) -> String /// { /// format!( "Help content based on grammar and command" ) /// } @@ -366,10 +358,10 @@ pub( crate ) mod private /// let help_fn = HelpGeneratorFn::new( my_help_generator ); /// # let grammar = &Dictionary::former().form(); /// - /// help_fn.exec( grammar, HelpGeneratorArgs::former().form() ); + /// help_fn.exec( grammar, HelpGeneratorOptions::former().form() ); /// // or /// # let cmd = Command::former().form(); - /// help_fn.exec( grammar, HelpGeneratorArgs::former().for_command( &cmd ).form() ); + /// help_fn.exec( grammar, HelpGeneratorOptions::former().for_command( &cmd ).form() ); /// ``` #[ derive( Clone ) ] pub struct HelpGeneratorFn( HelpFunctionFn ); @@ -387,7 +379,7 @@ pub( crate ) mod private /// Wrap a help function pub fn new< HelpFunction >( func : HelpFunction ) -> Self where - HelpFunction : Fn( &Dictionary, HelpGeneratorArgs< '_ > ) -> String + 'static + HelpFunction : Fn( &Dictionary, HelpGeneratorOptions< '_ > ) -> String + 'static { Self( Rc::new( func ) ) } @@ -396,7 +388,7 @@ pub( crate ) mod private impl HelpGeneratorFn { /// Executes the function to generate help content - pub fn exec( &self, dictionary : &Dictionary, args : HelpGeneratorArgs< '_ > ) -> String + pub fn exec( &self, dictionary : &Dictionary, args : HelpGeneratorOptions< '_ > ) -> String { self.0( dictionary, args ) } @@ -416,7 +408,6 @@ pub( crate ) mod private crate::mod_interface! { protected use HelpGeneratorFn; - protected use HelpGeneratorArgs; - protected use dot_command; + protected use HelpGeneratorOptions; prelude use HelpVariants; } diff --git a/module/move/wca/src/ca/mod.rs b/module/move/wca/src/ca/mod.rs index 47491177dc..de9ccf9392 100644 --- a/module/move/wca/src/ca/mod.rs +++ b/module/move/wca/src/ca/mod.rs @@ -18,6 +18,8 @@ crate::mod_interface! layer input; // /// The missing batteries of WCA. // layer facade; + /// Genera-purpose tools which might be moved out one day. + layer tool; /// Responsible for aggregating all commands that the user defines, and for parsing and executing them layer aggregator; diff --git a/module/move/wca/src/ca/parser/command.rs b/module/move/wca/src/ca/parser/command.rs index 1e9b7c6d90..13a9be8230 100644 --- a/module/move/wca/src/ca/parser/command.rs +++ b/module/move/wca/src/ca/parser/command.rs @@ -10,7 +10,6 @@ pub( crate ) mod private }, wtools }; - use std::collections::HashMap; use wtools::{ error:: Result, err }; use nom:: { @@ -66,33 +65,21 @@ pub( crate ) mod private properties : props.into_iter().map( |( _, prop )| prop ).collect() } }), - // it is the end + // spacial cases + map( tag( "?" ), | _ | Command { name : format!( "{}?", command_prefix ), ..Default::default() } ), map ( eof, | _ | Command { - properties : HashMap::from_iter([ ( "command_prefix".to_string(), command_prefix.to_string() ) ]), ..Default::default() + name : command_prefix.to_string(), + ..Default::default() } ) )), )), - |( _, _, command )| - { - if command.name.ends_with( command_prefix ) - { - Command { - name : "".to_string(), - subjects : vec![ command.name ], - properties : HashMap::from_iter([ ( "command_prefix".to_string(), command_prefix.to_string() ) ]), - } - } - else - { - command - } - } + |( _, _, command )| command )( input ) ) } } diff --git a/module/move/wca/src/ca/tool/mod.rs b/module/move/wca/src/ca/tool/mod.rs new file mode 100644 index 0000000000..a5700ae916 --- /dev/null +++ b/module/move/wca/src/ca/tool/mod.rs @@ -0,0 +1,7 @@ +crate::mod_interface! +{ + + /// Format table + layer table; + +} diff --git a/module/move/wca/src/ca/tool/table.rs b/module/move/wca/src/ca/tool/table.rs new file mode 100644 index 0000000000..7c62d76d3a --- /dev/null +++ b/module/move/wca/src/ca/tool/table.rs @@ -0,0 +1,126 @@ +mod private +{ + use crate::*; + + use wtools::error::{ Result, err }; + + /// Represents a table composed of multiple rows. + /// + /// The `Table` struct is a simple container that holds multiple `Row` objects. + #[ derive( Debug ) ] + pub struct Table( Vec< Row > ); + + impl< T, R > From< T > for Table + where + T : IntoIterator< Item = R >, + R : Into< Row >, + { + fn from( value : T ) -> Self + { + Self( value.into_iter().map( Into::into ).collect() ) + } + } + + impl Table + { + /// Validates the structure of the given `self` object. + /// + /// It checks if all the rows have the same length as the first row of the object. + /// If all the rows have the same length, it returns `true`, otherwise it returns `false`. + /// + /// # Returns + /// + /// - `true` if all the rows have the same length + /// - `false` if at least one row has a different length + pub fn validate( &self ) -> bool + { + let mut row_iter = self.0.iter(); + let Some( first_row ) = row_iter.next() else { return true }; + let first_row_length = first_row.0.len(); + for row in row_iter + { + if row.0.len() != first_row_length + { + return false; + } + } + + true + } + } + + /// Represents a row in a table. + /// + /// The `Row` struct is a container that holds multiple `String` objects representing the values in a table row. + #[ derive( Debug ) ] + pub struct Row( Vec< String > ); + + impl< R, V > From< R > for Row + where + R : IntoIterator< Item = V >, + V : Into< String >, + { + fn from( value : R ) -> Self + { + Self( value.into_iter().map( Into::into ).collect() ) + } + } + + fn max_column_lengths( table : &Table ) -> Vec< usize > + { + let num_columns = table.0.get( 0 ).map_or( 0, | row | row.0.len() ); + ( 0 .. num_columns ) + .map( | column_index | + { + table.0.iter() + .map( | row | row.0[ column_index ].len() ) + .max() + .unwrap_or( 0 ) + }) + .collect() + } + + /// Formats a table into a readable string representation. + /// + /// # Arguments + /// + /// * `table` - The table to be formatted. + /// + /// # Returns + /// + /// * `Result` - A `Result` containing the formatted table as a `String`, or an `Error` if the table is invalid. + pub fn format_table< IntoTable >( table : IntoTable ) -> Result< String > + where + IntoTable : Into< Table >, + { + let table = table.into(); + if !table.validate() + { + return Err( err!( "Invalid table" ) ); + } + + let max_lengths = max_column_lengths( &table ); + + let mut formatted_table = String::new(); + for row in table.0 + { + for ( i, cell ) in row.0.iter().enumerate() + { + formatted_table.push_str( &format!( "{:width$}", cell, width = max_lengths[ i ] ) ); + formatted_table.push( ' ' ); + } + formatted_table.pop(); // trailing space + formatted_table.push( '\n' ); + } + formatted_table.pop(); // trailing end of line + + Ok( formatted_table ) + } +} + +// + +crate::mod_interface! +{ + protected use format_table; +} diff --git a/module/move/wca/src/ca/verifier/command.rs b/module/move/wca/src/ca/verifier/command.rs index bd59173212..09628321c0 100644 --- a/module/move/wca/src/ca/verifier/command.rs +++ b/module/move/wca/src/ca/verifier/command.rs @@ -29,6 +29,8 @@ pub( crate ) mod private { /// Phrase descriptor for command. pub phrase : String, + /// Flag indicating whether a command is internal or not. + pub internal_command : bool, /// Command subjects. pub subjects : Vec< Value >, /// Command options. diff --git a/module/move/wca/src/ca/verifier/verifier.rs b/module/move/wca/src/ca/verifier/verifier.rs index 1759fb42cc..2927cb637a 100644 --- a/module/move/wca/src/ca/verifier/verifier.rs +++ b/module/move/wca/src/ca/verifier/verifier.rs @@ -441,6 +441,16 @@ pub( crate ) mod private /// Make sure that this command is described in the grammar and matches it(command itself and all it options too). pub fn to_command( &self, dictionary : &Dictionary, raw_command : ParsedCommand ) -> Result< VerifiedCommand > { + if raw_command.name.ends_with( '.' ) | raw_command.name.ends_with( ".?" ) + { + return Ok( VerifiedCommand + { + phrase : raw_command.name, + internal_command : true, + subjects : vec![], + properties : HashMap::new(), + }); + } let variants = dictionary.command( &raw_command.name ) .ok_or_else::< error::for_app::Error, _ > ( @@ -485,6 +495,7 @@ pub( crate ) mod private Ok( VerifiedCommand { phrase : cmd.phrase.to_owned(), + internal_command : false, subjects, properties, }) diff --git a/module/move/wca/tests/inc/commands_aggregator/basic.rs b/module/move/wca/tests/inc/commands_aggregator/basic.rs index f01965a6ed..292cc94ea7 100644 --- a/module/move/wca/tests/inc/commands_aggregator/basic.rs +++ b/module/move/wca/tests/inc/commands_aggregator/basic.rs @@ -71,8 +71,6 @@ tests_impls! a_id!( (), ca.perform( "." ).unwrap() ); // qqq : this use case is disabled // a_id!( (), ca.perform( ".cmd." ).unwrap() ); - - a_true!( ca.perform( ".c." ).is_err() ); } fn error_types() diff --git a/module/move/wca/tests/inc/commands_aggregator/help.rs b/module/move/wca/tests/inc/commands_aggregator/help.rs index 4d46399274..542b94ad43 100644 --- a/module/move/wca/tests/inc/commands_aggregator/help.rs +++ b/module/move/wca/tests/inc/commands_aggregator/help.rs @@ -53,7 +53,7 @@ wca = {{path = "{}"}}"#, assert_eq! ( - "Help command\n\necho < subjects > < properties > - prints all subjects and properties\n\nSubjects:\n\t- Subject [?String]\nProperties:\n\tproperty - simple property [?String]\n", + "Help command\n\n.echo < subjects > < properties > - prints all subjects and properties\n\nSubjects:\n\t- Subject [?String]\nProperties:\n\tproperty - simple property [?String]\n", result ); } diff --git a/module/move/wca/tests/inc/parser/command.rs b/module/move/wca/tests/inc/parser/command.rs index 35929c07ce..ff75539190 100644 --- a/module/move/wca/tests/inc/parser/command.rs +++ b/module/move/wca/tests/inc/parser/command.rs @@ -398,9 +398,9 @@ tests_impls! ( ParsedCommand { - name : "".into(), + name : ".".into(), subjects : vec![], - properties : HashMap::from_iter([( "command_prefix".into(), ".".into() ) ]), + properties : HashMap::new(), }, parser.command( "." ).unwrap() ); @@ -410,24 +410,12 @@ tests_impls! ( ParsedCommand { - name : "".into(), - subjects : vec![ "command.".into() ], - properties : HashMap::from_iter([( "command_prefix".into(), ".".into() ) ]), + name : "command.".into(), + subjects : vec![], + properties : HashMap::new(), }, parser.command( ".command." ).unwrap() ); - - // command . with subjects - a_id! - ( - ParsedCommand - { - name : "".into(), - subjects : vec![ "command.".into() ], - properties : HashMap::from_iter([( "command_prefix".into(), ".".into() ) ]), - }, - parser.command( ".command. " ).unwrap() - ); } } diff --git a/module/move/willbe/src/action/readme_health_table_renew.rs b/module/move/willbe/src/action/readme_health_table_renew.rs index d1e5671597..e6ebfe34f9 100644 --- a/module/move/willbe/src/action/readme_health_table_renew.rs +++ b/module/move/willbe/src/action/readme_health_table_renew.rs @@ -347,7 +347,18 @@ mod private packages::FilterMapOptions { package_filter: module_package_filter, dependency_filter: module_dependency_filter }, ); let module_graph = graph::construct( &module_packages_map ); - graph::toposort( module_graph ).map_err( | err | err!( "{}", err ) ) + let names = graph::topological_sort_with_grouping( module_graph ) + .into_iter() + .map + ( + | mut group | + { + group.sort(); + group + } + ).flatten().collect::< Vec< _ > >(); + + Ok(names) } /// Generate row that represents a module, with a link to it in the repository and optionals for stability, branches, documentation and links to the gitpod. @@ -356,7 +367,9 @@ mod private let mut rou = format!( "| [{}]({}/{}) |", &module_name, &table_parameters.base_path, &module_name ); if table_parameters.include_stability { - rou.push_str( &stability_generate( &stability.as_ref().unwrap() ) ); + let mut stability = stability_generate( &stability.as_ref().unwrap() ); + stability.push_str( " |" ); + rou.push_str( &stability ); } if parameters.branches.is_some() && table_parameters.include_branches { @@ -378,11 +391,11 @@ mod private { match stability { - Stability::Experimental => " [![experimental](https://raster.shields.io/static/v1?label=&message=experimental&color=orange)](https://github.com/emersion/stability-badges#experimental) |".into(), - Stability::Stable => " [![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://github.com/emersion/stability-badges#stable) |".into(), - Stability::Deprecated => " [![stability-deprecated](https://img.shields.io/badge/stability-deprecated-red.svg)](https://github.com/emersion/stability-badges#deprecated) |".into(), - Stability::Unstable => " [![stability-unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable) |".into(), - Stability::Frozen => " [![stability-frozen](https://img.shields.io/badge/stability-frozen-blue.svg)](https://github.com/emersion/stability-badges#frozen) |".into(), + Stability::Experimental => " [![experimental](https://raster.shields.io/static/v1?label=&message=experimental&color=orange)](https://github.com/emersion/stability-badges#experimental)".into(), + Stability::Stable => " [![stability-stable](https://img.shields.io/badge/stability-stable-green.svg)](https://github.com/emersion/stability-badges#stable)".into(), + Stability::Deprecated => " [![stability-deprecated](https://img.shields.io/badge/stability-deprecated-red.svg)](https://github.com/emersion/stability-badges#deprecated)".into(), + Stability::Unstable => " [![stability-unstable](https://img.shields.io/badge/stability-unstable-yellow.svg)](https://github.com/emersion/stability-badges#unstable)".into(), + Stability::Frozen => " [![stability-frozen](https://img.shields.io/badge/stability-frozen-blue.svg)](https://github.com/emersion/stability-badges#frozen)".into(), } } diff --git a/module/move/willbe/src/entity/mod.rs b/module/move/willbe/src/entity/mod.rs index 5a0be78e1f..24342f1c3b 100644 --- a/module/move/willbe/src/entity/mod.rs +++ b/module/move/willbe/src/entity/mod.rs @@ -12,6 +12,10 @@ crate::mod_interface! /// Handles operations related to packed Rust crates layer packed_crate; orphan use super::packed_crate; + + /// Facade for `preatytable` crate. + layer table; + orphan use super::table; /// Provides a set of functionalities for handling and manipulating packages. layer packages; diff --git a/module/move/willbe/src/entity/table.rs b/module/move/willbe/src/entity/table.rs new file mode 100644 index 0000000000..564c1f6d43 --- /dev/null +++ b/module/move/willbe/src/entity/table.rs @@ -0,0 +1,109 @@ +mod private +{ + use std::fmt::{Display, Formatter}; + + /// An owned printable table. + #[ derive( Debug ) ] + pub struct Table + { + inner : prettytable::Table, + } + + impl Display for Table + { + fn fmt( &self, f : &mut Formatter< '_ > ) -> std::fmt::Result + { + writeln!( f, "{}", self.inner.to_string() ) + } + } + + impl Table + { + /// Create an empty table. + pub fn new() -> Self + { + Self + { + inner : prettytable::Table::new(), + } + } + } + + impl Table + { + /// Set the optional header. + pub fn set_header(&mut self, row : Row ) + { + self.inner.set_titles( row.inner ); + } + + /// Append a row in the table. + pub fn add_row(&mut self, row : Row ) + { + self.inner.add_row( row.inner ); + } + } + + impl Default for Table + { + fn default() -> Self + { + let mut table = Self::new(); + let format = default_format(); + table.inner.set_format( format ); + table + } + } + + fn default_format() -> prettytable::format::TableFormat + { + let format = prettytable::format::FormatBuilder::new() + .column_separator( ' ' ) + .borders( ' ' ) + .separators + ( + &[ prettytable::format::LinePosition::Title ], + prettytable::format::LineSeparator::new( '-', '+', '+', '+' ) + ) + .padding( 1, 1 ) + .build(); + format + } + + /// Represent a table row made of cells. + #[ derive( Debug ) ] + pub struct Row + { + inner : prettytable::Row, + } + + impl Row + { + + /// Append a cell at the end of the row. + pub fn add_cell( &mut self, title : &str ) + { + let mut cell = prettytable::Cell::new( title ); + cell.align( prettytable::format::Alignment::CENTER ); + self.inner.add_cell( prettytable::Cell::new( title ) ); + } + } + + impl Row + { + /// Create an row of length size, with empty strings stored. + pub fn new() -> Self + { + Self + { + inner : prettytable::Row::empty(), + } + } + } +} + +crate::mod_interface! +{ + protected use Table; + protected use Row; +} \ No newline at end of file diff --git a/module/move/willbe/src/entity/test.rs b/module/move/willbe/src/entity/test.rs index 6b154b42a0..219fa2dfcb 100644 --- a/module/move/willbe/src/entity/test.rs +++ b/module/move/willbe/src/entity/test.rs @@ -4,6 +4,7 @@ mod private // qqq : for Petro : use https://github.com/console-rs/indicatif use crate::*; + use table::*; use std:: { collections::{ BTreeMap, BTreeSet, HashSet }, @@ -20,12 +21,6 @@ mod private // aaa : ✅ use colored::Colorize; // qqq : for Petro : don't do micro imports - use prettytable:: - { - Cell, - Row, - Table, - }; // qqq : for Petro : don't do micro imports #[ cfg( feature = "progress_bar" ) ] use indicatif:: @@ -34,12 +29,6 @@ mod private ProgressBar, ProgressStyle }; - use prettytable::format:: - { - Alignment, - FormatBuilder, - TableFormat - }; use rayon::ThreadPoolBuilder; use process_tools::process::*; use wtools::error::anyhow::{ Error, format_err }; @@ -181,26 +170,26 @@ mod private ff.push( feature ); } } - let mut table = Table::new(); - let format = format(); - table.set_format( format ); + let mut table = Table::default(); + // let format = format(); + // table.set_format( format ); - let mut header_row = Row::empty(); - header_row.add_cell( Cell::new( "Channel" ) ); - header_row.add_cell( Cell::new( "Opt" ) ); + let mut header_row = Row::new(); + header_row.add_cell( "Channel" ); + header_row.add_cell( "Opt" ); for feature in &ff { - header_row.add_cell( Cell::new( feature ) ); + header_row.add_cell( feature ); } - table.set_titles( header_row ); + table.set_header( header_row ); for variant in &self.test_variants { - let mut row = Row::empty(); + let mut row = Row::new(); - row.add_cell( Cell::new( &variant.channel.to_string() ) ); - row.add_cell( Cell::new( &variant.optimization.to_string() ) ); + row.add_cell( &variant.channel.to_string() ); + row.add_cell( &variant.optimization.to_string() ); let counter = 0; let flag = true; generate_features_cells(&mut ff, variant, &mut row, counter, flag, &self.enabled_features ); @@ -288,8 +277,7 @@ mod private { for feature in ff { - let mut c = Cell::new("+"); - c.align( Alignment::CENTER ); + let mut c = "+"; if variant.features.is_empty() && counter == enabled_features.len() && flag { flag = false; @@ -301,28 +289,13 @@ mod private } else { - c = Cell::new( "" ); + c = ""; row.add_cell( c ); } counter += 1; } } - - fn format() -> TableFormat - { - let format = FormatBuilder::new() - .column_separator( ' ' ) - .borders( ' ' ) - .separators - ( - &[ prettytable::format::LinePosition::Title ], - prettytable::format::LineSeparator::new( '-', '+', '+', '+' ) - ) - .padding( 1, 1 ) - .build(); - format - } - + #[ derive( Debug, Former ) ] pub struct PackageTestOptions< 'a > { @@ -566,18 +539,16 @@ mod private ff.push( feature ); } } - let mut table = Table::new(); - let format = format(); - table.set_format( format ); - let mut header_row = Row::empty(); - header_row.add_cell( Cell::new( "Result" ) ); - header_row.add_cell( Cell::new( "Channel" ) ); - header_row.add_cell( Cell::new( "Opt" ) ); + let mut table = Table::default(); + let mut header_row = Row::new(); + header_row.add_cell( "Result" ); + header_row.add_cell( "Channel" ); + header_row.add_cell( "Opt" ); for feature in &ff { - header_row.add_cell( Cell::new( feature ) ); + header_row.add_cell( feature ); } - table.set_titles( header_row ); + table.set_header( header_row ); writeln!( f, "{} {}\n", "\n=== Module".bold(), self.package_name.0.bold() )?; if self.tests.is_empty() @@ -587,7 +558,7 @@ mod private } for ( variant, result) in &self.tests { - let mut row = Row::empty(); + let mut row = Row::new(); let result_text = match result { Ok( _ ) => @@ -604,9 +575,9 @@ mod private "❌" }, }; - row.add_cell( Cell::new( result_text ) ); - row.add_cell( Cell::new( &variant.channel.to_string() ) ); - row.add_cell( Cell::new( &variant.optimization.to_string() ) ); + row.add_cell( result_text ); + row.add_cell( &variant.channel.to_string() ); + row.add_cell( &variant.optimization.to_string() ); let counter = 0; let flag = true; generate_features_cells( &mut ff, variant, &mut row, counter, flag, &self.enabled_features ); diff --git a/module/move/willbe/src/tool/graph.rs b/module/move/willbe/src/tool/graph.rs index 4e13a84fbc..9191e31117 100644 --- a/module/move/willbe/src/tool/graph.rs +++ b/module/move/willbe/src/tool/graph.rs @@ -10,6 +10,7 @@ pub( crate ) mod private hash::Hash, collections::{ HashMap, HashSet } }; + use std::collections::VecDeque; use std::path::PathBuf; use petgraph:: { @@ -102,6 +103,71 @@ pub( crate ) mod private } } + /// The function performs a topological sort of a graph with grouping. + /// + /// # Arguments + /// + /// * `graph` - A graph represented as an adjacency list. Each node in the graph represents a task, and edges represent dependencies. + /// + /// # Returns + /// + /// The function returns a vector of vectors, where each inner vector represents a group of nodes that can be executed in parallel. Tasks within each group are sorted in topological order. + pub fn topological_sort_with_grouping< 'a, PackageIdentifier : Clone + std::fmt::Debug > + ( + graph : Graph< &'a PackageIdentifier, &'a PackageIdentifier > + ) + -> Vec< Vec< PackageIdentifier > > + { + let mut in_degree = HashMap::new(); + for node in graph.node_indices() + { + in_degree.insert( node, graph.neighbors_directed( node, Incoming ).count() ); + } + + let mut roots = VecDeque::new(); + for ( node, °ree ) in in_degree.iter() + { + if degree == 0 + { + roots.push_back( *node ); + } + } + + let mut result = Vec::new(); + while !roots.is_empty() + { + let mut next_roots = Vec::new(); + let mut group = Vec::new(); + while let Some( node ) = roots.pop_front() + { + group.push( node ); + for edge in graph.neighbors( node ) + { + let degree = in_degree.get_mut( &edge ).unwrap(); + *degree -= 1; + if *degree == 0 + { + next_roots.push( edge ); + } + } + } + roots = VecDeque::from( next_roots ); + result.push( group ); + } + result + .into_iter() + .map + ( + | vec | + vec + .iter() + .map( | dep_idx | ( *graph.node_weight( *dep_idx ).unwrap() ).clone() ) + .collect() + ) + .rev() + .collect() + } + /// Creates a subgraph from the given graph, containing only the nodes and edges reachable from the roots. /// /// # Arguments @@ -238,6 +304,7 @@ crate::mod_interface! { protected use construct; protected use toposort; + protected use topological_sort_with_grouping; protected use subgraph; protected use remove_not_required_to_publish; } diff --git a/module/move/willbe/tests/inc/action/cicd_renew.rs b/module/move/willbe/tests/inc/action/cicd_renew.rs index 497e08b1b4..ddd92bb908 100644 --- a/module/move/willbe/tests/inc/action/cicd_renew.rs +++ b/module/move/willbe/tests/inc/action/cicd_renew.rs @@ -31,7 +31,7 @@ fn arrange( sample_dir : &str ) -> assert_fs::TempDir struct Workflow { name : String, - on : String, + on : HashMap>>, env : HashMap< String, String >, jobs : HashMap< String, Job >, } @@ -72,7 +72,18 @@ fn default_case() let expected = Workflow { name : "test_module".into(), - on : "push".into(), + on : + { + let mut map = HashMap::new(); + let mut push_map = HashMap::new(); + push_map.insert + ( + "branches-ignore".to_string(), + vec![ "alpha".to_string(), "beta".to_string(), "master".to_string() ], + ); + map.insert( "push".to_string(), push_map ); + map + }, env : HashMap::from_iter( [ ( "CARGO_TERM_COLOR".to_string(), "always".to_string() ) ] ), jobs : HashMap::from_iter( [ ( "test".to_string(), job ) ] ), }; diff --git a/module/move/willbe/tests/inc/action/readme_modules_headers_renew.rs b/module/move/willbe/tests/inc/action/readme_modules_headers_renew.rs index df61049bf9..3b731914bb 100644 --- a/module/move/willbe/tests/inc/action/readme_modules_headers_renew.rs +++ b/module/move/willbe/tests/inc/action/readme_modules_headers_renew.rs @@ -56,6 +56,7 @@ fn default_stability() // Assert assert!( actual.contains( "[![experimental](https://raster.shields.io/static/v1?label=&message=experimental&color=orange)](https://github.com/emersion/stability-badges#experimental)" ) ); + assert!( !actual.contains( "|" ) ); } #[ test ] diff --git a/module/move/willbe/tests/inc/tool/graph.rs b/module/move/willbe/tests/inc/tool/graph.rs index b7a3608cd9..28c08d7b75 100644 --- a/module/move/willbe/tests/inc/tool/graph.rs +++ b/module/move/willbe/tests/inc/tool/graph.rs @@ -4,6 +4,7 @@ mod toposort use graph::toposort; use std::collections::HashMap; use petgraph::Graph; + use willbe::graph::topological_sort_with_grouping; struct IndexMap< T >( HashMap< T, usize > ); @@ -119,4 +120,98 @@ mod toposort let _sorted = toposort( graph ).unwrap(); } + + // input + // B -> A + // C -> A + // output + // [A], [B,C] + #[ test ] + fn simple_case() + { + let mut graph = Graph::new(); + + let a_node = graph.add_node( &"A" ); + let b_node = graph.add_node( &"B" ); + let c_node = graph.add_node( &"C" ); + + graph.add_edge( b_node, a_node, &"B->A"); + graph.add_edge( c_node, a_node, &"C->A"); + + let groups = topological_sort_with_grouping( graph ); + + assert_eq!( groups[ 0 ], vec![ "A" ] ); + assert_eq!( groups[1].len(), 2 ); + assert!( groups[ 1 ].contains( &"C" ) ); + assert!( groups[ 1 ].contains( &"B" ) ); + } + + // input + // digraph { + // 0 [ label = "0" ] + // 1 [ label = "1" ] + // 2 [ label = "2" ] + // 3 [ label = "3" ] + // 4 [ label = "4" ] + // 5 [ label = "5" ] + // 6 [ label = "6" ] + // 7 [ label = "7" ] + // 4 -> 0 [ label = "" ] + // 5 -> 0 [ label = "" ] + // 6 -> 0 [ label = "" ] + // 1 -> 3 [ label = "" ] + // 2 -> 3 [ label = "" ] + // 7 -> 6 [ label = "" ] + // 3 -> 4 [ label = "" ] + // 3 -> 5 [ label = "" ] + // 3 -> 6 [ label = "" ] + // } + // visualization : https://viz-js.com/?dot=ZGlncmFwaCB7CiAgICAwIFsgbGFiZWwgPSAiMCIgXQogICAgMSBbIGxhYmVsID0gIjEiIF0KICAgIDIgWyBsYWJlbCA9ICIyIiBdCiAgICAzIFsgbGFiZWwgPSAiMyIgXQogICAgNCBbIGxhYmVsID0gIjQiIF0KICAgIDUgWyBsYWJlbCA9ICI1IiBdCiAgICA2IFsgbGFiZWwgPSAiNiIgXQogICAgNyBbIGxhYmVsID0gIjciIF0KICAgIDQgLT4gMCBbIGxhYmVsID0gIiIgXQogICAgNSAtPiAwIFsgbGFiZWwgPSAiIiBdCiAgICA2IC0-IDAgWyBsYWJlbCA9ICIiIF0KICAgIDEgLT4gMyBbIGxhYmVsID0gIiIgXQogICAgMiAtPiAzIFsgbGFiZWwgPSAiIiBdCiAgICA3IC0-IDYgWyBsYWJlbCA9ICIiIF0KICAgIDMgLT4gNCBbIGxhYmVsID0gIiIgXQogICAgMyAtPiA1IFsgbGFiZWwgPSAiIiBdCiAgICAzIC0-IDYgWyBsYWJlbCA9ICIiIF0KfQo~ + // output + // [0], [6,5,4], [3], [1,2,7] + #[ test ] + fn complicated_test() + { + let mut graph = Graph::new(); + + let n = graph.add_node( &"0" ); + let n_1 = graph.add_node( &"1" ); + let n_2 = graph.add_node( &"2" ); + let n_3 = graph.add_node( &"3" ); + let n_4 = graph.add_node( &"4" ); + let n_5 = graph.add_node( &"5" ); + let n_6 = graph.add_node( &"6" ); + let n_7 = graph.add_node( &"7" ); + + graph.add_edge( n_1, n_3, &"" ); + graph.add_edge( n_2, n_3, &"" ); + graph.add_edge( n_7, n_6, &"" ); + + graph.add_edge( n_3, n_4, &"" ); + graph.add_edge( n_3, n_5, &"" ); + graph.add_edge( n_3, n_6, &"" ); + + graph.add_edge( n_4, n, &"" ); + graph.add_edge( n_5, n, &"" ); + graph.add_edge( n_6, n, &"" ); + + let groups = topological_sort_with_grouping( graph ); + + dbg!(&groups); + + assert_eq!( groups[ 0 ], vec![ "0" ] ); + + assert_eq!( groups[1].len(), 3 ); + assert!( groups[ 1 ].contains( &"6" ) ); + assert!( groups[ 1 ].contains( &"5" ) ); + assert!( groups[ 1 ].contains( &"4" ) ); + + assert_eq!( groups[ 2 ], vec![ "3" ] ); + + assert_eq!( groups[3].len(), 3 ); + assert!( groups[ 3 ].contains( &"1" ) ); + assert!( groups[ 3 ].contains( &"2" ) ); + assert!( groups[ 3 ].contains( &"7" ) ); + } + } \ No newline at end of file