diff --git a/Cargo.toml b/Cargo.toml index 8d6389c8bf..cfbea9f300 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -492,7 +492,7 @@ version = "~0.2.0" path = "module/move/sqlx_query" [workspace.dependencies.deterministic_rand] -version = "~0.5.0" +version = "~0.6.0" path = "module/move/deterministic_rand" [workspace.dependencies.crates_tools] diff --git a/module/core/diagnostics_tools/src/diag/rta.rs b/module/core/diagnostics_tools/src/diag/rta.rs index 46b21050a2..4bd27b3bba 100644 --- a/module/core/diagnostics_tools/src/diag/rta.rs +++ b/module/core/diagnostics_tools/src/diag/rta.rs @@ -289,4 +289,3 @@ pub mod prelude }; } - diff --git a/module/core/diagnostics_tools/src/lib.rs b/module/core/diagnostics_tools/src/lib.rs index a3415c710e..8ed4ccb486 100644 --- a/module/core/diagnostics_tools/src/lib.rs +++ b/module/core/diagnostics_tools/src/lib.rs @@ -31,7 +31,6 @@ pub mod own #[ doc( inline ) ] pub use orphan::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super::diag::orphan::*; } @@ -54,7 +53,6 @@ pub mod exposed #[ doc( inline ) ] pub use prelude::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super::diag::exposed::*; } @@ -65,6 +63,5 @@ pub mod prelude { use super::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super::diag::prelude::*; } diff --git a/module/core/format_tools/src/format.rs b/module/core/format_tools/src/format.rs index fb207181b3..6200a4f5d8 100644 --- a/module/core/format_tools/src/format.rs +++ b/module/core/format_tools/src/format.rs @@ -289,6 +289,7 @@ pub mod string; pub mod table; pub mod to_string; pub mod to_string_with_fallback; +pub mod text_wrap; /// A strucutre for diagnostic and demonstration purpose. #[ doc( hidden ) ] @@ -317,6 +318,7 @@ pub mod own table::orphan::*, to_string::orphan::*, to_string_with_fallback::orphan::*, + text_wrap::orphan::*, }; } @@ -369,6 +371,7 @@ pub mod exposed table::exposed::*, to_string::exposed::*, to_string_with_fallback::exposed::*, + text_wrap::exposed::*, }; } @@ -391,6 +394,7 @@ pub mod prelude table::prelude::*, to_string::prelude::*, to_string_with_fallback::prelude::*, + text_wrap::prelude::*, }; } diff --git a/module/core/format_tools/src/format/as_table.rs b/module/core/format_tools/src/format/as_table.rs index 7409b42952..d269556525 100644 --- a/module/core/format_tools/src/format/as_table.rs +++ b/module/core/format_tools/src/format/as_table.rs @@ -32,7 +32,7 @@ mod private ) where RowKey : table::RowKey, - Row : Cells< CellKey>, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr ; @@ -41,7 +41,7 @@ mod private AsTable< 'table, Table, RowKey, Row, CellKey> where RowKey : table::RowKey, - Row : Cells< CellKey>, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, { @@ -56,7 +56,7 @@ mod private for AsTable< 'table, Table, RowKey, Row, CellKey> where RowKey : table::RowKey, - Row : Cells< CellKey>, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, { @@ -70,7 +70,7 @@ mod private for AsTable< 'table, Table, RowKey, Row, CellKey> where RowKey : table::RowKey, - Row : Cells< CellKey>, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, { @@ -86,7 +86,7 @@ mod private for AsTable< 'table, Table, RowKey, Row, CellKey> where RowKey : table::RowKey, - Row : Cells< CellKey>, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, { @@ -101,7 +101,7 @@ mod private where Table : fmt::Debug, RowKey : table::RowKey, - Row : Cells< CellKey>, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, { @@ -146,7 +146,7 @@ mod private for AsTable< 'table, Table, RowKey, Row, CellKey> where RowKey : table::RowKey, - Row : Cells< CellKey>, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, Self : Copy, diff --git a/module/core/format_tools/src/format/output_format.rs b/module/core/format_tools/src/format/output_format.rs index 1b3e13c5b9..1bf58b75e6 100644 --- a/module/core/format_tools/src/format/output_format.rs +++ b/module/core/format_tools/src/format/output_format.rs @@ -32,6 +32,8 @@ mod private { + use std::borrow::Cow; + use crate::*; use print:: { @@ -78,6 +80,36 @@ mod private } } + /// Print table, which is constructed with vectors and `Cow`s, with the + /// specified output formatter. + /// + /// This function is useful when you do not want to use `AsTable`, or implement `Fields`, and + /// other traits, but you just have string slices in vectors. + /// + /// `rows` should not contain header of the table, it will be automatically added if `has_header` + /// is true. + pub fn vector_table_write< 'data, 'context > + ( + column_names : Vec< Cow< 'data, str > >, + has_header : bool, + rows : Vec< Vec< Cow< 'data, str > > >, + c : &mut Context< 'context >, + ) -> fmt::Result + { + InputExtract::extract_from_raw_table + ( + column_names, + has_header, + rows, + c.printer.filter_col, + c.printer.filter_row, + | x | + { + c.printer.output_format.extract_write( x, c ) + } + ) + } + } mod table; @@ -106,6 +138,7 @@ pub mod own #[ doc( inline ) ] pub use private:: { + vector_table_write, }; } diff --git a/module/core/format_tools/src/format/output_format/records.rs b/module/core/format_tools/src/format/output_format/records.rs index 45a1206e41..1c89f34038 100644 --- a/module/core/format_tools/src/format/output_format/records.rs +++ b/module/core/format_tools/src/format/output_format/records.rs @@ -22,12 +22,12 @@ //! use crate::*; -use md_math::MdOffset; use print:: { InputExtract, Context, }; +use std::borrow::Cow; use core:: { fmt, @@ -59,6 +59,8 @@ pub struct Records pub cell_postfix : String, /// Separator used between table columns. pub cell_separator : String, + /// Limit table width. If the value is zero, then no limitation. + pub max_width: usize, // /// Horizontal line character. // pub h : char, // /// Vertical line character. @@ -91,6 +93,25 @@ impl Records static INSTANCE : OnceLock< Records > = OnceLock::new(); INSTANCE.get_or_init( || Records::default() ) } + + /// Calculate how much space is minimally needed in order to generate an output with this output formatter. + /// It will be impossible to render tables smaller than the result of `min_width()`. + /// + /// This function is similar to `output_format::Table::min_width`, but it does not contain a + /// `column_count` as it always equal to 2, and it aslo uses the `output_format::Records` + /// style parameters. + pub fn min_width + ( + &self, + ) -> usize + { + // 2 is used here, because `Records` displays 2 columns: keys and values. + self.row_prefix.chars().count() + + self.row_postfix.chars().count() + + 2 * ( self.cell_postfix.chars().count() + self.cell_prefix.chars().count() ) + + self.cell_separator.chars().count() + + 2 + } } impl Default for Records @@ -108,6 +129,8 @@ impl Default for Records let table_postfix = "".to_string(); let table_separator = "\n".to_string(); + let max_width = 0; + // let h = '─'; // let v = '|'; // let t_l = '├'; @@ -131,6 +154,7 @@ impl Default for Records cell_prefix, cell_postfix, cell_separator, + max_width, // h, // v, // t_l, @@ -155,70 +179,88 @@ impl TableOutputFormat for Records c : & mut Context< 'buf >, ) -> fmt::Result { + use format::text_wrap::{ text_wrap, width_calculate }; + + if self.max_width != 0 && self.max_width < self.min_width() + { + return Err( fmt::Error ); + } + + // 2 because there are only 2 columns: key and value. + let columns_max_width = if self.max_width == 0 { 0 } else { self.max_width - self.min_width() + 2 }; - let label_width = x.header().fold( 0, | acc, cell | acc.max( cell.1[ 0 ] ) ); + let keys : Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > = x.header().collect(); + let keys_width = width_calculate( &keys ); write!( c.buf, "{}", self.table_prefix )?; - let mut first = true; - // Write each record - for ( irow, row ) in x.rows() - { + let mut printed_tables_count = 0; - if !row.vis + for ( itable_descriptor, table_descriptor ) in x.row_descriptors.iter().enumerate() + { + if !table_descriptor.vis || ( x.has_header && itable_descriptor == 0 ) { continue; } - if first - { - first = false; - } - else + if printed_tables_count > 0 { write!( c.buf, "{}", self.table_separator )?; } - let slice_width = x.data[ irow ].iter().fold( 0, | acc, cell | acc.max( cell.1[ 0 ] ) ); + printed_tables_count += 1; + + writeln!( c.buf, " = {}", table_descriptor.irow )?; - writeln!( c.buf, " = {}", irow )?; + let values = &x.data[ itable_descriptor ]; + let values_width = width_calculate( &values ); - for ( icol, _col ) in x.col_descriptors.iter().enumerate() + let table_for_wrapping : Vec< Vec< ( Cow< 'data, str >, [ usize; 2] ) > > = + keys.iter().enumerate().map( | ( ikey, key ) | { - let cell = &x.data[ irow ][ icol ]; - let height = cell.1[ 1 ]; + vec![ key.clone(), values[ ikey ].clone() ] + }).collect(); - for islice in 0..height + let wrapped_text = text_wrap + ( + table_for_wrapping.iter(), + &[ keys_width, values_width ], + columns_max_width, + keys_width + values_width, + ); + + for ( irow, cols ) in wrapped_text.data.into_iter().enumerate() + { + if irow != 0 { - let label = x.header_slice( islice, icol ); - let md_index = [ islice, icol, irow ]; - let slice = x.slices[ x.slices_dim.md_offset( md_index ) ]; - - if icol > 0 || islice > 0 - { - write!( c.buf, "{}", self.row_separator )?; - } - - write!( c.buf, "{}", self.row_prefix )?; - - write!( c.buf, "{}", self.cell_prefix )?; - write!( c.buf, "{: usize + { + self.row_prefix.chars().count() + + self.row_postfix.chars().count() + + column_count * ( self.cell_postfix.chars().count() + self.cell_prefix.chars().count() ) + + if column_count == 0 { 0 } else { ( column_count - 1 ) * self.cell_separator.chars().count() } + + column_count + } } impl TableOutputFormat for Table { fn extract_write< 'buf, 'data >( &self, x : &InputExtract< 'data >, c : &mut Context< 'buf > ) -> fmt::Result { - use md_math::MdOffset; + use format::text_wrap::text_wrap; let cell_prefix = &self.cell_prefix; let cell_postfix = &self.cell_postfix; @@ -173,103 +196,92 @@ impl TableOutputFormat for Table let row_separator = &self.row_separator; let h = self.h.to_string(); - let mut delimitting_header = self.delimitting_header; - let row_width = if delimitting_header + let column_count = x.col_descriptors.len(); + + if self.max_width != 0 && ( self.min_width( column_count ) > self.max_width ) { - let mut grid_width = x.mcells_vis[ 0 ] * ( cell_prefix.chars().count() + cell_postfix.chars().count() ); - grid_width += row_prefix.chars().count() + row_postfix.chars().count(); - if x.mcells_vis[ 0 ] > 0 - { - grid_width += ( x.mcells_vis[ 0 ] - 1 ) * ( cell_separator.chars().count() ); - } - x.mchars[ 0 ] + grid_width + return Err( fmt::Error ); } - else - { - 0 - }; - let mut prev_typ : Option< LineType > = None; - // dbg!( x.row_descriptors.len() ); - - for ( irow, row ) in x.row_descriptors.iter().enumerate() + let columns_nowrap_width = x.col_descriptors.iter().map( |c| c.width ).sum::(); + let visual_elements_width = self.min_width( column_count ) - column_count; + + let filtered_data = x.row_descriptors.iter().filter_map( | r | { - let height = row.height; - - if delimitting_header + if r.vis { - if let Some( prev_typ ) = prev_typ - { - if prev_typ == LineType::Header && row.typ == LineType::Regular - { - write!( c.buf, "{}", row_separator )?; - write!( c.buf, "{}", h.repeat( row_width ) )?; - delimitting_header = false - } - } - if row.vis - { - prev_typ = Some( row.typ ); - } + Some( &x.data[ r.irow ] ) } - - if !row.vis + else { - continue; + None + } + }); + + let wrapped_text = text_wrap + ( + filtered_data, + x.col_descriptors.iter().map( | c | c.width ).collect::< Vec< usize > >(), + if self.max_width == 0 { 0 } else { self.max_width - visual_elements_width }, + columns_nowrap_width + ); + + let new_columns_widthes = wrapped_text.column_widthes.iter().sum::(); + let new_row_width = new_columns_widthes + visual_elements_width; + + let mut printed_row_count = 0; + + for row in wrapped_text.data.iter() + { + if printed_row_count == wrapped_text.first_row_height && x.has_header && self.delimitting_header + { + write!( c.buf, "{}", row_separator )?; + write!( c.buf, "{}", h.repeat( new_row_width ) )?; + } + + if printed_row_count > 0 + { + write!( c.buf, "{}", row_separator )?; } - // dbg!( row.height ); + printed_row_count += 1; - for islice in 0..height - { + write!( c.buf, "{}", row_prefix )?; - if irow > 0 + for ( icol, col ) in row.iter().enumerate() + { + let cell_wrapped_width = col.wrap_width; + let column_width = wrapped_text.column_widthes[ icol ]; + let slice_width = col.content.chars().count(); + + if icol > 0 { - write!( c.buf, "{}", row_separator )?; + write!( c.buf, "{}", cell_separator )?; } - write!( c.buf, "{}", row_prefix )?; + write!( c.buf, "{}", cell_prefix )?; + + let lspaces = ( column_width - cell_wrapped_width ) / 2; + let rspaces = ( ( column_width - cell_wrapped_width ) as f32 / 2 as f32 ).round() as usize + cell_wrapped_width - slice_width; - for icol in 0 .. x.col_descriptors.len() + if lspaces > 0 { - let col = &x.col_descriptors[ icol ]; - let cell_width = x.data[ irow ][ icol ].1[0]; - let width = col.width; - let md_index = [ islice, icol, irow as usize ]; - let slice = x.slices[ x.slices_dim.md_offset( md_index ) ]; - - // println!( "md_index : {md_index:?} | md_offset : {} | slice : {slice}", x.slices_dim.md_offset( md_index ) ); - - if icol > 0 - { - write!( c.buf, "{}", cell_separator )?; - } - - write!( c.buf, "{}", cell_prefix )?; - - // println!( "icol : {icol} | irow : {irow} | width : {width} | cell_width : {cell_width}" ); - let lspaces = ( width - cell_width ) / 2; - let rspaces = ( width - cell_width + 1 ) / 2 + cell_width - slice.len(); - // println!( "icol : {icol} | irow : {irow} | width : {width} | cell_width : {cell_width} | lspaces : {lspaces} | rspaces : {rspaces}" ); - - if lspaces > 0 - { - write!( c.buf, "{: 0 - { - write!( c.buf, "{:>width$}", " ", width = rspaces )?; - } + write!( c.buf, "{: 0 + { + write!( c.buf, "{:>width$}", " ", width = rspaces )?; } - write!( c.buf, "{}", row_postfix )?; + write!( c.buf, "{}", cell_postfix )?; } + write!( c.buf, "{}", row_postfix )?; } Ok(()) } -} +} \ No newline at end of file diff --git a/module/core/format_tools/src/format/print.rs b/module/core/format_tools/src/format/print.rs index bc49db448d..f1aa104c24 100644 --- a/module/core/format_tools/src/format/print.rs +++ b/module/core/format_tools/src/format/print.rs @@ -7,10 +7,9 @@ mod private { use crate::*; - use md_math::MdOffset; use std:: { - borrow::Cow, + borrow::{ Cow, Borrow }, collections::HashMap, }; use core:: @@ -230,9 +229,13 @@ mod private #[ derive( Debug, Default ) ] pub struct RowDescriptor { + /// Index of the row. pub irow : usize, + /// Height of the row. pub height : usize, + /// Type of the line: header or regular. pub typ : LineType, + /// Visibility of the row. pub vis : bool, } @@ -241,8 +244,11 @@ mod private #[ derive( Debug, Default ) ] pub struct ColDescriptor< 'label > { + /// Index of the column. pub icol : usize, + /// Column width. pub width : usize, + /// Label of the column. pub label : &'label str, } @@ -282,11 +288,6 @@ mod private // string, size, pub data : Vec< Vec< ( Cow< 'data, str >, [ usize ; 2 ] ) > >, // xxx : use maybe flat vector - /// Dimensions of slices for retrieving data from multi-matrix. - pub slices_dim : [ usize ; 3 ], - /// Extracted slices or strings for further processing. - pub slices : Vec< &'data str >, - } // @@ -340,70 +341,122 @@ mod private /// Returns a slice from the header, or an empty string if no header is present. /// - /// This function retrieves a specific slice from the header row based on the provided indices. - /// If the table does not have a header, it returns an empty string. - /// /// # Arguments /// - /// - `islice`: The slice index within the header cell. /// - `icol`: The column index within the header row. /// /// # Returns /// - /// A string slice representing the header content at the specified indices. + /// A string slice representing the header content. /// - pub fn header_slice( & self, islice : usize, icol : usize ) -> & str + pub fn header_slice( & self, icol : usize ) -> & str { if self.has_header { - let md_index = [ islice, icol, 0 ]; - self.slices[ self.slices_dim.md_offset( md_index ) ] + self.data[ 0 ][ icol ].0.borrow() } else { "" } } + + /// Extract input data from and collect it in a format consumable by output formatter. - pub fn extract< 't, 'context, Table, RowKey, Row, CellKey> + pub fn extract< 'context, Table, RowKey, Row, CellKey> ( - table : &'t Table, + table : &'data Table, filter_col : &'context ( dyn FilterCol + 'context ), filter_row : &'context ( dyn FilterRow + 'context ), callback : impl for< 'a2 > FnOnce( &'a2 InputExtract< 'a2 > ) -> fmt::Result, ) -> fmt::Result where - 'data : 't, - // 't : 'data, Table : TableRows< RowKey = RowKey, Row = Row, CellKey = CellKey >, Table : TableHeader< CellKey = CellKey >, RowKey : table::RowKey, - Row : Cells< CellKey> + 'data, + Row : Cells< CellKey > + 'data, + Row : Cells< CellKey > + 'data, CellKey : table::CellKey + ?Sized + 'data, // CellRepr : table::CellRepr, { - use md_math::MdOffset; + let mut key_to_ikey : HashMap< Cow< 'data, str >, usize > = HashMap::new(); + let mut keys_count = 0; + + let rows = table.rows().map( | r | + { + let mut unsorted : Vec< ( usize, Cow< 'data, str > ) > = r.cells().map( | ( key, c ) | + { + if !key_to_ikey.contains_key( key.borrow() ) + { + key_to_ikey.insert( key.borrow().into(), keys_count ); + keys_count += 1; + } + + ( key_to_ikey[ key.borrow() ], c.unwrap_or( Cow::from( "" ) ) ) + } ).collect(); + + unsorted.sort_by( | ( i1, _ ), ( i2, _ ) | i1.cmp(i2) ); + + unsorted.into_iter().map( | ( _, c ) | c).collect() + } ).collect(); + + let has_header = table.header().is_some(); + + let column_names = match table.header() + { + Some( header ) => header.map( | ( k, _ ) | Cow::from( k.borrow() ) ).collect(), + None => match table.rows().next() + { + Some( r ) => r.cells().map( | ( k, _ ) | Cow::from( k.borrow() ) ).collect(), + None => Vec::new() + } + }; + + Self::extract_from_raw_table + ( + column_names, + has_header, + rows, + filter_col, + filter_row, + callback, + ) + } + + /// Extract input data from a table that is constructed with vectors and `Cow`s and collect + /// it in a format consumable by output formatter. + /// + /// `rows` should not contain header of the table, it will be automatically added if `has_header` + /// is true. + pub fn extract_from_raw_table< 'context > + ( + column_names : Vec< Cow< 'data, str > >, + has_header : bool, + rows : Vec< Vec< Cow< 'data, str > > >, + filter_col : &'context ( dyn FilterCol + 'context ), + filter_row : &'context ( dyn FilterRow + 'context ), + callback : impl for< 'a2 > FnOnce( &'a2 InputExtract< 'a2 > ) -> fmt::Result, + ) -> fmt::Result + { // let mcells = table.mcells(); let mut mcells_vis = [ 0 ; 2 ]; let mut mcells = [ 0 ; 2 ]; let mut mchars = [ 0 ; 2 ]; // key width, index - let mut key_to_ikey : HashMap< &'t CellKey, usize > = HashMap::new(); + let mut key_to_ikey : HashMap< Cow< 'data, str >, usize > = HashMap::new(); let mut col_descriptors : Vec< ColDescriptor< '_ > > = Vec::with_capacity( mcells[ 0 ] ); let mut row_descriptors : Vec< RowDescriptor > = Vec::with_capacity( mcells[ 1 ] ); - let mut has_header = false; - let mut data : Vec< Vec< ( Cow< 't, str >, [ usize ; 2 ] ) > > = Vec::new(); - let rows = table.rows(); + let mut data : Vec< Vec< ( Cow< 'data, str >, [ usize ; 2 ] ) > > = Vec::new(); let mut irow : usize = 0; let filter_col_need_args = filter_col.need_args(); // let filter_row_need_args = filter_row.need_args(); - let mut row_add = | row_iter : &'_ mut dyn _IteratorTrait< Item = ( &'t CellKey, Cow< 't, str > ) >, typ : LineType | + let mut row_add = | row_data : Vec< Cow< 'data, str > >, typ : LineType | { irow = row_descriptors.len(); @@ -413,18 +466,21 @@ mod private let mut ncol = 0; let mut ncol_vis = 0; - let fields : Vec< ( Cow< 't, str >, [ usize ; 2 ] ) > = row_iter + let fields : Vec< ( Cow< 'data, str >, [ usize ; 2 ] ) > = row_data + .into_iter() + .enumerate() .filter_map ( - | ( key, val ) | + | ( ikey, val ) | { + let key = &column_names[ ikey ]; let l = col_descriptors.len(); ncol += 1; if filter_col_need_args { - if !filter_col.filter_col( key.borrow() ) + if !filter_col.filter_col( key.as_ref() ) { return None; } @@ -442,7 +498,7 @@ mod private let sz = string::size( &val ); key_to_ikey - .entry( key ) + .entry( key.clone() ) .and_modify( | icol | { let col = &mut col_descriptors[ *icol ]; @@ -481,18 +537,9 @@ mod private // process header first - if let Some( header ) = table.header() + if has_header { - rows.len().checked_add( 1 ).expect( "Table has too many rows" ); - // assert!( header.len() <= usize::MAX, "Header of a table has too many cells" ); - has_header = true; - - let mut row2 = header.map( | ( key, title ) | - { - ( key, Cow::Borrowed( title ) ) - }); - - row_add( &mut row2, LineType::Header ); + row_add( column_names.clone(), LineType::Header ); } // Collect rows @@ -501,53 +548,14 @@ mod private { // assert!( row.cells().len() <= usize::MAX, "Row of a table has too many cells" ); - let mut row2 = row - .cells() - .map - ( - | ( key, val ) | - { - - let val = match val - { - Some( val ) => - { - val - } - None => - { - Cow::Borrowed( "" ) - } - }; - - return ( key, val ); - } - ); - - row_add( &mut row2, LineType::Regular ); + row_add( row, LineType::Regular ); } // calculate size in chars mchars[ 0 ] = col_descriptors.iter().fold( 0, | acc, col | acc + col.width ); mchars[ 1 ] = row_descriptors.iter().fold( 0, | acc, row | acc + if row.vis { row.height } else { 0 } ); - - // cook slices multi-matrix - - let mut slices_dim = [ 1, mcells[ 0 ], mcells[ 1 ] ]; - slices_dim[ 0 ] = row_descriptors - .iter() - .fold( 0, | acc : usize, row | acc.max( row.height ) ) - ; - - let slices_len = slices_dim[ 0 ] * slices_dim[ 1 ] * slices_dim[ 2 ]; - let slices : Vec< &str > = vec![ "" ; slices_len ]; - - // assert_eq!( mcells, mcells, r#"Incorrect multidimensional size of table - // mcells <> mcells - // {mcells:?} <> {mcells:?}"# ); - // println!( "mcells : {mcells:?} | mcells : {mcells:?} | mcells_vis : {mcells_vis:?}" ); - + let mut x = InputExtract::< '_ > { mcells, @@ -557,42 +565,16 @@ mod private row_descriptors, data, has_header, - slices_dim, - slices, }; - // extract slices - - let mut slices : Vec< &str > = vec![]; - std::mem::swap( &mut x.slices, &mut slices ); - - let mut irow : isize = -1; - for row_data in x.data.iter() + if x.data.len() > 0 { - - irow += 1; - for icol in 0 .. x.col_descriptors.len() { - let cell = &row_data[ icol ]; - string::lines( cell.0.as_ref() ) - .enumerate() - .for_each( | ( layer, s ) | - { - let md_index = [ layer, icol, irow as usize ]; - slices[ x.slices_dim.md_offset( md_index ) ] = s; - }) - ; - if irow == 0 - { - x.col_descriptors[ icol ].label = cell.0.as_ref(); - } + x.col_descriptors[ icol ].label = x.data[ 0 ][ icol ].0.as_ref(); } - } - std::mem::swap( &mut x.slices, &mut slices ); - return callback( &x ); } @@ -617,6 +599,8 @@ pub mod own Context, Printer, InputExtract, + RowDescriptor, + ColDescriptor, }; } @@ -658,4 +642,4 @@ pub mod prelude use super::*; } -// \ No newline at end of file +// diff --git a/module/core/format_tools/src/format/string.rs b/module/core/format_tools/src/format/string.rs index 619d1690c2..ee34e9e718 100644 --- a/module/core/format_tools/src/format/string.rs +++ b/module/core/format_tools/src/format/string.rs @@ -114,6 +114,47 @@ mod private Lines::new( src.as_ref() ) } + /// Returns an iterator over the lines of a string slice with text wrapping. + /// + /// This function provides an iterator that yields each line of the input string slice. + /// It is based on previous iterator `lines` but it also includes text wrapping that is + /// controlled via `limit_width` argument. If the string contains a trailing new line, + /// then an empty string will be yielded in this iterator. + /// + /// # Arguments + /// + /// * `src` - A reference to a type that can be converted to a string slice. This allows + /// for flexibility in passing various string-like types. + /// + /// * `limit_width` - text wrapping limit. Lines that are longer than this parameter will + // be split into smaller lines. + /// + /// # Returns + /// + /// An iterator of type `LinesWithLimit` that yields each line as a `&str`. + /// + /// # Examples + /// + /// ``` + /// let text = "Hello\nWorld\n"; + /// let mut lines = format_tools::string::lines_with_limit( text, 3 ); + /// assert_eq!( lines.next(), Some( "Hel" ) ); + /// assert_eq!( lines.next(), Some( "lo" ) ); + /// assert_eq!( lines.next(), Some( "Wor" ) ); + /// assert_eq!( lines.next(), Some( "ld" ) ); + /// assert_eq!( lines.next(), Some( "" ) ); + /// assert_eq!( lines.next(), None ); + /// ``` + pub fn lines_with_limit< S : AsRef< str > + ?Sized > + ( + src : & S, + limit_width : usize + ) + -> LinesWithLimit< '_ > + { + LinesWithLimit::new( src.as_ref(), limit_width ) + } + /// An iterator over the lines of a string slice. /// /// This struct implements the `Iterator` trait, allowing you to iterate over the lines @@ -128,6 +169,7 @@ mod private has_trailing_newline : bool, finished : bool, } + impl< 'a > Lines< 'a > { fn new( input : &'a str ) -> Self @@ -172,6 +214,70 @@ mod private } } + /// An iterator over the lines of a string slice with text wrapping. + /// + /// This struct implements the `Iterator` trait, allowing you to iterate over the parts + /// of a string. It uses `Lines` iterator and splits lines if they are longer that the + /// `limit_width` parameter. If the string contains a trailing new line, then an empty + /// string will be yielded in this iterator. + /// + /// If `limit_width` is equal to 0, then no wrapping is applied, and behaviour of this + /// iterator is equals to `Lines` iterator. + #[ derive( Debug ) ] + pub struct LinesWithLimit< 'a > + { + lines : Lines< 'a >, + limit_width : usize, + cur : Option< &'a str >, + } + + impl< 'a > LinesWithLimit< 'a > + { + fn new( input : &'a str, limit_width : usize ) -> Self + { + LinesWithLimit + { + lines : lines( input ), + limit_width, + cur : None, + } + } + } + + impl< 'a > Iterator for LinesWithLimit< 'a > + { + type Item = &'a str; + + fn next( &mut self ) -> Option< Self::Item > + { + if self.cur.is_none() || self.cur.is_some_and( str::is_empty ) + { + self.cur = self.lines.next(); + } + + match self.cur + { + None => return None, + + Some( cur ) => + { + if self.limit_width == 0 + { + self.cur = None; + Some( cur ) + } + else + { + let (chunk, rest) = cur.split_at(self.limit_width.min(cur.len())); + self.cur = Some( rest ); + + Some(chunk) + } + } + } + } + } + } #[ allow( unused_imports ) ] @@ -191,6 +297,8 @@ pub mod own size, lines, Lines, + lines_with_limit, + LinesWithLimit, }; } diff --git a/module/core/format_tools/src/format/table.rs b/module/core/format_tools/src/format/table.rs index 27e44ca0e5..1fab2ab744 100644 --- a/module/core/format_tools/src/format/table.rs +++ b/module/core/format_tools/src/format/table.rs @@ -12,7 +12,11 @@ mod private // fmt, borrow::Borrow, }; - use std::borrow::Cow; + use std:: + { + borrow::Cow, + collections::HashMap, + }; use reflect_tools:: { IteratorTrait, @@ -72,7 +76,7 @@ mod private // = - /// Marker trait to tag structures for whcih table trait deducing should be done from trait Fields, which is reflection. + /// Marker trait to tag structures for which table trait deducing should be done from trait Fields, which is reflection. pub trait TableWithFields {} // = @@ -92,6 +96,16 @@ mod private ; } + impl Cells< str > for HashMap< String, String > + { + fn cells< 'a, 'b >( &'a self ) -> impl IteratorTrait< Item = ( &'b str, Option< Cow< 'b, str > > ) > + where + 'a : 'b, + { + self.iter().map( | ( k, v ) | ( k.as_str(), Some( Cow::from( v ) ) ) ) + } + } + impl< Row, CellKey > Cells< CellKey > for Row where @@ -188,7 +202,7 @@ mod private > + 'k + 'v, RowKey : table::RowKey, - Row : TableWithFields + Cells< CellKey >, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, { @@ -264,7 +278,7 @@ mod private where Self : TableRows< RowKey = RowKey, Row = Row, CellKey = CellKey >, RowKey : table::RowKey, - Row : TableWithFields + Cells< CellKey >, + Row : Cells< CellKey >, CellKey : table::CellKey + ?Sized, // CellRepr : table::CellRepr, { diff --git a/module/core/format_tools/src/format/text_wrap.rs b/module/core/format_tools/src/format/text_wrap.rs new file mode 100644 index 0000000000..695ac287cd --- /dev/null +++ b/module/core/format_tools/src/format/text_wrap.rs @@ -0,0 +1,256 @@ +//! +//! Text wrapping function. +//! + +/// Define a private namespace for all its items. +mod private +{ + + use std::borrow::Cow; + + use crate::*; + + /// Struct that represents a wrapped tabular data. It is similar to `InputExtract`, + /// but we cannot use it as it does not wrap the text and it contains wrong column + /// widthes and heights (as they are dependent on wrapping too). + #[ derive( Debug ) ] + pub struct WrappedInputExtract< 'data > + { + /// Tabular data of rows and columns. + /// Note: these cells does not represent the actual information cells in the + /// original table. These cells are wrapped and used only for displaying. This also + /// means that one row in original table can be represented here with one or more + /// rows. + pub data: Vec< Vec< WrappedCell< 'data > > >, + + /// New widthes of columns that include wrapping. + pub column_widthes : Vec< usize >, + + /// Size of the first row of the table. + /// This parameter is used in case header of the table should be displayed. + pub first_row_height : usize, + } + + /// Struct that represents a content of a wrapped cell. + /// It contains the slice of the cell as well as its original width. + /// + /// Parameter `wrap_width` is needed as text in `output_format::Table` is centered. + /// However it is centered according to whole cell size and not the size of wrapped + /// text slice. + /// + /// Example that depicts the importance of `wrap_width` parameter: + /// + /// 1) | [ | 2) | [ | + /// | line1, | | line1, | + /// | line2 | | line2 | + /// | ] | | ] | + /// + /// The first case seems to be properly formatted, while the second case took centering + /// too literally. That is why `wrap_width` is introduced, and additional spaces to the + /// right side should be included by the output formatter. + #[ derive( Debug ) ] + pub struct WrappedCell< 'data > + { + /// Width of the cell. In calculations use this width instead of slice length in order + /// to properly center the text. See example in the doc string of the parent struct. + pub wrap_width : usize, + + /// Actual content of the cell. + pub content : Cow< 'data, str > + } + + /// Wrap table cells. + /// + /// `InputExtract` contains cells with full content, so it represents the logical + /// structure of the table. `WrappedInputExtract` wraps original cells to smaller + /// cells. The resulting data is more low-level and corresponds to the table that + /// will be actually printed to the console (or other output type). + /// + /// `InputExtract` is not directly passed to this function, as it made to be general. + /// Instead you pass table cells in `data` argument and pass a vector of column widthes + /// in `columns_width_list` generated by `InputExtract`. + /// + /// `columns_width_list` is a slice, this is more effective and general than just a `Vec`. + /// In table style, there could be many columns, but in records style there will be + /// always 2 columns - this number is known at compile time, so we can use a slice object. + /// + /// Notice: + /// 1. Data passed to this function should contain only visible rows and columns. + /// It does not perform additional filtering. + /// 2. `data` parameters is **vector of rows of columns** (like and ordinary table). + /// This means that in styles like `Records` where headers and rows turned into columns + /// You have to transpose your data before passing it to this function. + /// + /// Wrapping is controlled by `columns_max_width` and `columns_nowrap_width` parameters. + /// + /// - `columns_max_width` is the size that is allowed to be occupied by columns. + /// It equals to maximum table width minus lengthes of visual elements (prefixes, + /// postfixes, separators, etc.). + /// + /// - `columns_nowrap_width` is the sum of column widthes of cells without wrapping (basically, + /// the sum of widthes of column descriptors in `InputExtract`). + /// + /// The function will perform wrapping and shrink the columns so that they occupy not + /// more than `columns_max_width`. + /// + /// If `columns_max_width` is equal to 0, then no wrapping will be performed. + pub fn text_wrap< 'data > + ( + data : impl Iterator< Item = &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > >, + columns_width_list : impl AsRef< [ usize ] >, + columns_max_width : usize, + columns_nowrap_width : usize, + ) + -> WrappedInputExtract< 'data > + { + let columns_width_list = columns_width_list.as_ref(); + + let mut first_row_height = 0; + let mut new_data = Vec::new(); + let mut column_widthes = Vec::new(); + + if columns_max_width == 0 || columns_max_width >= columns_nowrap_width + { + column_widthes.extend( columns_width_list.iter() ); + } + else + { + let shrink_factor : f32 = ( columns_max_width as f32 ) / ( columns_nowrap_width as f32 ); + + for ( icol, col_width ) in columns_width_list.iter().enumerate() + { + let col_limit_float = ( *col_width as f32 ) * shrink_factor; + let col_limit = col_limit_float.floor() as usize; + + let col_width_to_put = if icol == columns_width_list.len() - 1 + { + columns_max_width - column_widthes.iter().sum::() + } + else + { + col_limit.max(1) + }; + + column_widthes.push( col_width_to_put ); + } + } + + for ( irow, row ) in data.enumerate() + { + let mut wrapped_rows : Vec< Vec< Cow< 'data, str > > > = vec![]; + + for ( icol, col ) in row.iter().enumerate() + { + let col_limit = column_widthes[ icol ]; + let wrapped_col = string::lines_with_limit( col.0.as_ref(), col_limit ).map( Cow::from ).collect(); + wrapped_rows.push( wrapped_col ); + } + + let max_rows = wrapped_rows.iter().map( Vec::len ).max().unwrap_or(0); + + let mut transposed : Vec< Vec< WrappedCell< 'data > > > = Vec::new(); + + if max_rows == 0 + { + transposed.push( vec![] ); + } + + for i in 0..max_rows + { + let mut row_vec : Vec< WrappedCell< 'data > > = Vec::new(); + + for col_lines in &wrapped_rows + { + if col_lines.len() > i + { + let wrap_width = col_lines.iter().map( |c| c.len() ).max().unwrap_or(0); + row_vec.push( WrappedCell { wrap_width , content : col_lines[ i ].clone() } ); + } + else + { + row_vec.push( WrappedCell { wrap_width : 0, content : Cow::from( "" ) } ); + } + } + + transposed.push( row_vec ); + } + + if irow == 0 + { + first_row_height += transposed.len(); + } + + new_data.extend( transposed ); + } + + WrappedInputExtract + { + data: new_data, + first_row_height, + column_widthes + } + } + + /// Calculate width of the column without wrapping. + pub fn width_calculate< 'data > + ( + column : &'data Vec< ( Cow< 'data, str >, [ usize; 2 ] ) > + ) + -> usize + { + column.iter().map( |k| + { + string::lines( k.0.as_ref() ).map( |l| l.chars().count() ).max().unwrap_or( 0 ) + } ).max().unwrap_or( 0 ) + } + +} + +#[ allow( unused_imports ) ] +pub use own::*; + +/// Own namespace of the module. +#[ allow( unused_imports ) ] +pub mod own +{ + use super::*; + #[ doc( inline ) ] + pub use orphan::*; + + #[ doc( inline ) ] + pub use + { + }; + + #[ doc( inline ) ] + pub use private:: + { + text_wrap, + width_calculate, + }; + +} + +/// Orphan namespace of the module. +#[ allow( unused_imports ) ] +pub mod orphan +{ + use super::*; + #[ doc( inline ) ] + pub use exposed::*; +} + +/// Exposed namespace of the module. +#[ allow( unused_imports ) ] +pub mod exposed +{ + use super::*; + pub use super::super::output_format; +} + +/// Prelude to use essentials: `use my_module::prelude::*`. +#[ allow( unused_imports ) ] +pub mod prelude +{ + use super::*; +} diff --git a/module/core/format_tools/tests/inc/collection_test.rs b/module/core/format_tools/tests/inc/collection_test.rs index 6b17ce0975..0d066004e2 100644 --- a/module/core/format_tools/tests/inc/collection_test.rs +++ b/module/core/format_tools/tests/inc/collection_test.rs @@ -401,3 +401,46 @@ fn llist_basic() } // qqq : xxx : implement for other containers + +#[ test ] +fn vec_of_hashmap() +{ + let data : Vec< HashMap< String, String > > = vec! + [ + { + let mut map = HashMap::new(); + map.insert( "id".to_string(), "1".to_string() ); + map.insert( "created_at".to_string(), "1627845583".to_string() ); + map + }, + { + let mut map = HashMap::new(); + map.insert( "id".to_string(), "2".to_string() ); + map.insert( "created_at".to_string(), "13".to_string() ); + map + }, + ]; + + use std::borrow::Cow; + + use the_module::TableFormatter; + + let _as_table : AsTable< '_, Vec< HashMap< String, String > >, &str, HashMap< String, String >, str> = AsTable::new( &data ); + let as_table = AsTable::new( &data ); + + let rows = TableRows::rows( &as_table ); + assert_eq!( rows.len(), 2 ); + + let mut output = String::new(); + let mut context = the_module::print::Context::new( &mut output, Default::default() ); + + let _got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + let got = as_table.table_to_string(); + + println!("{}", got); + + assert!( got.contains( "│ id │ created_at │" ) || got.contains( "│ created_at │ id │" ) ); + assert!( got.contains( "│ 1 │ 1627845583 │" ) || got.contains( "│ 1627845583 │ 1 │" ) ); + assert!( got.contains( "│ 2 │ 13 │" ) || got.contains( "│ 13 │ 2 │" ) ); +} \ No newline at end of file diff --git a/module/core/format_tools/tests/inc/format_records_test.rs b/module/core/format_tools/tests/inc/format_records_test.rs index 72f23a5ff5..77b8de7364 100644 --- a/module/core/format_tools/tests/inc/format_records_test.rs +++ b/module/core/format_tools/tests/inc/format_records_test.rs @@ -316,4 +316,136 @@ fn filter_row_callback() // -// xxx : enable \ No newline at end of file +// xxx : enable + +#[ test ] +fn test_width_limiting() +{ + use the_module::string; + + for width in min_width()..max_width() + { + println!("width: {}", width); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Records::default(); + format.max_width = width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + if line.starts_with(" = ") + { + continue; + } + + if line.chars().count() > width + { + println!("{}", output); + } + + assert!( line.chars().count() <= width ); + } + } +} + +#[ test ] +fn test_error_on_unsatisfiable_limit() +{ + // 0 is a special value that signifies no limit. + for width in 1..( min_width() ) + { + println!( "width: {}", width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Records::default(); + format.max_width = width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_err() ); + } +} + +#[ test ] +fn test_table_not_grows() +{ + use the_module::string; + + let expected_width = max_width(); + + // The upper bound was chosen arbitrarily. + for width in ( expected_width + 1 )..500 + { + println!( "width: {}", width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Records::default(); + format.max_width = width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + println!("{}", output); + + for line in string::lines( &output ) + { + if line.starts_with(" = ") + { + continue; + } + + assert!( line.chars().count() <= expected_width ); + } + } +} + +/// Utility function for calculating minimum table width with `test_objects_gen()` with +/// the default table style. +fn min_width() -> usize +{ + let format = output_format::Records::default(); + format.min_width() +} + +/// Utility function for calculating default table width with `test_objects_gen()` with +/// the default table style with table width limit equals to 0. +fn max_width() -> usize +{ + use the_module::string; + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let format = output_format::Records::default(); + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + assert!( got.is_ok() ); + + string::lines( &output ).map( |s| s.chars().count() ).max().unwrap_or(0) +} \ No newline at end of file diff --git a/module/core/format_tools/tests/inc/format_table_test.rs b/module/core/format_tools/tests/inc/format_table_test.rs index eb8a3b17dd..945696f572 100644 --- a/module/core/format_tools/tests/inc/format_table_test.rs +++ b/module/core/format_tools/tests/inc/format_table_test.rs @@ -326,3 +326,148 @@ fn filter_row_callback() // // xxx : implement test for vector of vectors + +// + +#[ test ] +fn no_subtract_with_overflow() +{ + let test_objects = test_object::test_objects_gen_with_unicode(); + + let format = output_format::Table::default(); + let printer = print::Printer::with_format( &format ); + + let as_table = AsTable::new( &test_objects ); + let mut output = String::new(); + let mut context = print::Context::new( &mut output, printer ); + + let result = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( result.is_ok() ); +} + +#[ test ] +fn test_width_limiting() +{ + use the_module::string; + + for max_width in min_width()..max_width() + { + println!("max_width: {}", max_width); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Table::default(); + format.max_width = max_width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + assert_eq!( max_width, line.chars().count() ); + } + } +} + +#[ test ] +fn test_error_on_unsatisfiable_limit() +{ + // 0 is a special value that signifies no limit. Therefore, the lower bound is 1. + for max_width in 1..( min_width() ) + { + println!( "max_width: {}", max_width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Table::default(); + format.max_width = max_width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_err() ); + } +} + +#[ test ] +fn test_table_not_grows() +{ + use the_module::string; + + let expected_width = max_width(); + + // The upper bound was chosen arbitrarily. + for max_width in ( expected_width + 1 )..500 + { + println!( "max_width: {}", max_width ); + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let mut format = output_format::Table::default(); + format.max_width = max_width; + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + assert_eq!( expected_width, line.chars().count() ); + } + } +} + +/// Utility function for calculating minimum table width with `test_objects_gen()` with +/// the default table style. +fn min_width() -> usize +{ + use the_module::Fields; + + let format = output_format::Table::default(); + let test_objects = test_object::test_objects_gen(); + let col_count = test_objects[0].fields().count(); + + format.min_width( col_count ) +} + +/// Utility function for calculating default table width with `test_objects_gen()` with +/// the default table style without any maximum width. +fn max_width() -> usize +{ + use the_module::string; + + let test_objects = test_object::test_objects_gen(); + let as_table = AsTable::new( &test_objects ); + + let format = output_format::Table::default(); + + let mut output = String::new(); + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( &mut output, printer ); + + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + assert!( got.is_ok() ); + + for line in string::lines( &output ) + { + return line.chars().count(); + } + + 0 +} \ No newline at end of file diff --git a/module/core/format_tools/tests/inc/print_test.rs b/module/core/format_tools/tests/inc/print_test.rs new file mode 100644 index 0000000000..dd45f73de8 --- /dev/null +++ b/module/core/format_tools/tests/inc/print_test.rs @@ -0,0 +1,191 @@ +#[ allow( unused_imports ) ] +use super::*; + +use the_module:: +{ + Fields, + IteratorTrait, + AsTable, + Cells, + TableSize, + TableRows, + TableHeader, + Context, + WithRef, + MaybeAs, +}; + +use std:: +{ + collections::HashMap, + borrow::Cow, +}; + +/// Struct representing a test object with various fields. +#[ derive( Clone, Debug ) ] +pub struct TestObject +{ + pub id : String, + pub created_at : i64, + pub file_ids : Vec< String >, + pub tools : Option< Vec< HashMap< String, String > > >, +} + +impl Fields< &'static str, MaybeAs< '_, str, WithRef > > +for TestObject +{ + type Value< 'v > = MaybeAs< 'v, str, WithRef >; + + fn fields( &self ) -> impl IteratorTrait< Item = ( &'static str, MaybeAs< '_, str, WithRef > ) > + { + // use format_tools::ref_or_display_or_debug_multiline::field; + use format_tools::ref_or_display_or_debug::field; + let mut dst : Vec< ( &'static str, MaybeAs< '_, str, WithRef > ) > = Vec::new(); + + dst.push( field!( &self.id ) ); + dst.push( field!( &self.created_at ) ); + dst.push( field!( &self.file_ids ) ); + + if let Some( tools ) = &self.tools + { + dst.push( field!( tools ) ); + } + else + { + dst.push( ( "tools", MaybeAs::none() ) ); + } + + dst.into_iter() + } +} + +// + +fn test_objects_gen() -> Vec< TestObject > +{ + + vec! + [ + TestObject + { + id : "1".to_string(), + created_at : 1627845583, + file_ids : vec![ "file1".to_string(), "file2".to_string() ], + tools : None + }, + TestObject + { + id : "2".to_string(), + created_at : 13, + file_ids : vec![ "file3".to_string(), "file4\nmore details".to_string() ], + tools : Some + ( + vec! + [ + { + let mut map = HashMap::new(); + map.insert( "tool1".to_string(), "value1".to_string() ); + map + }, + { + let mut map = HashMap::new(); + map.insert( "tool2".to_string(), "value2".to_string() ); + map + } + ] + ), + }, + ] + +} + +// + +#[ test ] +fn table_to_string() +// where + // for< 'a > AsTable< 'a, Vec< TestObject >, usize, TestObject, &'static str, String, &'static str > : TableFormatter< 'a >, +{ + use the_module::TableToString; + let test_objects = test_objects_gen(); + + let cells = Cells::< &'static str, WithRef >::cells( &test_objects[ 0 ] ); + assert_eq!( cells.len(), 4 ); + let cells = Cells::< &'static str, WithRef >::cells( &test_objects[ 1 ] ); + assert_eq!( cells.len(), 4 ); + drop( cells ); + + let as_table : AsTable< '_, Vec< TestObject >, usize, TestObject, &str, WithRef > = AsTable::new( &test_objects ); + let size = TableSize::mcells( &as_table ); + assert_eq!( size, [ 2, 4 ] ); + let rows = TableRows::rows( &as_table ); + assert_eq!( rows.len(), 2 ); + dbg!( rows.collect::< Vec< _ > >() ); + let header = TableHeader::header( &as_table ); + assert!( header.is_some() ); + let header = header.unwrap(); + assert_eq!( header.len(), 4 ); + assert_eq!( header.clone().collect::< Vec< _ > >(), vec! + [ + ( "id", Cow::Owned( "id".to_string() ) ), + ( "created_at", Cow::Owned( "created_at".to_string() ) ), + ( "file_ids", Cow::Owned( "file_ids".to_string() ) ), + ( "tools", Cow::Owned( "tools".to_string() ) ) + ]); + dbg!( header.collect::< Vec< _ > >() ); + + let mut output = String::new(); + let mut context = Context::new( &mut output, Default::default() ); + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + assert!( got.is_ok() ); + println!( "{}", &output ); + + // with explicit arguments + + let as_table : AsTable< '_, Vec< TestObject >, usize, TestObject, &str, WithRef > = AsTable::new( &test_objects ); + let table_string = as_table.table_to_string(); + assert!( table_string.contains( "id" ) ); + assert!( table_string.contains( "created_at" ) ); + assert!( table_string.contains( "file_ids" ) ); + assert!( table_string.contains( "tools" ) ); + + // without explicit arguments + + println!( "" ); + let as_table = AsTable::new( &test_objects ); + let table_string = as_table.table_to_string(); + assert!( table_string.contains( "id" ) ); + assert!( table_string.contains( "created_at" ) ); + assert!( table_string.contains( "file_ids" ) ); + assert!( table_string.contains( "tools" ) ); + println!( "{table_string}" ); + +} + +#[ test ] +fn custom_formatter() +{ + // use the_module::TableToString; + let test_objects = test_objects_gen(); + + let mut output = String::new(); + let mut formatter = the_module::Styles::default(); + formatter.cell_separator = " | ".into(); + formatter.row_prefix = "> ".into(); + formatter.row_postfix = " <".into(); + + let as_table = AsTable::new( &test_objects ); + let mut context = Context::new( &mut output, formatter ); + let got = the_module::TableFormatter::fmt( &as_table, &mut context ); + assert!( got.is_ok() ); + // let table_string = got.unwrap(); + + assert!( output.contains( "id" ) ); + assert!( output.contains( "created_at" ) ); + assert!( output.contains( "file_ids" ) ); + assert!( output.contains( "tools" ) ); + println!( "{output}" ); + +} + +// xxx diff --git a/module/core/format_tools/tests/inc/table_test.rs b/module/core/format_tools/tests/inc/table_test.rs index c5fd38a8e9..af57655085 100644 --- a/module/core/format_tools/tests/inc/table_test.rs +++ b/module/core/format_tools/tests/inc/table_test.rs @@ -298,3 +298,62 @@ fn iterator_over_strings() // assert!( got.contains( "│ 1627845583 │ [ │ │" ) ); } + +#[ test ] +fn test_vector_table() +{ + let column_names : Vec< Cow< 'static, str > > = vec![ + "id".into(), + "created_at".into(), + "file_ids".into(), + "tools".into(), + ]; + + let rows : Vec< Vec< Cow< 'static, str > > > = vec! + [ + vec! + [ + "1".into(), + "1627845583".into(), + "[ file1, file2 ]".into(), + "".into(), + ], + + vec! + [ + "2".into(), + "13".into(), + "[ file3, file4 ]".into(), + "[ tool1 ]".into(), + ], + ]; + + use the_module:: + { + output_format, + filter, + print, + }; + + let mut output = String::new(); + let mut context = print::Context::new( &mut output, Default::default() ); + + let res = output_format::vector_table_write + ( + column_names, + true, + rows, + &mut context, + ); + + assert!( res.is_ok() ); + + println!( "{}", output ); + + let exp = r#"│ id │ created_at │ file_ids │ tools │ +────────────────────────────────────────────────── +│ 1 │ 1627845583 │ [ file1, file2 ] │ │ +│ 2 │ 13 │ [ file3, file4 ] │ [ tool1 ] │"#; + + a_id!( output.as_str(), exp ); +} \ No newline at end of file diff --git a/module/core/format_tools/tests/inc/test_object.rs b/module/core/format_tools/tests/inc/test_object.rs index f710266a4d..70c702d035 100644 --- a/module/core/format_tools/tests/inc/test_object.rs +++ b/module/core/format_tools/tests/inc/test_object.rs @@ -200,3 +200,17 @@ pub fn test_objects_gen() -> Vec< TestObject > ] } + +pub fn test_objects_gen_with_unicode() -> Vec< TestObject > +{ + vec! + [ + TestObject + { + id : "Юнікод".to_string(), + created_at : 100, + file_ids : vec![], + tools : None, + } + ] +} \ No newline at end of file diff --git a/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs b/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs index bd9947cd71..e0c39527c3 100644 --- a/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs +++ b/module/core/format_tools/tests/inc/to_string_with_fallback_test.rs @@ -9,12 +9,12 @@ use the_module:: WithDebug, WithDisplay, // the_module::to_string_with_fallback::Ref, - to_string_with_fallback, + to_string_with_fallback }; use std:: { - // fmt, + fmt, // collections::HashMap, borrow::Cow, }; diff --git a/module/core/format_tools/tests/tests.rs b/module/core/format_tools/tests/tests.rs index 4fca6dbc07..c8e636300b 100644 --- a/module/core/format_tools/tests/tests.rs +++ b/module/core/format_tools/tests/tests.rs @@ -1,6 +1,6 @@ //! Primary tests. -#![ feature( trace_macros ) ] +// #![ feature( trace_macros ) ] #![ allow( unused_imports ) ] use format_tools as the_module; diff --git a/module/core/mem_tools/Cargo.toml b/module/core/mem_tools/Cargo.toml index cec41724d4..ba71b5759a 100644 --- a/module/core/mem_tools/Cargo.toml +++ b/module/core/mem_tools/Cargo.toml @@ -24,7 +24,6 @@ workspace = true features = [ "full" ] all-features = false - include = [ "/rust/impl/mem", "/Cargo.toml", diff --git a/module/core/mem_tools/src/lib.rs b/module/core/mem_tools/src/lib.rs index 03270e0a05..141da61a9d 100644 --- a/module/core/mem_tools/src/lib.rs +++ b/module/core/mem_tools/src/lib.rs @@ -37,7 +37,6 @@ pub mod own #[ doc( inline ) ] pub use orphan::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super::mem::orphan::*; } @@ -60,7 +59,6 @@ pub mod exposed #[ doc( inline ) ] pub use prelude::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super::mem::exposed::*; } @@ -71,6 +69,5 @@ pub mod prelude { use super::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super::mem::prelude::*; } diff --git a/module/core/mem_tools/src/mem.rs b/module/core/mem_tools/src/mem.rs index 8a540d97e2..3a48ddad8b 100644 --- a/module/core/mem_tools/src/mem.rs +++ b/module/core/mem_tools/src/mem.rs @@ -64,30 +64,28 @@ mod private } +#[ doc( inline ) ] +#[ allow( unused_imports ) ] +pub use own::*; + /// Own namespace of the module. #[ allow( unused_imports ) ] pub mod own { use super::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super:: { orphan::*, }; } -#[ doc( inline ) ] -#[ allow( unused_imports ) ] -pub use own::*; - /// Orphan namespace of the module. #[ allow( unused_imports ) ] pub mod orphan { use super::*; #[ doc( inline ) ] - #[ allow( unused_imports ) ] pub use super:: { exposed::*, @@ -103,6 +101,9 @@ pub mod orphan pub mod exposed { use super::*; + // Expose itself. + pub use super::super::mem; + #[ doc( inline ) ] pub use prelude::*; } diff --git a/module/core/test_tools/Cargo.toml b/module/core/test_tools/Cargo.toml index 01ee46f740..4f17161640 100644 --- a/module/core/test_tools/Cargo.toml +++ b/module/core/test_tools/Cargo.toml @@ -80,7 +80,7 @@ standalone = [ "standalone_collection_tools", "standalone_impls_index", "standalone_mem_tools", - "dep:mem_tools", + # "dep:mem_tools", "dep:typing_tools", "dep:diagnostics_tools", "dep:process_tools", @@ -112,8 +112,8 @@ rand = { workspace = true } error_tools = { workspace = true, features = [ "full" ], optional = true } collection_tools = { workspace = true, features = [ "full" ], optional = true } impls_index = { workspace = true, features = [ "full" ], optional = true } - mem_tools = { workspace = true, features = [ "full" ], optional = true } + typing_tools = { workspace = true, features = [ "full" ], optional = true } diagnostics_tools = { workspace = true, features = [ "full" ], optional = true } process_tools = { workspace = true, features = [ "full" ], optional = true } @@ -129,7 +129,6 @@ thiserror = { workspace = true, optional = true } hashbrown = { workspace = true, optional = true } # impls_index impls_index_meta = { workspace = true, optional = true } -# mem_tools [build-dependencies] rustc_version = "0.4" diff --git a/module/core/test_tools/src/lib.rs b/module/core/test_tools/src/lib.rs index cb0d390392..4d1b25124e 100644 --- a/module/core/test_tools/src/lib.rs +++ b/module/core/test_tools/src/lib.rs @@ -112,11 +112,16 @@ mod standalone pub mod collection; pub use collection as collection_tools; - /// impl index macroc. + /// impl and index macros. #[ path = "../../../../core/impls_index/src/impls_index/mod.rs" ] pub mod impls_index; + /// Memory tools. + #[ path = "../../../../core/mem_tools/src/mem.rs" ] + pub mod mem_tools; + } + #[ cfg( feature = "enabled" ) ] #[ cfg( feature = "standalone" ) ] pub use standalone::*; @@ -128,12 +133,12 @@ pub use :: error_tools, collection_tools, impls_index, + mem_tools, }; #[ cfg( feature = "enabled" ) ] pub use :: { - mem_tools, typing_tools, diagnostics_tools, process_tools, @@ -162,7 +167,7 @@ pub mod own { error_tools::orphan::*, collection_tools::orphan::*, - // meta_tools::orphan::*, + impls_index::orphan::*, mem_tools::orphan::*, typing_tools::orphan::*, diagnostics_tools::orphan::*, @@ -203,7 +208,7 @@ pub mod exposed { error_tools::exposed::*, collection_tools::exposed::*, - // meta_tools::exposed::*, + impls_index::exposed::*, mem_tools::exposed::*, typing_tools::exposed::*, diagnostics_tools::exposed::*, @@ -226,7 +231,7 @@ pub mod prelude { error_tools::prelude::*, collection_tools::prelude::*, - // meta_tools::prelude::*, + impls_index::prelude::*, mem_tools::prelude::*, typing_tools::prelude::*, diagnostics_tools::prelude::*, diff --git a/module/core/test_tools/tests/inc/basic_test.rs b/module/core/test_tools/tests/inc/impls_index_test.rs similarity index 100% rename from module/core/test_tools/tests/inc/basic_test.rs rename to module/core/test_tools/tests/inc/impls_index_test.rs diff --git a/module/core/test_tools/tests/inc/mem_test.rs b/module/core/test_tools/tests/inc/mem_test.rs new file mode 100644 index 0000000000..1cf4a2b724 --- /dev/null +++ b/module/core/test_tools/tests/inc/mem_test.rs @@ -0,0 +1,26 @@ +use super::*; + +// + +#[ allow( dead_code ) ] +#[ test ] +fn same_data() +{ + let buf = [ 0u8; 128 ]; + assert!( the_module::mem::same_data( &buf, &buf ) ); + + let x = [ 0u8; 1 ]; + let y = 0u8; + + assert!( the_module::mem::same_data( &x, &y ) ); + + assert!( !the_module::mem::same_data( &buf, &x ) ); + assert!( !the_module::mem::same_data( &buf, &y ) ); + + struct H1( &'static str ); + struct H2( &'static str ); + + assert!( the_module::mem::same_data( &H1( "hello" ), &H2( "hello" ) ) ); + assert!( !the_module::mem::same_data( &H1( "qwerty" ), &H2( "hello" ) ) ); + +} diff --git a/module/core/test_tools/tests/inc/mod.rs b/module/core/test_tools/tests/inc/mod.rs index bf3d2e3d78..a6d6581d75 100644 --- a/module/core/test_tools/tests/inc/mod.rs +++ b/module/core/test_tools/tests/inc/mod.rs @@ -1,8 +1,26 @@ -#[ allow( unused_imports ) ] use super::*; -mod basic_test; +mod impls_index_test; +mod mem_test; mod try_build_test; // mod wtest_utility; // qqq : include tests of all internal dependencies + +// /// Error tools. +// #[ path = "../../../../core/error_tools/tests/inc/mod.rs" ] +// pub mod error; +// pub use error as error_tools; +// +// /// Collection tools. +// #[ path = "../../../../core/collection_tools/tests/inc/mod.rs" ] +// pub mod collection; +// pub use collection as collection_tools; + +// /// impl and index macros. +// #[ path = "../../../../core/impls_index/tests/inc/mod.rs" ] +// pub mod impls_index; + +// /// Memory tools. +// #[ path = "../../../../core/mem_tools/tests/inc/mod.rs" ] +// pub mod mem_tools; diff --git a/module/core/test_tools/tests/tests.rs b/module/core/test_tools/tests/tests.rs index 3cdbd75627..c9b1261fb2 100644 --- a/module/core/test_tools/tests/tests.rs +++ b/module/core/test_tools/tests/tests.rs @@ -1,12 +1,13 @@ +#![ allow( unused_imports ) ] + // #![ deny( rust_2018_idioms ) ] // #![ deny( missing_debug_implementations ) ] // #![ deny( missing_docs ) ] -#[ allow( unused_imports ) ] use test_tools as the_module; -#[ allow( unused_imports ) ] -#[ cfg( feature = "enabled" ) ] -#[ cfg( not( feature = "no_std" ) ) ] -use test_tools::exposed::*; + +// #[ cfg( feature = "enabled" ) ] +// #[ cfg( not( feature = "no_std" ) ) ] +// use test_tools::exposed::*; mod inc; diff --git a/module/move/assistant/.key/readme.md b/module/move/assistant/.key/readme.md new file mode 100644 index 0000000000..4209c24678 --- /dev/null +++ b/module/move/assistant/.key/readme.md @@ -0,0 +1,20 @@ +# Keys + +This document provides a concise example of an environment configuration script, used to set up environment variables for a project. These variables configure application behavior without altering the code. + +## Example of `.key/-env.sh` + +```bash +# OpenAI API key. +OPENAI_API_KEY=sk-proj-ABCDEFG +``` + +## How to Use in Shell + +To apply these variables to your current shell session, use: + +```bash +. ./key/-env.sh +``` + +This command sources the script, making the variables available in your current session. Ensure `-env.sh` is in the `key` directory relative to your current location. \ No newline at end of file diff --git a/module/move/assistant/Cargo.toml b/module/move/assistant/Cargo.toml index 144cfb6557..50700f8c3c 100644 --- a/module/move/assistant/Cargo.toml +++ b/module/move/assistant/Cargo.toml @@ -15,6 +15,7 @@ Assist AI in writing code. """ categories = [ "algorithms", "development-tools" ] keywords = [ "fundamental", "general-purpose" ] +default-run = "main" [lints] workspace = true @@ -32,15 +33,31 @@ enabled = [ "reflect_tools/enabled", ] +[[bin]] +name = "main" +path = "src/bin/main.rs" + +[[bin]] +name = "list_resources" +path = "src/bin/list_resources.rs" + [dependencies] # xxx : qqq : optimze features mod_interface = { workspace = true, features = [ "full" ] } former = { workspace = true, features = [ "full" ] } format_tools = { workspace = true, features = [ "full" ] } reflect_tools = { workspace = true, features = [ "full" ] } -openai-api-rs = { version = "=5.0.11" } +openai-api-rs = { version = "=5.0.14" } tokio = { version = "1", features = ["full"] } dotenv = "0.15" +clap = { version = "4.5.20", features = ["derive"] } +pth = "0.21.0" +serde = { version = "1.0.213", features = ["derive"] } +serde_with = "3.11.0" +error_tools = "0.17.0" +derive_tools = { version = "0.32.0", features = ["full"] } +regex = { version = "1.10.3" } +serde_yaml = "0.9" [dev-dependencies] test_tools = { workspace = true } diff --git a/module/move/assistant/design/agents_design.md b/module/move/assistant/design/agents_design.md new file mode 100644 index 0000000000..a4f9901294 --- /dev/null +++ b/module/move/assistant/design/agents_design.md @@ -0,0 +1,37 @@ +# Agents + +## YAML description structure + +Please refer to `examples/` directory. + +## Paths + +- Used in node types, templates. +- Parts are delimited with `::`. +- Absolute path has a leading `::`. +- All paths (expect absolute) **are subject to absolutization**. Absolutization also depends on the context: in `next` fields paths are absolutized to `::nodes` dir, in templates - to `::output` and so on. + +## Execution + +- YAML file contains section about `nodes:`. +- Next node is encoded in `next:` field. +- Output of the nodes are stored in `::output` dir. + +## Builtin scenarios + +- `::scenario::entry` +- `::scenario::termination` + +## Node types + +- Input nodes: + - `trigger::stdin` + - `trigger::file` +- Processing nodes: + - `script` + - `agent::completion` +- Output nodes: + - `event::stdout` + - `event::file` + +Refer to examples in `examples/` to see fields of nodes. \ No newline at end of file diff --git a/module/move/assistant/design/agents_examples/sql.yaml b/module/move/assistant/design/agents_examples/sql.yaml new file mode 100644 index 0000000000..5149f07a29 --- /dev/null +++ b/module/move/assistant/design/agents_examples/sql.yaml @@ -0,0 +1,23 @@ +nodes: + - id: input + type: trigger::stdin + prompt: 'Your query: ' + next: node::translator + + - id: sql_generator_stage1 + type: agent::completion + system_message: 'Your task is to think about how to translate the user query in natural langauge in SQL langauge. Think step by steps.' + user_message: '{{input}}' + next: node::sql_generator_stage2 + + - id: sql_generator_stage2 + type: agent::completion + system_message: 'Your task to make an SQL code based on user query in natural language and the results of thinking on that query'. + user_message: '{{sql_generator_stage1}}' + agent_reuse: node::sql_generator_stage2 + next: node::output + + - id: output + type: event::stdout + output: '{{sql_generator_stage2}}' + next: scenario::termination \ No newline at end of file diff --git a/module/move/assistant/design/commands.md b/module/move/assistant/design/commands.md new file mode 100644 index 0000000000..d1c0410dba --- /dev/null +++ b/module/move/assistant/design/commands.md @@ -0,0 +1,73 @@ +# Commands + +## Legend + +- `<...>` - argument. +- `<..?>` - optional argument. +- `<...=...>` - argument with default value. +- `(...)+` - one or more times. + +## OpenAI + +### Files + +```shell +assistant openai files upload +assistant openai files list +assistant openai files retrieve +assistant openai files delete +assistant openai files retrieve-content +``` + +### Assistants + +```shell +assistant openai assistants create +assistant openai assistants list +assistant openai assistants retrieve +assistant openai assistants modify +assistant openai assistants delete +``` + +### Threads + +```shell +assistant openai threads create +assistant openai threads retrieve +assistant openai threads delete +``` + +### Messages + +```shell +assistant openai messages create +assistant openai messages list +assistant openai messages retrieve +assistant openai messages modify +assistant openai messages delete +``` + +### Chat + +```shell +assistant openai chat create-completion ( )+ +``` + +### Runs + +```shell +assistant openai runs create +assistant openai runs create-with-thread +assistant openai runs list +assistant openai runs retrieve +assistant openai runs cancel +``` + +## Anthropic + +### Messages + +```shell +assistant anthropic messages create ( )+ +``` + diff --git a/module/move/assistant/design/entities.mmd b/module/move/assistant/design/entities.mmd new file mode 100644 index 0000000000..53e9e6baa5 --- /dev/null +++ b/module/move/assistant/design/entities.mmd @@ -0,0 +1,78 @@ +--- +title: OpenAI API +--- +erDiagram + File { + string id PK + string object + integer bytes + integer created_at + string file_name + string purpose + } + + Assistant { + string id PK + string object + string model + integer created_at + string name + string description + string instructions + tool[] tools + metadata metadata + headers headers + } + + Thread { + string id PK + string object + integer created_at + object tool_resources + metadata metadata + } + + Message { + string id PK + string object + integer created_at + string thread_id FK + string status + object incomplete_details + integer completed_at + integer incomplete_at + string role + array content + string assistant_id FK + string run_id FK + array attachments + metadata metadata + } + + Run { + string id PK + string object + integer created_at + string thread_id FK + string assistant_id FK + string status + object required_action + object last_error + integer expires_at + integer started_at + integer cancelled_at + integer failed_at + integer completed_at + object incomplete_details + string model + string instructions + tool[] tools + metadata metadata + headers headers + } + + Assistant |o--o{ Run : "" + + Thread ||--o{ Message : "" + Thread ||--o{ Run: "" + diff --git a/module/move/assistant/design/entities.png b/module/move/assistant/design/entities.png new file mode 100644 index 0000000000..69906c0fea Binary files /dev/null and b/module/move/assistant/design/entities.png differ diff --git a/module/move/assistant/design/scenarios.md b/module/move/assistant/design/scenarios.md new file mode 100644 index 0000000000..a32543fbc9 --- /dev/null +++ b/module/move/assistant/design/scenarios.md @@ -0,0 +1,32 @@ +# Scenarios + +## OpenAI + +### Assistants + +#### Make new assistant + +```shell +assistant openai assistants create gpt-4o-mini CoolBot 'CoolBot is a helpful assistant.' 'You are a helpful assistant.' +``` + +This command will return assistant ID. + +#### Chat with the assistant + +To chat with OpenAI assistant, one should do this: + +1. Create a thread. Thread is like a chat. +2. Write a message in thread (e.g. a question). +3. Run the assistant in the thread. + +```shell +assistant openai threads create +``` + +This command will return the new thread ID (referred as `thread_id`). To call an assistant, you need to know its ID. + +```shell +assistant openai messages create user '2 + 2 = ?' +assistant openai runs create +``` diff --git a/module/move/assistant/src/actions.rs b/module/move/assistant/src/actions.rs new file mode 100644 index 0000000000..826bf1603e --- /dev/null +++ b/module/move/assistant/src/actions.rs @@ -0,0 +1,13 @@ +//! +//! CLI actions of the tool. +//! + +mod private {} + +crate::mod_interface! +{ + layer openai; + layer openai_assistants_list; + layer openai_files_list; + layer openai_runs_list; +} diff --git a/module/move/assistant/src/actions/openai.rs b/module/move/assistant/src/actions/openai.rs new file mode 100644 index 0000000000..da8be32030 --- /dev/null +++ b/module/move/assistant/src/actions/openai.rs @@ -0,0 +1,66 @@ +//! +//! OpenAI API actions. +//! +//! This module also contains the definition of OpenAI Error. +//! + +mod private +{ + + use error_tools::typed::Error; + use derive_tools::{ AsRefStr }; + + use crate::*; + use ser::DisplayFromStr; + + use commands::TableConfig; + + /// Collective enum for errors in OpenAI actions. + #[ ser::serde_as ] + #[ derive( Debug, Error, AsRefStr, ser::Serialize ) ] + #[ serde( tag = "type", content = "data" ) ] + pub enum Error + { + /// API error from the underlying implementation crate. + #[ error( "OpenAI API returned error:\n{0}" ) ] + ApiError + ( + #[ from ] + #[ serde_as( as = "DisplayFromStr" ) ] + openai_api_rs::v1::error::APIError + ), + + /// User chosen a mix of table styles instead of a single one. + /// E.g.: both `--as-table` and `--as-records` were set, however only one style must be chosen + #[ error( "Select only one table style: either `--as-table`, `--as-records`, or `--columns`" ) ] + WrongTableStyle, + } + + /// Shorthand for `Result` in OpenAI actions. + pub type Result< T > = core::result::Result< T, Error >; + + /// Check the CLI arguments for table style. + /// There are 3 arguments: `--as-table`, `--as-records`, `--columns`. Only one argument + /// should be active a time. + pub fn check_table_style( table_config: &TableConfig ) -> Result< () > + { + if table_config.as_table && ( table_config.as_records || table_config.columns ) + || table_config.as_records && ( table_config.as_table || table_config.columns ) + || table_config.columns && ( table_config.as_records || table_config.as_table ) + { + return Err( Error::WrongTableStyle ) + } + + Ok( () ) + } +} + +crate::mod_interface! +{ + own use + { + Error, + Result, + check_table_style, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/actions/openai_assistants_list.rs b/module/move/assistant/src/actions/openai_assistants_list.rs new file mode 100644 index 0000000000..2326ac2a6a --- /dev/null +++ b/module/move/assistant/src/actions/openai_assistants_list.rs @@ -0,0 +1,63 @@ +//! +//! List assistants in OpenAI API (action part). +//! + +mod private +{ + + use std::fmt; + + use format_tools::AsTable; + + use crate::*; + use client::Client; + + use debug::AssistantObjectWrap; + + use actions::openai::{ Result, check_table_style }; + + use commands::TableConfig; + use util::display_table::display_tabular_data; + + /// Report for `openai assistants list`. + #[ derive( Debug ) ] + pub struct ListReport + { + /// Configure table formatting. + pub table_config : TableConfig, + + /// OpenAI assistants. + pub assistants: Vec< AssistantObjectWrap > + } + + impl fmt::Display for ListReport + { + fn fmt + ( + &self, + f : &mut fmt::Formatter< '_ > + ) -> fmt::Result + { + display_tabular_data( &AsTable::new( &self.assistants ), f, &self.table_config ) + } + } + + /// List OpenAI assistants action. + pub async fn action + ( + client : &Client, + table_config : TableConfig, + ) -> Result < ListReport > + { + check_table_style( &table_config )?; + + let response = client.list_assistant( None, None, None, None ).await?; + let assistants = response.data.into_iter().map( AssistantObjectWrap ).collect(); + Ok( ListReport { table_config, assistants } ) + } +} + +crate::mod_interface! +{ + own use action; +} \ No newline at end of file diff --git a/module/move/assistant/src/actions/openai_files_list.rs b/module/move/assistant/src/actions/openai_files_list.rs new file mode 100644 index 0000000000..12f6ab7bd0 --- /dev/null +++ b/module/move/assistant/src/actions/openai_files_list.rs @@ -0,0 +1,64 @@ +//! +//! List files in OpenAI API (action part). +//! + +mod private +{ + + use std::fmt; + + use format_tools::AsTable; + + use crate::*; + use client::Client; + + use debug::FileDataWrap; + + use actions::openai::{ Result, check_table_style }; + + use commands::TableConfig; + use util::display_table::display_tabular_data; + + /// Report for `openai files list`. + #[ derive( Debug ) ] + pub struct ListReport + { + /// Configure table formatting. + pub table_config : TableConfig, + + /// Files in OpenAI. + pub files : Vec< FileDataWrap > + } + + impl fmt::Display for ListReport + { + fn fmt + ( + &self, + f : &mut fmt::Formatter< '_ > + ) -> fmt::Result + { + display_tabular_data( &AsTable::new( &self.files ), f, &self.table_config ) + } + } + + /// List OpenAI files action. + pub async fn action + ( + client : &Client, + table_config : TableConfig, + ) -> Result < ListReport > + { + check_table_style( &table_config )?; + + let response = client.file_list().await?; + let files = response.data.into_iter().map( FileDataWrap ).collect(); + Ok( ListReport { table_config, files } ) + } + +} + +crate::mod_interface! +{ + own use action; +} \ No newline at end of file diff --git a/module/move/assistant/src/actions/openai_runs_list.rs b/module/move/assistant/src/actions/openai_runs_list.rs new file mode 100644 index 0000000000..d5bf8098c6 --- /dev/null +++ b/module/move/assistant/src/actions/openai_runs_list.rs @@ -0,0 +1,65 @@ +//! +//! List runs in OpenAI API (action part). +//! + +mod private +{ + + use std::fmt; + + use format_tools::AsTable; + + use crate::*; + use client::Client; + + use debug::RunObjectWrap; + + use actions::openai::{ Result, check_table_style }; + + use commands::TableConfig; + use util::display_table::display_tabular_data; + + /// Report for `openai runs list`. + #[ derive( Debug ) ] + pub struct ListReport + { + /// Configure table formatting. + pub table_config : TableConfig, + + /// Current OpenAI runs. + pub runs : Vec< RunObjectWrap >, + } + + impl fmt::Display for ListReport + { + fn fmt + ( + &self, + f : &mut fmt::Formatter< '_ > + ) -> fmt::Result + { + display_tabular_data( &AsTable::new( &self.runs ), f, &self.table_config ) + } + } + + /// List OpenAI runs action. + pub async fn action + ( + client : &Client, + thread_id : String, + table_config : TableConfig, + ) -> Result < ListReport > + { + check_table_style( &table_config )?; + + let response = client.list_run( thread_id, None, None, None, None ).await?; + let runs = response.data.into_iter().map( RunObjectWrap ).collect(); + Ok( ListReport { table_config, runs } ) + } + +} + +crate::mod_interface! +{ + own use action; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents.rs b/module/move/assistant/src/agents.rs new file mode 100644 index 0000000000..0f04d8a61c --- /dev/null +++ b/module/move/assistant/src/agents.rs @@ -0,0 +1,16 @@ +//! +//! Main module for agents framework. +//! + +mod private {} + +crate::mod_interface! +{ + + layer path; + layer context; + layer scenario_raw; + layer scenario_raw_processors; + layer scenario_processed; + +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/context.rs b/module/move/assistant/src/agents/context.rs new file mode 100644 index 0000000000..a18a535780 --- /dev/null +++ b/module/move/assistant/src/agents/context.rs @@ -0,0 +1,150 @@ +//! +//! Context representation. Can be used as compile-time context and as a runtime-context. +//! +//! Represents a simplistic "filesystem" with directories and terminal objects. +//! + +mod private +{ + use std::collections::HashMap; + + use crate::*; + use agents::path:: + { + Path, + PATH_SEPARATOR, + }; + + /// Represents a directory in a simplistic in-memory "filesystem" + /// with other directories and terminal objects. + /// + /// `T` is the type of terminal object. + #[ derive( Debug, PartialEq, Clone, Default ) ] + pub struct ContextDir< T > + { + /// Internal map of entry names and entries data (a directory or a terminal object). + map : HashMap< String, ContextEntry< T > >, + } + + impl< T > ContextDir< T > + { + /// Create an empty `ContextDir`. + pub fn new() -> Self + { + Self + { + map : HashMap::new() + } + } + + /// Add new entry to the directory. + /// + /// Returns `true` if entry was successfully added. + /// Returns `false` if there is already and entry with such name. + /// Old entry will not be overriden. + pub fn add( &mut self, name : impl Into< String >, entry : ContextEntry< T > ) -> bool + { + let name = name.into(); + + if self.map.contains_key( name.as_str() ) + { + false + } + else + { + self.map.insert( name, entry ); + true + } + } + + /// Get an entry by its name. Returns `None` is there is no such entry. + /// + /// `name` must be a valid path item. Refer to `path::PATH_ITEM_REGEX_STR` for syntax. + /// + /// This method is useful for quickly getting an entry only by its name. + /// For complex paths, where your object is located in several consecutives directories, + /// you can use `Path` type and use method `ContextDir::get_by_path`. + pub fn get( &self, name : impl AsRef< str > ) -> Option< &ContextEntry< T > > + { + self.map.get( name.as_ref() ) + } + + /// Get an entry by its path. Returns `None` is there is no such entry. + /// + /// This function accepts both relative and absolute paths and it will + /// treat itself as the root. + pub fn get_by_path( &self, path : &Path ) -> Option< &ContextEntry< T > > + { + let mut cur : Option< &ContextEntry< T > > = None; + + for component in path.components() + { + if component == PATH_SEPARATOR + { + continue; + } + + match cur + { + None => + { + cur = self.get( component ); + }, + + Some( entry ) => + { + match entry + { + ContextEntry::Terminal( _ ) => + { + return None; + }, + + ContextEntry::Dir( dir ) => + { + cur = dir.get( component ); + } + } + } + } + + if cur.is_none() + { + return None; + } + } + + cur + } + } + + /// Entry in a simplistic in-memory "filesystem": either a directory or a terminal object `T`. + /// + /// Notice, this struct does not store the name of the entry. + #[ derive( Debug, PartialEq, Clone ) ] + pub enum ContextEntry< T > + { + /// Directory in context. + Dir( ContextDir< T > ), + + /// Terminal object. + Terminal( T ), + } + + impl< T > Into< ContextEntry< T > > for ContextDir< T > + { + fn into( self ) -> ContextEntry< T > + { + ContextEntry::Dir( self ) + } + } +} + +crate::mod_interface! +{ + own use + { + ContextDir, + ContextEntry, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/path.rs b/module/move/assistant/src/agents/path.rs new file mode 100644 index 0000000000..d0c29a15dd --- /dev/null +++ b/module/move/assistant/src/agents/path.rs @@ -0,0 +1,241 @@ +//! +//! Paths in agents graph. +//! + +mod private +{ + use std:: + { + io, + fmt, + ops::Deref, + sync::LazyLock, + }; + + use serde:: + { + Serialize, + Deserialize, + }; + + use regex::Regex; + + /// Path separator string. + pub const PATH_SEPARATOR : &str = "::"; + + /// Regular expression for `Path` items. Represented in `&str`. + /// It is not anchored to start and end of the string. + /// + /// If you want to match against this expression, use `PATH_ITEM_REGEX`. + pub const PATH_ITEM_REGEX_STR : &str = r"[a-zA-Z0-9_ -]+"; + + /// Regular expression for `Path`. You can match whole `&str` with this type. + pub static PATH_REGEX : LazyLock< Regex > = LazyLock::new( || + { + let regex = format! + ( + r"^({sep})?({item}({sep}{item})*({sep})?)?$", + sep = PATH_SEPARATOR, + item = PATH_ITEM_REGEX_STR, + ); + + Regex::new( ®ex ).unwrap() + }); + + /// New type for paths in agents graph. Use `TryFrom` implementation + /// to create `Path`s. + /// + /// Paths resemble filesystem path, path separator is `::`. + /// Absolute path starts with `::`. + #[ derive( Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize ) ] + pub struct Path( String ); + + impl Path + { + /// Returns the parent directory, if it exists. + /// + /// Returns `None` if the `Path` terminates in a root or if it's the empty string. + #[ inline ] + pub fn parent( &self ) -> Option< Path > + { + /// Find parent of a `Path`. + /// + /// This method uses `&str` as an argument instead of `Path` + /// in order to be more general and handle trailing `::` case. + fn find_parent( s : &str ) -> Option< &str > + { + s.rfind( PATH_SEPARATOR ) + .map( | sep_pos | + { + if sep_pos == 0 + { + // We found root. We should not return string before `::`, + // as it will be empty. + Some( PATH_SEPARATOR ) + } + else if sep_pos == s.len() - PATH_SEPARATOR.len() + { + // We found trailing `::`. We should continue looking for last separator. + find_parent( &s[ .. sep_pos ] ) + } + else + { + Some( &s[ .. sep_pos ] ) + } + }) + .flatten() + } + + find_parent( self.0.as_str() ) + .map( | s | Self( s.to_string() ) ) + } + + /// Returns whether the `Path` is relative (does not start with `::`). + pub fn is_relative( &self ) -> bool + { + !self.is_absolute() + } + + /// Returns whether the `Path` is absolute (starts with `::`). + pub fn is_absolute( &self ) -> bool + { + self.0.starts_with( PATH_SEPARATOR ) + } + + /// Creates an owned `Path` by joining a given path to `self`. + /// + /// If path is joined with an absolute path, then this absolute + /// path will be returned. + #[ inline ] + pub fn join( &self, path : &Path ) -> Self + { + if path.is_absolute() + { + path.clone() + } + else + { + if self.0.ends_with( PATH_SEPARATOR ) + { + Self( format!( "{}{}", self.0, path.0 ) ) + } + else + { + Self( format!( "{}::{}", self.0, path.0 ) ) + } + } + } + + /// Checks if the `Path` starts with a given base path. + #[ inline ] + pub fn starts_with( &self, base : &Path ) -> bool + { + self.0.starts_with( base.0.as_str() ) + } + + /// Returns the inner `String`. + #[ inline( always ) ] + pub fn inner( self ) -> String + { + self.0 + } + + /// Iterate over components of a `Path`. If the `Path` is absolute, then the first + /// element will be `::`. + pub fn components( &self ) -> impl Iterator< Item = &str > + { + self.0.split( PATH_SEPARATOR ).map( | c | + { + if c.is_empty() + { + PATH_SEPARATOR + } + else + { + c + } + }) + } + } + + impl fmt::Display for Path + { + #[ inline ] + fn fmt( &self, f : &mut fmt::Formatter<'_> ) -> fmt::Result + { + write!( f, "{}", self.0 ) + } + } + + impl TryFrom< String > for Path + { + type Error = io::Error; + + fn try_from( src : String ) -> Result< Self, Self::Error > + { + if PATH_REGEX.is_match( src.as_str() ) + { + Ok( Self ( src ) ) + } + else + { + Err( io::Error::from( io::ErrorKind::InvalidData ) ) + } + } + } + + impl TryFrom< &str > for Path + { + type Error = io::Error; + + fn try_from( src : &str ) -> Result< Self, Self::Error > + { + if PATH_REGEX.is_match( src ) + { + Ok( Self ( src.to_string() ) ) + } + else + { + Err( io::Error::from( io::ErrorKind::InvalidData ) ) + } + } + } + + impl AsRef< str > for Path + { + #[ inline ] + fn as_ref( &self ) -> &str + { + self.0.as_ref() + } + } + + impl Into< String > for Path + { + #[ inline ] + fn into( self ) -> String + { + self.0 + } + } + + impl Deref for Path + { + type Target = str; + + #[ inline ] + fn deref( &self ) -> &Self::Target + { + &self.0 + } + } +} + +crate::mod_interface! +{ + own use + { + Path, + PATH_SEPARATOR, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_processed.rs b/module/move/assistant/src/agents/scenario_processed.rs new file mode 100644 index 0000000000..b43e9c0075 --- /dev/null +++ b/module/move/assistant/src/agents/scenario_processed.rs @@ -0,0 +1,99 @@ +//! +//! Scenario representation. Stores parsed representation of templates and paths. +//! This is the type used for running scenarios. +//! +//! For a more simplistic representation use `ScenarioRaw`. +//! + +mod private +{ + use std::collections::HashMap; + + use crate::*; + use agents:: + { + path::Path, + scenario_raw:: + { + ScenarioRaw, + NodeRaw, + }, + }; + + /// New type for templates in scenarios. + #[ derive( Debug, PartialEq ) ] + pub struct TemplateBody( pub String ); + + /// Struct that represents user written scenarios. + /// + /// This is a processed form of a scenario, paths are distinguished here with types. + /// For more simplistic representation of scenarios, use `ScenarioRaw` type. + #[ derive( Debug, PartialEq ) ] + pub struct ScenarioProcessed + { + /// Nodes in the scenario. + pub nodes: Vec< Node >, + } + + impl TryFrom< ScenarioRaw > for ScenarioProcessed + { + type Error = std::io::Error; + + fn try_from( scenario_raw : ScenarioRaw ) -> Result< Self, Self::Error > + { + let nodes : Result< Vec< Node >, Self::Error > = + scenario_raw.nodes.into_iter().map( | rn | Node::try_from( rn ) ).collect(); + + Ok( Self { nodes : nodes? } ) + } + } + + /// Node representation in a scenario file. + /// + /// This is a processed form of a node, paths are distinguished here with types. + /// For more simplistic representation of scenarios, use `NodeRaw` type. + #[ derive( Debug, PartialEq ) ] + pub struct Node + { + /// ID of the node. Must be unique, will also identify node output. + pub id : String, + + /// Type of the node. + pub r#type : Path, + + /// Parameters of the node. + pub params : HashMap< String, String >, + + /// ID of the next node to execute. + pub next : Path, + } + + impl TryFrom< NodeRaw > for Node + { + type Error = std::io::Error; + + fn try_from( node_raw : NodeRaw ) -> Result< Self, Self::Error > + { + Ok + ( + Self + { + id : node_raw.id, + r#type : Path::try_from( node_raw.r#type )?, + params : node_raw.params, + next : Path::try_from( node_raw.next )?, + } + ) + } + } +} + +crate::mod_interface! +{ + own use + { + TemplateBody, + ScenarioProcessed, + Node, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw.rs b/module/move/assistant/src/agents/scenario_raw.rs new file mode 100644 index 0000000000..8aef1e2250 --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw.rs @@ -0,0 +1,71 @@ +//! +//! Raw scenario representation. Captures only the basic syntax of scenario file. +//! +//! For more detailed representation, use `ScenarioProcessed`. +//! + +mod private +{ + use std:: + { + io, + collections::HashMap, + }; + + use former::Former; + use serde:: + { + Serialize, + Deserialize, + }; + + /// Struct that represents user written scenarios. + /// + /// This is a raw form of a scenario, only the general structure is captured there. + /// For more detailed representation of scenarios, use `ScenarioProcessed` type. + #[ derive( Debug, Serialize, Deserialize, Former, PartialEq ) ] + pub struct ScenarioRaw + { + /// Nodes in the scenario. + pub nodes: Vec< NodeRaw >, + } + + impl ScenarioRaw + { + /// Read scenario file in YAML format. + pub fn read( reader : impl io::Read ) -> Result< Self, serde_yaml::Error > + { + serde_yaml::from_reader( reader ) + } + } + + /// Node representation in a scenario file. + /// + /// This is a raw form of a node, only the general structure is captured there. + /// For more detailed representation of scenarios, use `Node` type. + #[ derive( Debug, Serialize, Deserialize, Former, PartialEq ) ] + pub struct NodeRaw + { + /// ID of the node. Must be unique, will also identify node output. + pub id : String, + + /// Type of the node. Represented as a path. + pub r#type : String, + + /// Rest of the key-value pairs in the node that are specific to node types. + #[ serde( flatten ) ] + pub params : HashMap< String, String >, + + /// ID of the next node to execute. Represented as a path. + pub next : String, + } +} + +crate::mod_interface! +{ + own use + { + ScenarioRaw, + NodeRaw, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw_processors.rs b/module/move/assistant/src/agents/scenario_raw_processors.rs new file mode 100644 index 0000000000..4e9ebb7798 --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw_processors.rs @@ -0,0 +1,13 @@ +//! +//! `ScenarioRaw` processors: functions that work with `ScenarioRaw`. +//! +//! Currently only formatters are implemented. +//! + +mod private {} + +crate::mod_interface! +{ + layer yaml_formatter; + layer plantuml_formatter; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw_processors/plantuml_formatter.rs b/module/move/assistant/src/agents/scenario_raw_processors/plantuml_formatter.rs new file mode 100644 index 0000000000..8f1114fe2d --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw_processors/plantuml_formatter.rs @@ -0,0 +1,76 @@ +//! +//! Format scenario in PlantUML diagram. +//! + +mod private +{ + use std::io; + + use crate::*; + use agents::scenario_raw::ScenarioRaw; + + /// Format scenario in PlantUML diagram. + pub fn plantuml_formatter + ( + scenario : &ScenarioRaw, + writer : &mut impl io::Write, + ) -> Result< (), io::Error > + { + writer.write( b"@startuml\n" )?; + + for node in &scenario.nodes + { + writer.write( b"json " )?; + writer.write( node.id.as_bytes() )?; + writer.write( b" {\n" )?; + + writer.write( b" \"type\": \"" )?; + writer.write( node.r#type.as_bytes() )?; + writer.write( b"\"" )?; + + if node.params.len() > 0 + { + writer.write( b"," )?; + } + + writer.write( b"\n" )?; + + for ( i, ( key, value ) ) in node.params.iter().enumerate() + { + writer.write( b" \"" )?; + writer.write( key.as_bytes() )?; + writer.write( b"\": \"" )?; + writer.write( value.as_bytes() )?; + writer.write( b"\"" )?; + + if i != node.params.len() - 1 + { + writer.write( b"," )?; + } + + writer.write( b"\n" )?; + } + + writer.write( b"}\n" )?; + } + + writer.write( b"json ::scenario::termination {\n" )?; + writer.write( b"}\n" )?; + + for node in &scenario.nodes + { + writer.write( node.id.as_bytes() )?; + writer.write( b" --> " )?; + writer.write( node.next.as_bytes() )?; + writer.write( b" : next\n" )?; + } + + writer.write( b"@enduml" )?; + Ok( () ) + } +} + +crate::mod_interface! +{ + own use plantuml_formatter; +} \ No newline at end of file diff --git a/module/move/assistant/src/agents/scenario_raw_processors/yaml_formatter.rs b/module/move/assistant/src/agents/scenario_raw_processors/yaml_formatter.rs new file mode 100644 index 0000000000..05d1bb5668 --- /dev/null +++ b/module/move/assistant/src/agents/scenario_raw_processors/yaml_formatter.rs @@ -0,0 +1,26 @@ +//! +//! Format scenario in YAML format (pretty-printing). +//! + +mod private +{ + use std::io; + + use crate::*; + use agents::scenario_raw::ScenarioRaw; + + /// Pretty-print `ScenarioRaw` in YAML format. + pub fn yaml_formatter + ( + scenario : &ScenarioRaw, + writer : &mut impl io::Write, + ) -> Result< (), serde_yaml::Error > + { + serde_yaml::to_writer( writer, scenario ) + } +} + +crate::mod_interface! +{ + own use yaml_formatter; +} \ No newline at end of file diff --git a/module/move/assistant/src/main.rs b/module/move/assistant/src/bin/list_resources.rs similarity index 93% rename from module/move/assistant/src/main.rs rename to module/move/assistant/src/bin/list_resources.rs index d8a93d1956..d85d524ceb 100644 --- a/module/move/assistant/src/main.rs +++ b/module/move/assistant/src/bin/list_resources.rs @@ -19,7 +19,8 @@ use dotenv::dotenv; use assistant:: { - client, + client::client, + Secret }; #[ tokio::main ] @@ -27,7 +28,9 @@ async fn main() -> Result< (), Box< dyn Error > > { dotenv().ok(); - let client = client()?; + let secret = Secret::load()?; + + let client = client( &secret )?; let response = client.file_list().await?; // println!( "Files: {:?}", response.data ); @@ -49,4 +52,4 @@ async fn main() -> Result< (), Box< dyn Error > > ); Ok( () ) -} +} \ No newline at end of file diff --git a/module/move/assistant/src/bin/main.rs b/module/move/assistant/src/bin/main.rs new file mode 100644 index 0000000000..419030d03b --- /dev/null +++ b/module/move/assistant/src/bin/main.rs @@ -0,0 +1,42 @@ +#![ doc( html_logo_url = "https://raw.githubusercontent.com/Wandalen/wTools/master/asset/img/logo_v3_trans_square.png" ) ] +#![ doc( html_favicon_url = "https://raw.githubusercontent.com/Wandalen/wTools/alpha/asset/img/logo_v3_trans_square_icon_small_v2.ico" ) ] +#![ doc( html_root_url = "https://docs.rs/assistant/latest/assistant/" ) ] +#![ doc = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/", "Readme.md" ) ) ] + +use std:: +{ + env, + error::Error, +}; + +use dotenv::dotenv; +use clap::Parser; + +use assistant:: +{ + client::client, + commands::{ Cli, CliCommand, self }, + Secret +}; + +#[ tokio::main ] +async fn main() -> Result< (), Box< dyn Error > > +{ + dotenv().ok(); + + let secret = Secret::load()?; + + let client = client( &secret )?; + + let cli = Cli::parse(); + + match cli.command + { + CliCommand::OpenAi( openai_command ) => + { + commands::openai::command( &client, openai_command ).await; + } + } + + Ok( () ) +} diff --git a/module/move/assistant/src/client.rs b/module/move/assistant/src/client.rs index 3276edc7a9..1c9fd0bbee 100644 --- a/module/move/assistant/src/client.rs +++ b/module/move/assistant/src/client.rs @@ -6,36 +6,24 @@ mod private { - pub use openai_api_rs::v1:: { api::OpenAIClient as Client, - // api::Client, assistant::AssistantObject, }; use std:: { - env, error::Error, }; - use former::Former; - - /// Options for configuring the OpenAI API client. - #[ derive( Former, Debug ) ] - pub struct ClientOptions - { - /// The API key for authenticating with the OpenAI API. - pub api_key : Option< String >, - } + use crate::*; + use secret::Secret; - /// Creates a new OpenAI API client using the API key from the environment variable `OPENAI_API_KEY`. - pub fn client() -> Result< Client, Box< dyn Error > > + /// Creates a new OpenAI API client using the secrets. + pub fn client( secrets : &Secret ) -> Result< Client, Box< dyn Error > > { - let api_key = env::var( "OPENAI_API_KEY" )?; - println!( "api_key : {}", api_key ); - Ok( Client::new( api_key ) ) + Ok( Client::new( secrets.OPENAI_API_KEY.clone() ) ) } } @@ -45,8 +33,7 @@ crate::mod_interface! exposed use { Client, - ClientOptions, AssistantObject, - client, + client }; } diff --git a/module/move/assistant/src/commands.rs b/module/move/assistant/src/commands.rs new file mode 100644 index 0000000000..480b13d8d5 --- /dev/null +++ b/module/move/assistant/src/commands.rs @@ -0,0 +1,77 @@ +//! +//! CLI commands of the tool. +//! + +/// Internal namespace. +mod private +{ + + use clap::{ Parser, Subcommand }; + + use crate::*; + use commands::openai; + + /// CLI commands of the tool. + #[ derive ( Debug, Parser ) ] + pub struct Cli + { + /// Root of the CLI commands. + #[ command ( subcommand ) ] + pub command : CliCommand, + } + + /// Root of the CLI commands. + #[ derive ( Debug, Subcommand ) ] + pub enum CliCommand + { + /// OpenAI API commands. + #[ command ( subcommand, name = "openai" ) ] + OpenAi( openai::Command ), + } + + const DEFAULT_MAX_TABLE_WIDTH : usize = 130; + + /// Common collection of arguments for formatting tabular data. + #[ derive( Debug, Parser ) ] + pub struct TableConfig + { + /// Limit table widht. + #[ arg( long, default_value_t = DEFAULT_MAX_TABLE_WIDTH ) ] + pub max_table_width : usize, + + /// Show tabular data as an ordinary table. + #[ arg( long ) ] + pub as_table : bool, + + /// Show each record of a tabular data as a separate table. + #[ arg( long ) ] + pub as_records : bool, + + /// Show only keys (columns) of tabular data. + #[ arg( long ) ] + pub columns : bool, + + /// Filter columns of tabular data. + #[ arg( long, value_delimiter( ',' ) ) ] + pub filter_columns : Vec< String >, + } + +} + +crate::mod_interface! +{ + layer openai; + layer openai_assistants; + layer openai_assistants_list; + layer openai_runs; + layer openai_runs_list; + layer openai_files; + layer openai_files_list; + + own use + { + Cli, + CliCommand, + TableConfig, + }; +} diff --git a/module/move/assistant/src/commands/openai.rs b/module/move/assistant/src/commands/openai.rs new file mode 100644 index 0000000000..42c7ea5595 --- /dev/null +++ b/module/move/assistant/src/commands/openai.rs @@ -0,0 +1,75 @@ +//! +//! Collection of OpenAI API commands. +//! + +mod private +{ + + use clap::Subcommand; + + use crate::*; + use client::Client; + use commands::{ openai_assistants, openai_files, openai_runs }; + + /// OpenAI API commands. + #[ derive ( Debug, Subcommand ) ] + pub enum Command + { + /// OpenAI assistants. + #[ command ( subcommand ) ] + Assistants + ( + openai_assistants::Command + ), + + /// OpenAI files. + #[ command ( subcommand ) ] + Files + ( + openai_files::Command + ), + + /// OpenAI runs. + #[ command ( subcommand ) ] + Runs + ( + openai_runs::Command + ), + } + + /// Execute OpenAI command. + pub async fn command + ( + client : &Client, + command : Command, + ) + { + match command + { + Command::Assistants( assistants_command ) => + { + openai_assistants::command( client, assistants_command ).await; + } + + Command::Files( files_command ) => + { + openai_files::command( client, files_command ).await; + } + + Command::Runs( runs_command ) => + { + openai_runs::command( client, runs_command ).await; + } + } + } + +} + +crate::mod_interface! +{ + own use + { + Command, + command, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/commands/openai_assistants.rs b/module/move/assistant/src/commands/openai_assistants.rs new file mode 100644 index 0000000000..0d941c94ba --- /dev/null +++ b/module/move/assistant/src/commands/openai_assistants.rs @@ -0,0 +1,52 @@ +//! +//! Collection of assistants commands for OpenAI API. +//! + +mod private +{ + + use clap::Subcommand; + + use crate::*; + use client::Client; + use commands::{ openai_assistants_list, TableConfig }; + + /// OpenAI assistants. + #[ derive ( Debug, Subcommand ) ] + pub enum Command + { + /// List OpenAI assistants. + List + { + /// Configure table formatting. + #[ clap( flatten ) ] + table_config : TableConfig, + }, + } + + /// Execute OpenAI command related to assistants. + pub async fn command + ( + client : &Client, + command : Command, + ) + { + match command + { + Command::List{ table_config } => + { + openai_assistants_list::command( client, table_config ).await; + } + } + } + +} + +crate::mod_interface! +{ + own use + { + Command, + command, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/commands/openai_assistants_list.rs b/module/move/assistant/src/commands/openai_assistants_list.rs new file mode 100644 index 0000000000..6ce7a80ac4 --- /dev/null +++ b/module/move/assistant/src/commands/openai_assistants_list.rs @@ -0,0 +1,34 @@ +//! +//! List assistants in OpenAI API (command part). +//! + +mod private +{ + + use crate::*; + use client::Client; + use actions; + use commands::TableConfig; + + /// List OpenAI assistants command. + pub async fn command + ( + client : &Client, + table_config : TableConfig, + ) + { + let result = actions::openai_assistants_list::action( client, table_config ).await; + + match result + { + Ok ( report ) => println!( "{}", report ), + Err ( error ) => println!( "{}", error ) + } + } + +} + +crate::mod_interface! +{ + own use command; +} \ No newline at end of file diff --git a/module/move/assistant/src/commands/openai_files.rs b/module/move/assistant/src/commands/openai_files.rs new file mode 100644 index 0000000000..ea72a42ad1 --- /dev/null +++ b/module/move/assistant/src/commands/openai_files.rs @@ -0,0 +1,52 @@ +//! +//! Collection of files commands for OpenAI API. +//! + +mod private +{ + + use clap::Subcommand; + + use crate::*; + use client::Client; + use commands::{ openai_files_list, TableConfig }; + + /// OpenAI files. + #[ derive ( Debug, Subcommand ) ] + pub enum Command + { + /// List OpenAI files. + List + { + /// Configure table formatting. + #[ clap( flatten ) ] + table_config : TableConfig, + }, + } + + /// Execute OpenAI commands related to files. + pub async fn command + ( + client : &Client, + command : Command, + ) + { + match command + { + Command::List{ table_config } => + { + openai_files_list::command( client, table_config ).await; + } + } + } + +} + +crate::mod_interface! +{ + own use + { + Command, + command, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/commands/openai_files_list.rs b/module/move/assistant/src/commands/openai_files_list.rs new file mode 100644 index 0000000000..6225b9faf2 --- /dev/null +++ b/module/move/assistant/src/commands/openai_files_list.rs @@ -0,0 +1,34 @@ +//! +//! List files in OpenAI API (command part). +//! + +mod private +{ + + use crate::*; + use client::Client; + use actions; + use commands::TableConfig; + + /// List files in your OpenAI API. + pub async fn command + ( + client : &Client, + table_config : TableConfig, + ) + { + let result = actions::openai_files_list::action( client, table_config ).await; + + match result + { + Ok ( report ) => println!( "{}", report ), + Err ( error ) => println!( "{}", error ) + } + } + +} + +crate::mod_interface! +{ + own use command; +} \ No newline at end of file diff --git a/module/move/assistant/src/commands/openai_runs.rs b/module/move/assistant/src/commands/openai_runs.rs new file mode 100644 index 0000000000..2cf7812000 --- /dev/null +++ b/module/move/assistant/src/commands/openai_runs.rs @@ -0,0 +1,55 @@ +//! +//! Collection of runs commands for OpenAI API. +//! + +mod private +{ + + use clap::Subcommand; + + use crate::*; + use client::Client; + use commands::{ openai_runs_list, TableConfig }; + + /// OpenAI runs. + #[ derive ( Debug, Subcommand ) ] + pub enum Command + { + /// List OpenAI runs in a thread. + List + { + /// Thread ID. + thread_id : String, + + /// Configure table formatting. + #[ clap( flatten ) ] + table_config : TableConfig, + } + } + + /// Execute OpenAI commands related to runs. + pub async fn command + ( + client : &Client, + command : Command, + ) + { + match command + { + Command::List { thread_id, table_config } => + { + openai_runs_list::command( client, thread_id, table_config ).await; + } + } + } + +} + +crate::mod_interface! +{ + own use + { + Command, + command, + }; +} \ No newline at end of file diff --git a/module/move/assistant/src/commands/openai_runs_list.rs b/module/move/assistant/src/commands/openai_runs_list.rs new file mode 100644 index 0000000000..6d08d64ed3 --- /dev/null +++ b/module/move/assistant/src/commands/openai_runs_list.rs @@ -0,0 +1,35 @@ +//! +//! List runs in OpenAI API (command part). +//! + +mod private +{ + + use crate::*; + use client::Client; + use actions; + use commands::TableConfig; + + /// List runs in the thread in OpenAI API. + pub async fn command + ( + client : &Client, + thread_id : String, + table_config : TableConfig, + ) + { + let result = actions::openai_runs_list::action( client, thread_id, table_config ).await; + + match result + { + Ok ( report ) => println!( "{}", report ), + Err ( error ) => println!( "{}", error ) + } + } + +} + +crate::mod_interface! +{ + own use command; +} \ No newline at end of file diff --git a/module/move/assistant/src/debug.rs b/module/move/assistant/src/debug.rs index d35c02699a..294333abf0 100644 --- a/module/move/assistant/src/debug.rs +++ b/module/move/assistant/src/debug.rs @@ -17,6 +17,7 @@ use std::borrow::Cow; mod assistant_object; mod file_data; +mod run_object; crate::mod_interface! { @@ -24,5 +25,6 @@ crate::mod_interface! { assistant_object::AssistantObjectWrap, file_data::FileDataWrap, + run_object::RunObjectWrap, }; } diff --git a/module/move/assistant/src/debug/assistant_object.rs b/module/move/assistant/src/debug/assistant_object.rs index 1535245f67..9ebcead56e 100644 --- a/module/move/assistant/src/debug/assistant_object.rs +++ b/module/move/assistant/src/debug/assistant_object.rs @@ -5,6 +5,7 @@ use openai_api_rs::v1::assistant; #[ derive( Debug ) ] pub struct AssistantObjectWrap( pub assistant::AssistantObject ); +/// Manually implemented `Clone`, as `FileData` does not implement it. impl Clone for AssistantObjectWrap { fn clone( &self ) -> Self diff --git a/module/move/assistant/src/debug/file_data.rs b/module/move/assistant/src/debug/file_data.rs index ca1fb242d4..b8029949c7 100644 --- a/module/move/assistant/src/debug/file_data.rs +++ b/module/move/assistant/src/debug/file_data.rs @@ -8,6 +8,7 @@ use openai_api_rs::v1::file::FileData; #[ derive( Debug ) ] pub struct FileDataWrap( pub FileData ); +/// Manually implemented `Clone`, as `FileData` does not implement it. impl Clone for FileDataWrap { fn clone( &self ) -> Self diff --git a/module/move/assistant/src/debug/run_object.rs b/module/move/assistant/src/debug/run_object.rs new file mode 100644 index 0000000000..efe2ce1e02 --- /dev/null +++ b/module/move/assistant/src/debug/run_object.rs @@ -0,0 +1,73 @@ + +use super::*; +use openai_api_rs::v1::run::RunObject; + +// Assuming the `format_tools` module and `field!` macro are defined elsewhere + +/// A wrapper for `RunObject` to make pretty print. +#[ derive( Debug ) ] +pub struct RunObjectWrap( pub RunObject ); + +/// Manually implemented `Clone`, as `RunObject` does not implement it. +impl Clone for RunObjectWrap { + fn clone(&self) -> Self { + RunObjectWrap(RunObject { + id : self.0.id.clone(), + object : self.0.object.clone(), + created_at : self.0.created_at, + thread_id : self.0.thread_id.clone(), + assistant_id : self.0.assistant_id.clone(), + status : self.0.status.clone(), + required_action : self.0.required_action.clone(), + last_error : self.0.last_error.clone(), + expires_at : self.0.expires_at, + started_at : self.0.started_at, + cancelled_at : self.0.cancelled_at, + failed_at : self.0.failed_at, + completed_at : self.0.completed_at, + model : self.0.model.clone(), + instructions : self.0.instructions.clone(), + tools : self.0.tools.clone(), + metadata : self.0.metadata.clone(), + headers : self.0.headers.clone(), + }) + } +} + +impl TableWithFields for RunObjectWrap {} +impl Fields< &'_ str, Option< Cow< '_, str > > > +for RunObjectWrap +{ + type Key< 'k > = &'k str; + type Val< 'v > = Option< Cow< 'v, str > >; + + fn fields( &self ) -> impl format_tools::IteratorTrait< Item = ( &'_ str, Option< Cow< '_, str > > ) > + { + use format_tools::ref_or_display_or_debug_multiline::field; + let mut dst = Vec::new(); + + dst.push( field!( &self.0.id ) ); + dst.push( field!( &self.0.object ) ); + dst.push( ( "created_at", Some( Cow::Owned( self.0.created_at.to_string() ) ) ) ); + dst.push( field!( &self.0.thread_id ) ); + dst.push( field!( &self.0.assistant_id ) ); + dst.push( field!( &self.0.status ) ); + + dst.push( ( "required_action", self.0.required_action.as_ref().map( |ra| Cow::Owned( format!( "{:?}", ra ) ) ) ) ); + dst.push( ( "last_error", self.0.last_error.as_ref().map( |le| Cow::Owned( format!( "{:?}", le ) ) ) ) ); + dst.push( ( "expires_at", self.0.expires_at.map( |ea| Cow::Owned( ea.to_string() ) ) ) ); + dst.push( ( "started_at", self.0.started_at.map( |sa| Cow::Owned( sa.to_string() ) ) ) ); + dst.push( ( "cancelled_at", self.0.cancelled_at.map( |ca| Cow::Owned( ca.to_string() ) ) ) ); + dst.push( ( "failed_at", self.0.failed_at.map( |fa| Cow::Owned( fa.to_string() ) ) ) ); + dst.push( ( "completed_at", self.0.completed_at.map( |ca| Cow::Owned( ca.to_string() ) ) ) ); + + dst.push( field!( &self.0.model ) ); + dst.push( ( "instructions", self.0.instructions.as_ref().map( |i| Cow::Owned( i.clone() ) ) ) ); + + dst.push( ( "tools", Some( Cow::Owned( format!( "{:?}", self.0.tools ) ) ) ) ); + dst.push( ( "metadata", Some( Cow::Owned( format!( "{:?}", self.0.metadata ) ) ) ) ); + dst.push( ( "headers", self.0.headers.as_ref().map( |h| Cow::Owned( format!( "{:?}", h ) ) ) ) ); + + dst.into_iter() + } +} diff --git a/module/move/assistant/src/lib.rs b/module/move/assistant/src/lib.rs index dd2ca047ba..4d21799cc5 100644 --- a/module/move/assistant/src/lib.rs +++ b/module/move/assistant/src/lib.rs @@ -4,12 +4,24 @@ #![ doc = include_str!( concat!( env!( "CARGO_MANIFEST_DIR" ), "/", "Readme.md" ) ) ] use mod_interface::mod_interface; +use error_tools::thiserror; /// Define a private namespace for all its items. mod private { } +/// Serde-related exports. +pub mod ser +{ + pub use serde:: + { + Serialize, + Deserialize, + }; + pub use serde_with::*; +} + // pub mod client; crate::mod_interface! @@ -17,6 +29,11 @@ crate::mod_interface! layer client; layer debug; + layer commands; + layer actions; + layer secret; + layer util; + layer agents; exposed use ::reflect_tools:: { diff --git a/module/move/assistant/src/secret.rs b/module/move/assistant/src/secret.rs new file mode 100644 index 0000000000..aa90da77bc --- /dev/null +++ b/module/move/assistant/src/secret.rs @@ -0,0 +1,219 @@ +//! +//! Tool's secrets. +//! + +/// Internal namespace. +mod private +{ + use crate::*; + use std:: + { + env, + sync::OnceLock, + }; + + use error_tools::typed::Error; + use ser::DisplayFromStr; + + /// Typed secret error. + #[ ser::serde_as ] + #[ derive( Debug, Error, ser::Serialize ) ] + #[ serde( tag = "type", content = "data" ) ] + pub enum Error + { + + /// Secret file is illformed. + #[ error( "Secret file is illformed\n{0}" ) ] + SecretFileIllformed + ( + #[ from ] + #[ serde_as( as = "DisplayFromStr" ) ] + dotenv::Error + ), + + /// Some variable in the secrets is missing. + #[ error( "Secret misssing the variable {0}" ) ] + VariableMissing( &'static str ), + + /// Some variable in the secrets is illformed. + #[ error( "Secret error processing the variable {0}\n{1}" ) ] + VariableIllformed( &'static str, String ), + + } + + /// Result type for `Secret` methods. + pub type Result< R > = core::result::Result< R, Error >; + + /// Represents the application secrets loaded from environment variables. + #[ derive( Debug ) ] + #[ allow( non_snake_case ) ] + pub struct Secret + { + /// OpenAI API key. + pub OPENAI_API_KEY : String, + } + + impl Secret + { + + /// Loads secrets from environment variables. + /// + /// # Returns + /// + /// * `Result< Self >` - On success, returns a `Secret` instance with values from environment variables. + /// * On failure, returns an error indicating which environment variable is missing or invalid. + #[ allow( non_snake_case ) ] + pub fn load() -> Result< Self > + { + let path = "./.key/-env.sh"; + + // Attempt to load environment variables from the specified file + let r = dotenv::from_filename( path ); + if let Err( ref err ) = r + { + // Only return an error if it's not an Io error, and include the file path in the error message + if !matches!( err, dotenv::Error::Io( _ ) ) + { + return Err( r.expect_err( &format!( "Failed to load {path}" ) ).into() ); + } + } + + let config = Self + { + OPENAI_API_KEY : var( "OPENAI_API_KEY", None )?, + }; + Ok( config ) + } + + /// Reads the secrets, panicking with an explanation if loading fails. + /// + /// # Returns + /// + /// * `Secret` - The loaded secrets. + /// + /// # Panics + /// + /// * Panics with a detailed explanation if the secrets cannot be loaded. + + pub fn read() -> Secret + { + Self::load().unwrap_or_else( | err | + { + let example = include_str!( "../.key/readme.md" ); + let explanation = format! + ( +r#" = Lack of secrets + +Failed to load secret or some its parameters. +{err} + + = Fix + +Either define missing environment variable or make sure `./.key/-env.toml` file has it defined. + + = More information + +{example} +"# + ); + panic!( "{}", explanation ); + }) + } + + /// Retrieves a static reference to the secrets, initializing it if necessary. + /// + /// # Returns + /// + /// * `&'static Secret` - A static reference to the secrets. + /// + /// # Warning + /// + /// * Do not use this function unless absolutely necessary. + /// * Avoid using it in `lib.rs`. + pub fn get() -> &'static Secret + { + static INSTANCE : OnceLock< Secret > = OnceLock::new(); + INSTANCE.get_or_init( || Self::read() ) + } + + } + + /// Retrieves the value of an environment variable as a `String`. + /// + /// This function attempts to fetch the value of the specified environment variable. + /// If the variable is not set, it returns a provided default value if available, or an error if not. + /// + /// # Arguments + /// + /// * `name` - The name of the environment variable to retrieve. + /// * `default` - An optional default value to return if the environment variable is not set. + /// + /// # Returns + /// + /// * `Result` - On success, returns the value of the environment variable or the default value. + /// * On failure, returns an error indicating the missing environment variable. + fn var + ( + name : &'static str, + default : Option< &'static str >, + ) -> Result< String > + { + match env::var( name ) + { + Ok( value ) => Ok( value ), + Err( _ ) => + { + if let Some( default_value ) = default + { + Ok( default_value.to_string() ) + } + else + { + Err( Error::VariableMissing( name ) ) + } + } + } + } + + /// Retrieves the value of an environment variable as an `AbsolutePath`. + /// + /// This function attempts to fetch the value of the specified environment variable and convert it into an `AbsolutePath`. + /// If the variable is not set, it returns a provided default value if available, or an error if not. + /// + /// # Arguments + /// + /// * `name` - The name of the environment variable to retrieve. + /// * `default` - An optional default value to return if the environment variable is not set. + /// + /// # Returns + /// + /// * `Result` - On success, returns the parsed `AbsolutePath`. + /// * On failure, returns an error indicating the missing or ill-formed environment variable. + fn _var_path + ( + name : &'static str, + default : Option< &'static str >, + ) -> Result< pth::AbsolutePath > + { + let p = var( name, default )?; + pth::AbsolutePath::from_paths( ( pth::CurrentPath, p ) ) + .map_err( |e| Error::VariableIllformed( name, e.to_string() ) ) + } + +} + +crate::mod_interface! +{ + + own use + { + Error, + Result, + }; + + orphan use + { + Secret, + }; + +} \ No newline at end of file diff --git a/module/move/assistant/src/util.rs b/module/move/assistant/src/util.rs new file mode 100644 index 0000000000..7e34c0fd16 --- /dev/null +++ b/module/move/assistant/src/util.rs @@ -0,0 +1,10 @@ +//! +//! Collection of utility functions for this crate. +//! + +mod private {} + +crate::mod_interface! +{ + layer display_table; +} \ No newline at end of file diff --git a/module/move/assistant/src/util/display_table.rs b/module/move/assistant/src/util/display_table.rs new file mode 100644 index 0000000000..c4e7ddcd28 --- /dev/null +++ b/module/move/assistant/src/util/display_table.rs @@ -0,0 +1,128 @@ +//! +//! Function for displaying tabular data according to `TableConfig`. +//! + +mod private +{ + + use std::fmt; + + use format_tools:: + { + TableFormatter, + output_format, + print, + TableOutputFormat, + }; + + use crate::*; + use commands::{ TableConfig }; + + /// Function for displaying tabular data according to `TableConfig`. + pub fn display_tabular_data<'a> + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ >, + table_config : &'a TableConfig, + ) -> fmt::Result + { + if table_config.as_table + { + display_table( data, f, table_config ) + } + else if table_config.as_records + { + display_records( data, f, table_config ) + } + else if table_config.columns + { + display_columns( data, f, table_config ) + } + else + { + display_table( data, f, table_config ) + } + } + + fn display_table<'a> + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ >, + table_config : &'a TableConfig, + ) -> fmt::Result + { + let mut format = output_format::Table::default(); + format.max_width = table_config.max_table_width; + + display_data + ( + data, + f, + format, + &table_config.filter_columns, + ) + } + + fn display_records<'a> + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ >, + table_config : &'a TableConfig, + ) -> fmt::Result + { + let mut format = output_format::Records::default(); + format.max_width = table_config.max_table_width; + + display_data + ( + data, + f, + format, + &table_config.filter_columns, + ) + } + + fn display_columns<'a> + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ >, + table_config : &'a TableConfig, + ) -> fmt::Result + { + let mut format = output_format::Records::default(); + format.max_width = table_config.max_table_width; + + display_data + ( + data, + f, + format, + &table_config.filter_columns, + ) + } + + fn display_data<'a> + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ >, + format : impl TableOutputFormat, + filter_columns : &'a Vec< String >, + ) -> fmt::Result + { + let mut printer = print::Printer::with_format( &format ); + let binding = | title : &str | + { + filter_columns.is_empty() || filter_columns.iter().any( |c| c.as_str() == title ) + }; + printer.filter_col = &binding; + + let mut context = print::Context::new( f, printer ); + TableFormatter::fmt( data, &mut context ) + } + +} + +crate::mod_interface! +{ + own use display_tabular_data; +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/context_test.rs b/module/move/assistant/tests/inc/agents_tests/context_test.rs new file mode 100644 index 0000000000..e28fc8c264 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/context_test.rs @@ -0,0 +1,128 @@ +use super::*; + +use the_module::agents:: +{ + path::Path, + context:: + { + ContextDir, + ContextEntry, + }, +}; + +#[ test ] +fn context_dir_add_terminal() +{ + let mut ctx : ContextDir< () > = ContextDir::new(); + let entry = ContextEntry::Terminal( () ); + let name = "test"; + + let res = ctx.add( name, entry.clone() ); + + assert!( res ); + assert_eq!( ctx.get( name ), Some( &entry ) ); +} + +#[ test ] +fn context_dir_add_dir() +{ + let mut ctx : ContextDir< () > = ContextDir::new(); + let entry : ContextEntry< () > = ContextDir::new().into(); + let name = "test"; + + let res = ctx.add( name, entry.clone() ); + + assert!( res ); + assert_eq!( ctx.get( name ), Some( &entry ) ); +} + +#[ test ] +fn context_dir_add_duplicate() +{ + let name = "test"; + let orig_entry = ContextEntry::Terminal( 1 ); + + let mut ctx : ContextDir< usize > = ContextDir::new(); + ctx.add( name, orig_entry.clone() ); + + let res = ctx.add( name, ContextEntry::Terminal( 2 ) ); + + assert!( !res ); + assert_eq!( ctx.get( name ), Some( &orig_entry ) ); +} + +#[ test ] +fn context_dir_get() +{ + let mut ctx : ContextDir< usize > = ContextDir::new(); + ctx.add( "test_1", ContextEntry::Terminal( 1 ) ); + ctx.add( "test_2", ContextEntry::Terminal( 2 ) ); + ctx.add( "test_3", ContextEntry::Terminal( 3 ) ); + + assert_eq!( ctx.get( "test_1" ), Some( &ContextEntry::Terminal( 1 ) ) ); + assert_eq!( ctx.get( "test_2" ), Some( &ContextEntry::Terminal( 2 ) ) ); + assert_eq!( ctx.get( "test_3" ), Some( &ContextEntry::Terminal( 3 ) ) ); +} + +#[ test ] +fn context_dir_get_non_existing() +{ + let ctx : ContextDir< () > = ContextDir::new(); + + let res = ctx.get( "test" ); + + assert!( res.is_none() ); +} + +#[ test ] +fn context_dir_get_by_path_relative() +{ + let value_1 = ContextEntry::Terminal( 1 ); + let value_2 = ContextEntry::Terminal( 2 ); + let value_3 = ContextEntry::Terminal( 3 ); + + let mut dir_1 : ContextDir< usize > = ContextDir::new(); + dir_1.add( "value_1", value_1.clone() ); + dir_1.add( "value_2", value_2.clone() ); + + let mut dir_3 : ContextDir< usize > = ContextDir::new(); + dir_3.add( "value_3", value_3.clone() ); + + let mut dir_2 : ContextDir< usize > = ContextDir::new(); + dir_2.add( "dir_3", dir_3.into() ); + + let mut ctx : ContextDir< usize > = ContextDir::new(); + ctx.add( "dir_1", dir_1.into() ); + ctx.add( "dir_2", dir_2.into() ); + + let got_value_1 = ctx.get_by_path( &Path::try_from( "dir_1::value_1" ).unwrap() ); + let got_value_2 = ctx.get_by_path( &Path::try_from( "dir_1::value_2" ).unwrap() ); + let got_value_3 = ctx.get_by_path( &Path::try_from( "dir_2::dir_3::value_3" ).unwrap() ); + + assert_eq!( got_value_1, Some( &value_1 ) ); + assert_eq!( got_value_2, Some( &value_2 ) ); + assert_eq!( got_value_3, Some( &value_3 ) ); +} + +#[ test ] +fn context_dir_get_by_path_absolute() +{ + let entry = ContextEntry::Terminal( () ); + let mut ctx : ContextDir< () > = ContextDir::new(); + ctx.add( "test", entry.clone() ); + + let res = ctx.get_by_path( &&Path::try_from( "::test" ).unwrap() ); + + assert!( res.is_some() ); + assert_eq!( res.unwrap(), &entry ); +} + +#[ test ] +fn context_dir_get_by_path_non_existing() +{ + let ctx : ContextDir< () > = ContextDir::new(); + + let res = ctx.get_by_path( &Path::try_from( "test" ).unwrap() ); + + assert!( res.is_none() ); +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/mod.rs b/module/move/assistant/tests/inc/agents_tests/mod.rs new file mode 100644 index 0000000000..f4260d9ed5 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/mod.rs @@ -0,0 +1,9 @@ +use super::*; + +mod test_scenarios; + +mod path_test; +mod context_test; +mod scenario_raw_test; +mod scenario_raw_processors; +mod scenario_processed_test; \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/path_test.rs b/module/move/assistant/tests/inc/agents_tests/path_test.rs new file mode 100644 index 0000000000..277f4965ff --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/path_test.rs @@ -0,0 +1,259 @@ +use super::*; + +use the_module::agents::path::Path; + +#[ test ] +fn path_create_right() +{ + let path_str = "agent::completion"; + + let path = Path::try_from( path_str ); + + assert!( path.is_ok() ); + assert_eq! ( path.unwrap().inner(), path_str ); +} + +#[ test ] +fn path_create_wrong() +{ + let path = Path::try_from( "agent:completion" ); + assert!( path.is_err() ); +} + +#[ test ] +fn path_create_absolute() +{ + let path_str = "::agent::completion"; + + let path = Path::try_from( path_str ); + + assert!( path.is_ok() ); + assert_eq! ( path.unwrap().inner(), path_str ); +} + +#[ test ] +fn path_create_trailing() +{ + let path_str = "agent::completion::"; + + let path = Path::try_from( path_str ); + + assert!( path.is_ok() ); + assert_eq! ( path.unwrap().inner(), path_str ); +} + +#[ test ] +fn path_some_parent_relative() +{ + let path_str = "agent::completion"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "agent" ); +} + +#[ test ] +fn path_some_parent_relative_trailing() +{ + let path_str = "agent::completion::"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "agent" ); +} + +#[ test ] +fn path_some_parent_absolute() +{ + let path_str = "::agent"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "::" ); +} + +#[ test ] +fn path_some_parent_absolute_trailing() +{ + let path_str = "::agent::"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_some() ); + assert_eq!( path_parent.unwrap().inner(), "::" ); +} + +#[ test ] +fn path_none_parent() +{ + let path_str = "agent"; + let path = Path::try_from( path_str ).unwrap(); + + let path_parent = path.parent(); + + assert!( path_parent.is_none() ); +} + +#[ test ] +fn path_is_relative() +{ + let path_str = "agent"; + let path = Path::try_from( path_str ).unwrap(); + + let is_relative = path.is_relative(); + let is_absolute = path.is_absolute(); + + assert!( is_relative ); + assert!( !is_absolute ); +} + +#[ test ] +fn path_is_absolute() +{ + let path_str = "::agent"; + let path = Path::try_from( path_str ).unwrap(); + + let is_relative = path.is_relative(); + let is_absolute = path.is_absolute(); + + assert!( !is_relative ); + assert!( is_absolute ); +} + +#[ test ] +fn path_join_relative() +{ + let orig_path = Path::try_from( "agent" ).unwrap(); + let append = Path::try_from( "completion" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert_eq!( combined.inner(), "agent::completion" ); +} + +#[ test ] +fn path_join_absolute() +{ + let orig_path = Path::try_from( "agent" ).unwrap(); + let append = Path::try_from( "::completion" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert_eq!( combined.inner(), "::completion" ); +} + +#[ test ] +fn path_join_root() +{ + let orig_path = Path::try_from( "::" ).unwrap(); + let append = Path::try_from( "agent" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert_eq!( combined.inner(), "::agent" ); +} + +#[ test ] +fn path_join_trailing() +{ + let orig_path = Path::try_from( "agents::" ).unwrap(); + let append = Path::try_from( "completion" ).unwrap(); + + let combined = orig_path.join( &append ); + + assert_eq!( combined.inner(), "agents::completion" ); +} + +#[ test ] +fn path_starts_with_abs_abs() +{ + let a = Path::try_from( "::agent::completion" ).unwrap(); + let b = Path::try_from( "::agent" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( starts_with ); +} + +#[ test ] +fn path_starts_with_abs_rel() +{ + let a = Path::try_from( "::agent::completion" ).unwrap(); + let b = Path::try_from( "agent" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_starts_with_rel_abs() +{ + let a = Path::try_from( "agent" ).unwrap(); + let b = Path::try_from( "::agent::completion" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_starts_with_rel_rel() +{ + let a = Path::try_from( "agent::completion" ).unwrap(); + let b = Path::try_from( "agent" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( starts_with ); +} + +#[ test ] +fn path_not_starts_with_abs_abs() +{ + let a = Path::try_from( "::agent::completion" ).unwrap(); + let b = Path::try_from( "::output" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_not_starts_with_rel_rel() +{ + let a = Path::try_from( "agent::completion" ).unwrap(); + let b = Path::try_from( "output" ).unwrap(); + + let starts_with = a.starts_with( &b ); + + assert!( !starts_with ); +} + +#[ test ] +fn path_inner() +{ + let path_str = "::agent::completion"; + let path = Path::try_from( path_str ).unwrap(); + + let inner = path.inner(); + + assert_eq!( inner, path_str ); +} + +#[ test ] +fn path_components() +{ + let path = Path::try_from( "::agents::completion" ).unwrap(); + + let components : Vec< &str > = path.components().collect(); + + assert_eq!( components, vec![ "::", "agents", "completion" ] ); +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_processed_test.rs b/module/move/assistant/tests/inc/agents_tests/scenario_processed_test.rs new file mode 100644 index 0000000000..5fc734ae41 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_processed_test.rs @@ -0,0 +1,25 @@ +use super::*; + +use the_module::agents::scenario_processed::ScenarioProcessed; + +use test_scenarios:: +{ + gen_test_scenario_raw, + gen_test_scenario_raw_wrong, +}; + +#[ test ] +fn scenario_processed_right() +{ + let scenario_processed = ScenarioProcessed::try_from( gen_test_scenario_raw() ); + + assert!( scenario_processed.is_ok() ); +} + +#[ test ] +fn scenario_processed_wrong() +{ + let scenario_processed = ScenarioProcessed::try_from( gen_test_scenario_raw_wrong() ); + + assert!( scenario_processed.is_err() ); +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/mod.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/mod.rs new file mode 100644 index 0000000000..bbaccfe254 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/mod.rs @@ -0,0 +1,4 @@ +use super::*; + +mod plantuml_formatter_test; +mod yaml_formatter_test; \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/plantuml_formatter_test.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/plantuml_formatter_test.rs new file mode 100644 index 0000000000..44d5cf86b7 --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/plantuml_formatter_test.rs @@ -0,0 +1,33 @@ +use super::*; + +use the_module::agents::scenario_raw_processors::plantuml_formatter::plantuml_formatter; + +use test_scenarios::gen_test_scenario_raw; + + +#[ test ] +fn plantuml_formatter_test() +{ + let expected_plantuml = r#"@startuml +json node_1 { + "type": "agents::completion", + "model": "gpt-4o-mini" +} +json node_2 { + "type": "agents::classify", + "model": "gpt-4o" +} +json ::scenario::termination { +} +node_1 --> node_2 : next +node_2 --> ::scenario::termination : next +@enduml"#; + + let scenario_raw = gen_test_scenario_raw(); + + let mut buffer = Vec::new(); + let result = plantuml_formatter( &scenario_raw, &mut buffer ); + + assert!( result.is_ok() ); + assert_eq!( String::from_utf8( buffer ).unwrap(), expected_plantuml ); +} diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/yaml_formatter_test.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/yaml_formatter_test.rs new file mode 100644 index 0000000000..fd64cbacec --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_processors/yaml_formatter_test.rs @@ -0,0 +1,33 @@ +use super::*; + +use the_module::agents::scenario_raw_processors::yaml_formatter::yaml_formatter; + +use test_scenarios::gen_test_scenario_raw; + +#[ test ] +fn yaml_formatter_test() +{ + let expected_yaml = r#"nodes: +- id: node_1 + type: agents::completion + model: gpt-4o-mini + next: node_2 +- id: node_2 + type: agents::classify + model: gpt-4o + next: ::scenario::termination"#; + + let scenario_raw = gen_test_scenario_raw(); + + let mut buffer = Vec::new(); + let result = yaml_formatter( &scenario_raw, &mut buffer ); + assert!( result.is_ok() ); + + let result = String::from_utf8( buffer ); + assert!( result.is_ok() ); + + let result = result.unwrap(); + println!( "{}", result ); + + assert_eq!( result.trim(), expected_yaml.trim() ); +} diff --git a/module/move/assistant/tests/inc/agents_tests/scenario_raw_test.rs b/module/move/assistant/tests/inc/agents_tests/scenario_raw_test.rs new file mode 100644 index 0000000000..2f8acc60fe --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/scenario_raw_test.rs @@ -0,0 +1,49 @@ +use super::*; + +use the_module::agents::scenario_raw::ScenarioRaw; + +use test_scenarios::gen_test_scenario_raw; + +#[ test ] +fn scenario_read() +{ + let scenario_text = r#" + nodes: + - id: node_1 + type: agents::completion + model: gpt-4o-mini + next: node_2 + + - id: node_2 + type: agents::classify + model: gpt-4o + next: ::scenario::termination + "#; + + let expected_scenario_raw = gen_test_scenario_raw(); + + let scenario_raw = ScenarioRaw::read( scenario_text.as_bytes() ); + + assert!( scenario_raw.is_ok() ); + + let scenario_raw = scenario_raw.unwrap(); + assert_eq!( scenario_raw, expected_scenario_raw ); +} + +#[ test ] +fn scenario_wrong() +{ + let scenario_text = r#" + nodes: + - completion: + model: + company: openai + name: gpt-4o + depends_on: + node_2 + "#; + + let scenario_raw = ScenarioRaw::read( scenario_text.as_bytes() ); + + assert!( scenario_raw.is_err() ); +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/agents_tests/test_scenarios.rs b/module/move/assistant/tests/inc/agents_tests/test_scenarios.rs new file mode 100644 index 0000000000..02433a68ea --- /dev/null +++ b/module/move/assistant/tests/inc/agents_tests/test_scenarios.rs @@ -0,0 +1,64 @@ +use super::*; + +use the_module::agents::scenario_raw:: +{ + ScenarioRaw, + NodeRaw, +}; + +/// Generates an example `ScenarioRaw`. +pub fn gen_test_scenario_raw() -> ScenarioRaw +{ + ScenarioRaw::former() + .nodes( vec! + [ + NodeRaw::former() + .id( "node_1".to_string() ) + .r#type( "agents::completion".to_string() ) + .params( + { + let mut map : HashMap< String, String > = HashMap::new(); + map.insert( "model".into(), "gpt-4o-mini".into() ); + map + } + ) + .next( "node_2".to_string() ) + .form(), + + NodeRaw::former() + .id( "node_2".to_string() ) + .r#type( "agents::classify".to_string() ) + .params( + { + let mut map : HashMap< String, String > = HashMap::new(); + map.insert( "model".into(), "gpt-4o".into() ); + map + } + ) + .next( "::scenario::termination".to_string() ) + .form(), + ] ) + .form() +} + +/// Generates a `ScenarioRaw` with wrong syntax for `Path`. +pub fn gen_test_scenario_raw_wrong() -> ScenarioRaw +{ + ScenarioRaw::former() + .nodes( vec! + [ + NodeRaw::former() + .id( "node_1".to_string() ) + .r#type( ":agents:".to_string() ) // This part is incorrect. Path written in wrong syntax. + .params( + { + let mut map : HashMap< String, String > = HashMap::new(); + map.insert( "model".into(), "gpt-4o-mini".into() ); + map + } + ) + .next( "node_2".to_string() ) + .form(), + ] ) + .form() +} \ No newline at end of file diff --git a/module/move/assistant/tests/inc/mod.rs b/module/move/assistant/tests/inc/mod.rs index 0706620c6e..abf35e2f97 100644 --- a/module/move/assistant/tests/inc/mod.rs +++ b/module/move/assistant/tests/inc/mod.rs @@ -1,6 +1,7 @@ #[ allow( unused_imports ) ] use super::*; -mod basic_test; +mod agents_tests; +mod basic_test; mod experiment; diff --git a/module/move/deterministic_rand/Cargo.toml b/module/move/deterministic_rand/Cargo.toml index 1a469f1249..ae667e3e41 100644 --- a/module/move/deterministic_rand/Cargo.toml +++ b/module/move/deterministic_rand/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deterministic_rand" -version = "0.5.0" +version = "0.6.0" edition = "2021" authors = [ "Kostiantyn Wandalen ", diff --git a/module/move/gspread/.secret/readme.md b/module/move/gspread/.secret/readme.md new file mode 100644 index 0000000000..e3e100f72d --- /dev/null +++ b/module/move/gspread/.secret/readme.md @@ -0,0 +1,42 @@ +# Getting API Keys for OAuth Authentication + +Follow these steps to create and configure your OAuth credentials for using Google APIs. + +## 1. Create API Credentials + +1. Go to the [Google API Console](https://console.developers.google.com/). +2. From the projects list, select an existing project or create a new one. +3. In the left side menu, select **APIs & Services**. +4. On the left menu, click **Credentials**. +5. Click **Create Credentials** and select **OAuth client ID**. +6. In the **Application type** section, select **Desktop app**. +7. Provide an appropriate name for your client ID (e.g., "Gspread OAuth Client"). +8. Click **Create**. + +Once the credential is created, you will receive a **Client ID** and **Client Secret**. These are required for accessing the API. + +## 2. Store Your Credentials + +Save the **Client ID** and **Client Secret** in a `.env` within a `.secret` directory. The file should look like this: + +```bash +CLIENT_ID=YOUR_CLIENT_ID +CLIENT_SECRET=YOUR_SECRET_KEY +``` + +## 3. Why do we need it? + +After executing each command, you need to grant the GSPREAD program access to the Google API. You will receive a link that begin with 'Please direct your browser to https://....' that will redirect you to your browser, where you must authorize the access. You will need to select the appropriate Google account that has the credentials for the application. The **CLIENT_ID** and **CLIENT_SECRET** are set up to do this process. + +## 4. Troubleshooting + +If you encounter a page displaying an error instead of the Google account selection screen, it is likely that you need to add **AUTH_URI** or **TOKEN_URI** to the .env file. In this case, all four secrets are required. To retrieve them, download the API key you created in JSON format. Open the file and copy the necessary keys into the .env file. After making these changes, your .env file should look like this: + +```bash +CLIENT_ID=YOUR_CLIENT_ID +CLIENT_SECRET=YOUR_SECRET_KEY +AUTH_URI=YOUR_AUTH_URI +TOKEN_URI=YOUR_TOKEN_URI +``` + +If you still get some issues, follow [Google OAuth Documentation](https://developers.google.com/identity/protocols/oauth2/). \ No newline at end of file diff --git a/module/move/gspread/Cargo.toml b/module/move/gspread/Cargo.toml new file mode 100644 index 0000000000..8d1d86b4a3 --- /dev/null +++ b/module/move/gspread/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "gspread" +version = "0.1.0" +edition = "2021" +authors = [ + "Vsevolod Bakutov " +] +license = "MIT" +description = """ + Google Sheets Cli API +""" +categories = [ "algorithms", "development-tools" ] +keywords = [ "fundamental", "general-purpose" ] +default-run = "main" + +[[bin]] +name = "main" +path = "src/bin/main.rs" + +[features] +default = [ "enabled" ] +full = [ "enabled" ] +enabled = [ + "former/enabled", + "format_tools/enabled", + "reflect_tools/enabled", +] + +[dependencies] +mod_interface = { workspace = true, features = ["full"] } +former = { workspace = true, features = ["full"] } +format_tools = { workspace = true, features = ["full"] } +reflect_tools = { workspace = true, features = [ "full" ] } +clap = { version = "4.5.20", features = ["derive"] } +tokio = { version = "1", features = ["full"] } +google-sheets4 = "6.0.0" +hyper-util = "0.1.10" +yup-oauth2 = "11.0.0" +pth = "0.21.0" +dotenv = "0.15" +serde = { version = "1.0.213", features = ["derive"] } +serde_with = "3.11.0" +error_tools = "0.19.0" +derive_tools = { version = "0.32.0", features = ["full"] } +serde_json = "1.0.132" +regex = "1.11.1" + +[dev-dependencies] +test_tools = { workspace = true } diff --git a/module/move/gspread/readme.md b/module/move/gspread/readme.md new file mode 100644 index 0000000000..2fdeb0f40a --- /dev/null +++ b/module/move/gspread/readme.md @@ -0,0 +1 @@ +## Google Sheets CLI \ No newline at end of file diff --git a/module/move/gspread/src/actions.rs b/module/move/gspread/src/actions.rs new file mode 100644 index 0000000000..1c96538040 --- /dev/null +++ b/module/move/gspread/src/actions.rs @@ -0,0 +1,16 @@ +//! +//! CLI actions of the tool. +//! + +mod private {} + +crate::mod_interface! +{ + layer gspread; + layer gspread_get_header; + layer gspread_get_rows; + layer gspread_cell_get; + layer gspread_cell_set; + layer gspread_cells_set; +} + diff --git a/module/move/gspread/src/actions/gspread.rs b/module/move/gspread/src/actions/gspread.rs new file mode 100644 index 0000000000..60b0fd980c --- /dev/null +++ b/module/move/gspread/src/actions/gspread.rs @@ -0,0 +1,66 @@ +//! +//! Google Sheets API actions. +//! +//! This module also contains the definition of Google Sheets Error. +//! + +mod private +{ + use regex::Regex; + use error_tools::typed::Error; + use derive_tools::AsRefStr; + use crate::*; + use ser::DisplayFromStr; + + #[ ser::serde_as ] + #[ derive( Debug, Error, AsRefStr, ser::Serialize ) ] + #[ serde( tag = "type", content = "data" ) ] + pub enum Error + { + #[ error( "Google Sheets returned error:\n{0}" ) ] + ApiError + ( + #[ from ] + #[ serde_as( as = "DisplayFromStr" ) ] + google_sheets4::Error + ), + + #[ error( "Invalid URL format: {0}" ) ] + InvalidUrl + ( + String + ), + } + + pub fn get_spreadsheet_id_from_url + ( + url : &str + ) -> Result< &str > + { + + let re = Regex::new( r"d/([^/]+)/edit" ).unwrap(); + if let Some( captures ) = re.captures( url ) + { + if let Some( id ) = captures.get( 1 ) + { + return Ok( id.as_str() ); + } + } + + Err + ( + Error::InvalidUrl( "Wrong url format.\nFix: copy sheet's the whole url from your browser. Usage: --url ''".to_string() ) + ) + } + + pub type Result< T > = core::result::Result< T, Error >; +} + +crate::mod_interface! +{ + own use + { + Result, + get_spreadsheet_id_from_url, + }; +} \ No newline at end of file diff --git a/module/move/gspread/src/actions/gspread_cell_get.rs b/module/move/gspread/src/actions/gspread_cell_get.rs new file mode 100644 index 0000000000..3a4d6b1be3 --- /dev/null +++ b/module/move/gspread/src/actions/gspread_cell_get.rs @@ -0,0 +1,42 @@ +//! +//! Action for command "cell get" +//! +//! It returns a selected cell +//! + +mod private +{ + use crate::*; + use actions::gspread::Result; + use client::SheetsType; + use ser::JsonValue; + + pub async fn action + ( + hub : &SheetsType, + spreadsheet_id : &str, + table_name : &str, + cell_id : &str, + ) -> Result< JsonValue > + { + let result = hub + .spreadsheets() + .values_get( spreadsheet_id, format!( "{}!{}", table_name, cell_id ).as_str() ) + .doit() + .await? + .1 + .values; + + match result + { + Some( values ) => Ok( values.get( 0 ).unwrap().get( 0 ).unwrap().clone() ), + None => Ok( JsonValue::Null.clone() ) + } + + } +} + +crate::mod_interface! +{ + own use action; +} \ No newline at end of file diff --git a/module/move/gspread/src/actions/gspread_cell_set.rs b/module/move/gspread/src/actions/gspread_cell_set.rs new file mode 100644 index 0000000000..818a667f1c --- /dev/null +++ b/module/move/gspread/src/actions/gspread_cell_set.rs @@ -0,0 +1,50 @@ +//! +//! Action for command "cell set" +//! +//! It updates a selected cell +//! + + +mod private +{ + use google_sheets4::api::ValueRange; + use crate::*; + use actions::gspread::Result; + use client::SheetsType; + use ser::JsonValue; + + pub async fn action + ( + hub : &SheetsType, + spreadsheet_id : &str, + table_name : &str, + cell_id : &str, + value : &str + ) -> Result< i32 > + { + + let value = JsonValue::String( value.to_string() ); + let value_range = ValueRange + { + values : Some( vec![ vec![ value ] ] ), + ..ValueRange::default() + }; + + let result = hub + .spreadsheets() + .values_update( value_range, spreadsheet_id, format!( "{}!{}", table_name, cell_id ).as_str() ) + .value_input_option( "USER_ENTERED" ) + .doit() + .await? + .1 + .updated_cells + .unwrap(); + + Ok( result ) + } +} + +crate::mod_interface! +{ + own use action; +} \ No newline at end of file diff --git a/module/move/gspread/src/actions/gspread_cells_set.rs b/module/move/gspread/src/actions/gspread_cells_set.rs new file mode 100644 index 0000000000..a6528b6c4b --- /dev/null +++ b/module/move/gspread/src/actions/gspread_cells_set.rs @@ -0,0 +1,132 @@ +//! +//! Set command -> set specified values in specified columns in specified row +//! + +mod private +{ + use crate::*; + use google_sheets4::api:: + { + BatchUpdateValuesRequest, + ValueRange + }; + use ser:: + { + Deserialize, + JsonValue + }; + use std::collections::HashMap; + + /// Structure for --json value + #[ derive( Deserialize, Debug ) ] + struct ParsedJson + { + #[ serde( flatten ) ] + columns : HashMap< String, String > + } + + /// Parse --json value + fn parse_json + ( + json_str : &str + ) -> Result< ParsedJson, String > + { + serde_json::from_str::< ParsedJson >( json_str ).map_err + ( + | err | format!( "Failed to parse JSON: {}", err ) + ) + } + + /// Check availables keys. + /// Available keys: "id" -> row's id + fn check_select_row_by_key + ( + key : &str + ) -> Result< (), String > + { + let keys = vec![ "id" ]; + if keys.contains( &key ) + { + Ok( () ) + } + else + { + Err( format!( "Invalid select_row_by_key: '{}'. Allowed keys: {:?}", key, keys ) ) + } + } + + fn is_all_uppercase_letters + ( + s : &str + ) -> Result< (), String > + { + if s.chars().all( | c | c.is_ascii_uppercase() ) + { + Ok( () ) + } + else + { + Err( format!( "The string '{}' contains invalid characters. Only uppercase letters (A-Z) are allowed.", s ) ) + } + } + + pub async fn action + ( + hub : &SheetsType, + select_row_by_key : &str, + json_str : &str, + spreadsheet_id : &str, + table_name : &str + ) -> Result< String, String > + { + check_select_row_by_key( select_row_by_key )?; + + let mut pairs = parse_json( json_str )?; + + let row_id = pairs + .columns + .remove( select_row_by_key ) + .ok_or_else( || format!( "Key '{}' not found in JSON", select_row_by_key ) )?; + + let mut value_ranges= Vec::new(); + + for ( key, value ) in pairs.columns.into_iter() + { + is_all_uppercase_letters( key.as_str() )?; + value_ranges.push + ( + ValueRange + { + range: Some( format!( "{}!{}{}", table_name, key, row_id ) ), + values: Some( vec![ vec![ JsonValue::String( value.to_string() ) ] ] ), + ..Default::default() + } + ); + }; + + let req = BatchUpdateValuesRequest + { + value_input_option: Some( "USER_ENTERED".to_string() ), + data: Some( value_ranges ), + include_values_in_response: Some( true ), + ..Default::default() + }; + + let result = hub + .spreadsheets() + .values_batch_update( req, spreadsheet_id ) + .doit() + .await; + + match result + { + Ok( _ ) => Ok( format!( "Cells were sucsessfully updated!" ) ), + Err( error ) => Err( format!( "{}", error ) ) + } + } +} + +crate::mod_interface! +{ + own use action; +} \ No newline at end of file diff --git a/module/move/gspread/src/actions/gspread_get_header.rs b/module/move/gspread/src/actions/gspread_get_header.rs new file mode 100644 index 0000000000..8f7b83c477 --- /dev/null +++ b/module/move/gspread/src/actions/gspread_get_header.rs @@ -0,0 +1,58 @@ +//! +//! Action for command "header" +//! +//! It returns header (first row) +//! + + +mod private +{ + use std::fmt; + use crate::*; + use client::SheetsType; + use actions::gspread::Result; + use format_tools::AsTable; + use util::display_table::display_header; + use ser::JsonValue; + + #[ derive( Debug ) ] + pub struct Report + { + pub rows : Vec< RowWrapper > + } + + impl fmt::Display for Report + { + fn fmt + ( + &self, + f : &mut fmt::Formatter + ) -> fmt::Result + { + display_header( &AsTable::new( &self.rows ), f ) + } + } + + pub async fn action + ( + hub : &SheetsType, + spreadsheet_id : &str, + table_name: &str) -> Result< Vec< Vec< JsonValue > > > + { + let result = hub + .spreadsheets() + .values_get( spreadsheet_id, format!( "{}!A1:Z1", table_name ).as_str() ) + .doit() + .await? + .1 + .values + .unwrap_or_else( | | Vec::new() ); + + Ok( result ) + } +} + +crate::mod_interface! +{ + own use action; +} \ No newline at end of file diff --git a/module/move/gspread/src/actions/gspread_get_rows.rs b/module/move/gspread/src/actions/gspread_get_rows.rs new file mode 100644 index 0000000000..3a083217ed --- /dev/null +++ b/module/move/gspread/src/actions/gspread_get_rows.rs @@ -0,0 +1,38 @@ +//! +//! Action for command "rows" +//! +//! It returns all rows but not header +//! + + +mod private +{ + use crate::*; + use client::SheetsType; + use actions::gspread::Result; + use ser::JsonValue; + + pub async fn action + ( + hub : &SheetsType, + spreadsheet_id : &str, + table_name : &str + ) -> Result< Vec< Vec < JsonValue > > > + { + let result = hub + .spreadsheets() + .values_get( spreadsheet_id, format!( "{}!A2:Z", table_name ).as_str() ) + .doit() + .await? + .1 + .values + .unwrap_or_else( | | Vec::new() ); + + Ok( result ) + } +} + +crate::mod_interface! +{ + own use action; +} diff --git a/module/move/gspread/src/bin/main.rs b/module/move/gspread/src/bin/main.rs new file mode 100644 index 0000000000..8f55f07f1c --- /dev/null +++ b/module/move/gspread/src/bin/main.rs @@ -0,0 +1,32 @@ +use std::error::Error; +use clap::Parser; +use dotenv::dotenv; + +use gspread:: +{ + client::hub, + commands::{ Cli, CliCommand, self }, + secret::Secret, +}; + +#[ tokio::main ] +async fn main() -> Result< (), Box< dyn Error > > +{ + dotenv().ok(); + + let secret = Secret::read(); + + let hub = hub( &secret ).await?; + + let cli = Cli::parse(); + + match cli.command + { + CliCommand::GSpread( cmd ) => + { + commands::gspread::command( &hub, cmd ).await; + } + } + + Ok( () ) +} diff --git a/module/move/gspread/src/client.rs b/module/move/gspread/src/client.rs new file mode 100644 index 0000000000..c836ccf4d3 --- /dev/null +++ b/module/move/gspread/src/client.rs @@ -0,0 +1,79 @@ +//! +//! Client of API. +//! + +mod private +{ + + use google_sheets4 as sheets4; + use sheets4::Sheets; + use sheets4::hyper_rustls; + use sheets4::hyper_util; + use sheets4::yup_oauth2:: + { + self, + ApplicationSecret + }; + use hyper_util::client::legacy::connect::HttpConnector; + + pub use hyper_util::client::legacy::Client; + + use std:: + { + error::Error, + }; + + use crate::*; + use secret::Secret; + + pub type SheetsType = Sheets< hyper_rustls::HttpsConnector< HttpConnector > >; + + pub async fn hub( secrets: &Secret ) -> Result< SheetsType, Box< dyn Error > > + { + let secret: ApplicationSecret = ApplicationSecret + { + client_id : secrets.CLIENT_ID.clone(), + auth_uri : secrets.AUTH_URI.clone(), + token_uri : secrets.TOKEN_URI.clone(), + client_secret : secrets.CLIENT_SECRET.clone(), + .. Default::default() + }; + + let auth = yup_oauth2::InstalledFlowAuthenticator::builder + ( + secret, + yup_oauth2::InstalledFlowReturnMethod::HTTPRedirect, + ) + .build() + .await + .unwrap(); + + let client = Client::builder + ( + hyper_util::rt::TokioExecutor::new() + ) + .build + ( + hyper_rustls::HttpsConnectorBuilder::new() + .with_native_roots() + .unwrap() + .https_or_http() + .enable_http1() + .build() + ); + + Ok( Sheets::new( client, auth ) ) + } + + +} + +crate::mod_interface! +{ + exposed use + { + hub, + Client, + SheetsType + }; +} \ No newline at end of file diff --git a/module/move/gspread/src/commands.rs b/module/move/gspread/src/commands.rs new file mode 100644 index 0000000000..5ce88f9eb2 --- /dev/null +++ b/module/move/gspread/src/commands.rs @@ -0,0 +1,52 @@ +//! +//! Commands +//! + + +mod private +{ + + use clap:: + { + Parser, + Subcommand + }; + + use crate::*; + use commands::gspread; + + /// CLI commands of the tool. + #[ derive ( Debug, Parser ) ] + pub struct Cli + { + /// Root of the CLI commands. + #[ command ( subcommand ) ] + pub command : CliCommand, + } + + /// Root of the CLI commands. + #[ derive ( Debug, Subcommand ) ] + pub enum CliCommand + { + /// Google Sheets commands. + #[ command ( subcommand, name = "gspread" ) ] + GSpread( gspread::Command ), + } + +} + +crate::mod_interface! +{ + layer gspread; + layer gspread_header; + layer gspread_rows; + layer gspread_cell; + layer gspread_cells; + + own use + { + Cli, + CliCommand, + }; +} + diff --git a/module/move/gspread/src/commands/gspread.rs b/module/move/gspread/src/commands/gspread.rs new file mode 100644 index 0000000000..8398aa3ec6 --- /dev/null +++ b/module/move/gspread/src/commands/gspread.rs @@ -0,0 +1,104 @@ +//! +//! Collection of Google Sheets API commands. +//! + + +mod private +{ + + use clap::{ Subcommand, Parser }; + + use crate::*; + use client::SheetsType; + + use commands:: + { + gspread_header, + gspread_rows, + gspread_cell, + gspread_cells + }; + + #[ derive( Debug, Parser ) ] + pub struct CommonArgs + { + #[ arg( long ) ] + pub url : String, + + #[ arg( long ) ] + pub tab : String + } + + #[ derive( Debug, Subcommand ) ] + pub enum Command + { + + #[ command ( name = "header" ) ] + Header + ( + CommonArgs + ), + + #[ command( name = "rows" ) ] + Rows + ( + CommonArgs + ), + + #[ command ( subcommand, name = "cell" ) ] + Cell + ( + gspread_cell::Commands + ), + + #[ command ( subcommand, name = "cells" ) ] + Cells + ( + gspread_cells::Commands + ) + + } + + pub async fn command + ( + hub : &SheetsType, + command : Command, + ) + { + match command + { + + Command::Header( header_command ) => + { + gspread_header::command( hub, header_command ).await; + }, + + Command::Rows( rows_command ) => + { + gspread_rows::command( hub, rows_command ).await; + }, + + Command::Cell( cell_command ) => + { + gspread_cell::command( hub, cell_command ).await; + }, + + Command::Cells( cells_command) => + { + gspread_cells::command( hub, cells_command ).await; + }, + + } + } + +} + +crate::mod_interface! +{ + own use + { + CommonArgs, + Command, + command, + }; +} \ No newline at end of file diff --git a/module/move/gspread/src/commands/gspread_cell.rs b/module/move/gspread/src/commands/gspread_cell.rs new file mode 100644 index 0000000000..057da2dd09 --- /dev/null +++ b/module/move/gspread/src/commands/gspread_cell.rs @@ -0,0 +1,122 @@ +//! +//! Collection of subcommands fo command "cell" +//! + +mod private +{ + + use clap::Subcommand; + + use crate::*; + use actions; + use actions::gspread::get_spreadsheet_id_from_url; + use client::SheetsType; + + #[ derive( Debug, Subcommand ) ] + pub enum Commands + { + #[ command( name = "get" ) ] + Get + { + #[ arg( long ) ] + url : String, + + #[ arg( long ) ] + tab : String, + + #[ arg( long ) ] + cel : String, + }, + + #[ command( name = "set" ) ] + Set + { + #[ arg( long ) ] + url : String, + + #[ arg( long ) ] + tab : String, + + #[ arg( long ) ] + cel : String, + + #[ arg( long ) ] + val : String + } + } + + pub async fn command + ( + hub : &SheetsType, + commands : Commands + ) + { + match commands + { + Commands::Get { url, tab, cel } => + { + let spreadsheet_id = match get_spreadsheet_id_from_url( url.as_str() ) + { + Ok( id ) => id, + Err( error ) => + { + eprintln!( "Error extracting spreadsheet ID: {}", error ); + return; + } + }; + + let result = actions::gspread_cell_get::action + ( + hub, + spreadsheet_id, + tab.as_str(), + cel.as_str() + ).await; + + match result + { + Ok( value ) => println!( "Value: {}", value ), + Err( error ) => println!( "Error: {}", error ), + } + }, + + Commands::Set { url, tab, cel, val } => + { + let spreadsheet_id = match get_spreadsheet_id_from_url( url.as_str() ) + { + Ok( id ) => id, + Err( error ) => + { + eprintln!( "Error extracting spreadsheet ID: {}", error ); + return; + } + }; + + let result = actions::gspread_cell_set::action + ( + hub, + spreadsheet_id, + tab.as_str(), + cel.as_str(), + val.as_str() + ).await; + + match result + { + Ok( value ) => println!( "Success: {:?}", value ), + Err( error ) => println!( "Error: {}", error ), + } + } + + } + } +} + +crate::mod_interface! +{ + own use + { + command, + Commands, + }; +} \ No newline at end of file diff --git a/module/move/gspread/src/commands/gspread_cells.rs b/module/move/gspread/src/commands/gspread_cells.rs new file mode 100644 index 0000000000..13ecf1e378 --- /dev/null +++ b/module/move/gspread/src/commands/gspread_cells.rs @@ -0,0 +1,80 @@ +//! +//! Cells commands. +//! set command -> set specified values in specified columns in specified row. +//! + +mod private +{ + use clap::Subcommand; + + use crate::*; + use actions::gspread::get_spreadsheet_id_from_url; + + #[ derive( Debug, Subcommand ) ] + pub enum Commands + { + #[ command( name = "set" ) ] + Set + { + #[ arg( long ) ] + select_row_by_key : String, + + #[ arg( long ) ] + json : String, + + #[ arg( long ) ] + url : String, + + #[ arg( long ) ] + tab : String + } + + } + + pub async fn command + ( + hub : &SheetsType, + commands : Commands + ) + { + match commands + { + Commands::Set { select_row_by_key, json, url, tab } => + { + let spreadsheet_id = match get_spreadsheet_id_from_url( url.as_str() ) + { + Ok( id ) => id, + Err( error ) => + { + eprintln!( "Error extracting spreadsheet ID: {}", error ); + return; + } + }; + + let result = actions::gspread_cells_set::action + ( + &hub, + select_row_by_key.as_str(), + json.as_str(), + spreadsheet_id, + tab.as_str() + ).await; + + match result + { + Ok( msg ) => println!( "{}", msg ), + Err( error ) => println!( "{}", error ) + } + } + } + } +} + +crate::mod_interface! +{ + own use + { + command, + Commands + }; +} \ No newline at end of file diff --git a/module/move/gspread/src/commands/gspread_header.rs b/module/move/gspread/src/commands/gspread_header.rs new file mode 100644 index 0000000000..5048d3e4ed --- /dev/null +++ b/module/move/gspread/src/commands/gspread_header.rs @@ -0,0 +1,86 @@ +//! +//! Command "header" +//! + +mod private +{ + use std::fmt; + use crate::*; + use commands::gspread::CommonArgs; + use client::SheetsType; + use actions; + use actions::gspread::get_spreadsheet_id_from_url; + use format_tools::AsTable; + use util::display_table::display_header; + + #[ derive( Debug ) ] + pub struct Report + { + pub rows : Vec< RowWrapper > + } + + impl fmt::Display for Report + { + fn fmt + ( + &self, + f : &mut fmt::Formatter + ) -> fmt::Result + { + display_header( &AsTable::new( &self.rows ), f ) + } + } + + pub async fn command + ( + hub : &SheetsType, + args : CommonArgs, + ) + { + match args + { + CommonArgs { url, tab } => + { + let spreadsheet_id = match get_spreadsheet_id_from_url( url.as_str() ) + { + Ok( id ) => id, + Err( error ) => + { + eprintln!( "Error extracting spreadsheet ID: {}", error ); + return; + } + }; + + let result = actions::gspread_get_header::action + ( + hub, + spreadsheet_id, + tab.as_str() + ).await; + + match result + { + Ok( header ) => + { + let header_wrapped = header + .into_iter() + .map( | row | RowWrapper{ max_len: row.len(), row } ) + .collect(); + + println!( "Header: \n {}", Report{ rows: header_wrapped } ); + } + Err( error ) => println!( "Error: {}", error ), + } + } + } + } +} + +crate::mod_interface! +{ + own use + { + command + }; +} + diff --git a/module/move/gspread/src/commands/gspread_rows.rs b/module/move/gspread/src/commands/gspread_rows.rs new file mode 100644 index 0000000000..426d7f2dde --- /dev/null +++ b/module/move/gspread/src/commands/gspread_rows.rs @@ -0,0 +1,85 @@ +//! +//! Command "rows" +//! + +mod private +{ + use std::fmt; + use crate::*; + use commands::gspread::CommonArgs; + use client::SheetsType; + use actions; + use actions::gspread::get_spreadsheet_id_from_url; + use format_tools::AsTable; + use util::display_table::display_rows; + + pub struct Report + { + pub rows : Vec< RowWrapper > + } + + impl fmt::Display for Report + { + fn fmt + ( + &self, + f : &mut fmt::Formatter + ) -> fmt::Result + { + display_rows( &AsTable::new( &self.rows ), f ) + } + } + + pub async fn command + ( + hub : &SheetsType, + args : CommonArgs + ) + { + match args + { + CommonArgs { url, tab } => + { + let spreadsheet_id = match get_spreadsheet_id_from_url( url.as_str() ) + { + Ok( id ) => id, + Err( error ) => + { + eprintln!( "Error extracting spreadsheet ID: {}", error ); + return; + } + }; + + let result = actions::gspread_get_rows::action + ( + hub, + spreadsheet_id, + tab.as_str() + ).await; + + match result + { + Ok( rows ) => + { + let max_len = rows.iter().map(|row| row.len()).max().unwrap_or(0); + let rows_wrapped: Vec = rows + .into_iter() + .map(|row| RowWrapper { row, max_len }) + .collect(); + + println!( "Rows: \n {}", Report{ rows: rows_wrapped } ); + } + Err( error ) => println!( "Error: {}", error ), + } + } + } + } +} + +crate::mod_interface! +{ + own use + { + command + }; +} diff --git a/module/move/gspread/src/debug.rs b/module/move/gspread/src/debug.rs new file mode 100644 index 0000000000..11f63d821e --- /dev/null +++ b/module/move/gspread/src/debug.rs @@ -0,0 +1,20 @@ +mod private +{ +} + +use format_tools:: +{ + Fields, + TableWithFields, +}; +use std::borrow::Cow; + +pub mod row_wrapper; + +crate::mod_interface! +{ + exposed use + { + row_wrapper::RowWrapper, + }; +} diff --git a/module/move/gspread/src/debug/row_wrapper.rs b/module/move/gspread/src/debug/row_wrapper.rs new file mode 100644 index 0000000000..b8e1635ac7 --- /dev/null +++ b/module/move/gspread/src/debug/row_wrapper.rs @@ -0,0 +1,59 @@ +//! +//! Gspread wrapper for outputting data to console +//! +//! It is used for "header" and "rows" commands +//! + +use super::*; +use crate::*; +use ser::JsonValue; + + +#[ derive( Debug ) ] +pub struct RowWrapper +{ + pub row: Vec< JsonValue >, + pub max_len: usize +} + +impl Clone for RowWrapper +{ + fn clone( &self ) -> Self + { + Self + { + row: self.row.clone(), + max_len: self.max_len.clone() + } + } +} + +impl TableWithFields for RowWrapper {} +impl Fields< &'_ str, Option< Cow< '_, str > > > +for RowWrapper +{ + type Key< 'k > = &'k str; + type Val< 'v > = Option< Cow< 'v, str > >; + + fn fields( &self ) -> impl IteratorTrait< Item= ( &'_ str, Option > ) > + { + let mut dst = Vec::new(); + + for ( index, value ) in self.row.iter().enumerate() + { + let column_name = format!( "Column{}", index ); + let title = Box::leak( column_name.into_boxed_str() ) as &str; + dst.push( ( title, Some( Cow::Owned( value.to_string() ) ) ) ) + } + + //adding empty values for missing cells + for index in self.row.len()..self.max_len + { + let column_name = format!( "Column{}", index ); + let title = Box::leak( column_name.into_boxed_str() ) as &str; + dst.push( ( title, Some( Cow::Owned( "".to_string() ) ) ) ); + } + + dst.into_iter() + } +} \ No newline at end of file diff --git a/module/move/gspread/src/lib.rs b/module/move/gspread/src/lib.rs new file mode 100644 index 0000000000..c0d2432985 --- /dev/null +++ b/module/move/gspread/src/lib.rs @@ -0,0 +1,41 @@ +use mod_interface::mod_interface; +use error_tools::thiserror; + +mod private +{ +} + +pub mod ser +{ + pub use serde:: + { + Serialize, + Deserialize, + }; + pub use serde_json:: + { + value::{ Value as JsonValue, Number as JsonNumber }, + error::Error, + self + }; + pub use serde_with::*; +} + +crate::mod_interface! +{ + + layer client; + layer debug; + layer commands; + layer actions; + layer secret; + layer util; + + exposed use ::reflect_tools:: + { + Fields, + _IteratorTrait, + IteratorTrait, + }; + +} \ No newline at end of file diff --git a/module/move/gspread/src/secret.rs b/module/move/gspread/src/secret.rs new file mode 100644 index 0000000000..48567b77f4 --- /dev/null +++ b/module/move/gspread/src/secret.rs @@ -0,0 +1,159 @@ +//! +//! Tool's secret +//! + +mod private +{ + use crate::*; + use std:: + { + env, + sync::OnceLock, + }; + + use error_tools::typed::Error; + use ser::DisplayFromStr; + + #[ ser::serde_as ] + #[ derive( Debug, Error, ser::Serialize ) ] + #[ serde( tag = "type", content = "data" ) ] + pub enum Error + { + #[ error( "Secret file is illformed\n{0}" ) ] + SecretFileIllformed + ( + #[ from ] + #[ serde_as( as = "DisplayFromStr" ) ] + dotenv::Error + ), + + #[ error( "Secret missing the variable {0}" ) ] + VariableMissing( &'static str ), + + #[ error( "Secret error processing in the variable {0}\n{1}" ) ] + VariableIllformed( &'static str, String ), + + } + + pub type Result< R > = std::result::Result< R, Error >; + + #[ derive( Debug ) ] + #[ allow( non_snake_case ) ] + pub struct Secret + { + pub CLIENT_SECRET : String, + pub CLIENT_ID: String, + pub AUTH_URI : String, + pub TOKEN_URI : String, + } + + impl Secret + { + #[ allow( non_snake_case ) ] + pub fn load() -> Result< Self > + { + let path = "./.secret/.env"; + + let r = dotenv::from_path( path ); + if let Err( ref err ) = r + { + if !matches!( err, dotenv::Error::Io(_) ) + { + return Err( r.expect_err( &format!( "Failed to load {path}" ) ).into() ); + } + } + + let config = Self + { + CLIENT_SECRET : var( "CLIENT_SECRET", None )?, + CLIENT_ID : var( "CLIENT_ID", None )?, + AUTH_URI : var ( "AUTH_URI", Some( "https://accounts.google.com/o/oauth2/auth" ) )?, + TOKEN_URI : var ( "TOKEN_URI", Some( "https://oauth2.googleapis.com/token" ) )? + }; + Ok( config ) + } + + pub fn read() -> Secret + { + Self::load().unwrap_or_else( | err | + { + let example = include_str!("../.secret/readme.md"); + let explanation = format! + ( + r#" = Lack of secrets + +Failed to load secret or some its parameters. +{err} + + = Fix + +Add missing secret to .env file in .secret directory. Example: MISSING_SECRET=YOUR_MISSING_SECRET + + = More information + +{example} +"# + ); + panic!( "{}", explanation ); + }) + } + + pub fn get() -> &'static Secret + { + static INSTANCE : OnceLock< Secret > = OnceLock::new(); + INSTANCE.get_or_init( || Self::read() ) + } + + } + + fn var + ( + name : &'static str, + default : Option< &'static str >, + ) -> Result < String > + { + match env::var( name ) + { + Ok( val ) => Ok ( val ), + Err( _ ) => + { + if let Some( default_value ) = default + { + Ok( default_value.to_string() ) + } + else + { + Err ( Error::VariableMissing( name ) ) + } + } + } + } + + fn _var_path + ( + name : &'static str, + default : Option<&'static str>, + ) -> Result < pth::AbsolutePath > + { + let p = var( name, default )?; + pth::AbsolutePath::from_paths( ( pth::CurrentPath, p ) ) + .map_err( |e| Error::VariableIllformed( name, e.to_string() ) ) + } + +} + +crate::mod_interface! +{ + + own use + { + Error, + Result, + }; + + orphan use + { + Secret, + }; + +} \ No newline at end of file diff --git a/module/move/gspread/src/util.rs b/module/move/gspread/src/util.rs new file mode 100644 index 0000000000..ac76ad86b2 --- /dev/null +++ b/module/move/gspread/src/util.rs @@ -0,0 +1,6 @@ +mod private {} + +crate::mod_interface! +{ + layer display_table; +} \ No newline at end of file diff --git a/module/move/gspread/src/util/display_table.rs b/module/move/gspread/src/util/display_table.rs new file mode 100644 index 0000000000..3d96964d9b --- /dev/null +++ b/module/move/gspread/src/util/display_table.rs @@ -0,0 +1,55 @@ + + +mod private +{ + + use std::fmt; + + use format_tools:: + { + TableFormatter, + print, + output_format, + TableOutputFormat + }; + + pub fn display_rows< 'a > + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ > + ) -> fmt::Result + { + display_data( data, f, output_format::Table::default() ) + } + + pub fn display_header < 'a > + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ > + ) -> fmt::Result + { + display_data( data, f, output_format::Table::default() ) + } + + pub fn display_data < 'a > + ( + data : &'a impl TableFormatter< 'a >, + f : &mut fmt::Formatter< '_ >, + format : impl TableOutputFormat, + ) -> fmt::Result + { + let printer = print::Printer::with_format( &format ); + let mut context = print::Context::new( f, printer ); + TableFormatter::fmt( data, &mut context ) + } + +} + +crate::mod_interface! +{ + own use + { + display_rows, + display_header + }; +} \ No newline at end of file diff --git a/module/move/gspread/tests/inc/cell_tests.rs b/module/move/gspread/tests/inc/cell_tests.rs new file mode 100644 index 0000000000..f93ec7b1a4 --- /dev/null +++ b/module/move/gspread/tests/inc/cell_tests.rs @@ -0,0 +1,99 @@ +#[ allow( unused_imports ) ] +use super::*; + +use the_module:: +{ + hub, + Secret, + actions, + SheetsType, + ser::JsonValue +}; + +async fn setup() -> ( SheetsType, &'static str, &'static str ) +{ + let secret = Secret::load().expect( "Failed to load secret" ); + let hub = hub( &secret ).await.expect( "Failed to create a hub" ); + let spreadsheet_id = "1EAEdegMpitv-sTuxt8mV8xQxzJE7h_J0MxQoyLH7xxU"; + let table_name = "tab5"; + + ( hub, spreadsheet_id, table_name ) +} + +#[ tokio::test ] +async fn test_get_cell() +{ + let ( hub, spreadsheet_id, table_name ) = setup().await; + let cell_id = "R2C1"; + + let result = actions::gspread_cell_get::action + ( + &hub, + spreadsheet_id, + table_name, + cell_id + ) + .await + .expect( "Error getting cell" ); + + assert_eq!( result, "Vsevolod" ) +} + +#[ tokio::test ] +async fn test_get_cell_empty() +{ + let ( hub, spreadsheet_id, table_name ) = setup().await; + let cell_id = "R4C1"; + + let result = actions::gspread_cell_get::action + ( + &hub, + spreadsheet_id, + table_name, + cell_id + ) + .await + .expect( "Error getting cell" ); + + assert_eq!( result, JsonValue::Null ) +} + +#[ tokio::test ] +async fn test_set_cell() +{ + let ( hub, spreadsheet_id, table_name ) = setup().await; + let cell_id = "R2C1"; + let value = "Seva"; + + let result = actions::gspread_cell_set::action + ( + &hub, + spreadsheet_id, + table_name, + cell_id, + value + ) + .await; + + assert!( result.is_ok() ); +} + +#[ tokio::test ] +async fn test_set_empty_cell() +{ + let ( hub, spreadsheet_id, table_name ) = setup().await; + let cell_id = "R4C1"; + let value = "Stanislav"; + + let result = actions::gspread_cell_set::action + ( + &hub, + spreadsheet_id, + table_name, + cell_id, + value + ) + .await; + + assert!( result.is_ok() ); +} \ No newline at end of file diff --git a/module/move/gspread/tests/inc/cells_tests.rs b/module/move/gspread/tests/inc/cells_tests.rs new file mode 100644 index 0000000000..4c91a9a19c --- /dev/null +++ b/module/move/gspread/tests/inc/cells_tests.rs @@ -0,0 +1,79 @@ +#[ allow( unused_imports ) ] +use super::*; + +use the_module:: +{ + hub, + Secret, + actions, + SheetsType, +}; + +async fn setup() -> ( SheetsType, &'static str, &'static str, &'static str ) +{ + let secret = Secret::load().expect( "Failed to load secret" ); + let hub = hub( &secret ).await.expect( "Failed to create a hub" ); + let select_row_by_key = "id"; + let spreadsheet_id = "1EAEdegMpitv-sTuxt8mV8xQxzJE7h_J0MxQoyLH7xxU"; + let table_name = "tab7"; + + ( hub, select_row_by_key, spreadsheet_id, table_name ) +} + +#[ tokio::test ] +async fn test_set_cells() +{ + let + ( + hub, + select_row_by_key, + spreadsheet_id, + table_name + ) = setup().await; + + let json = r#"{ "id": "2", "A": "new_val1", "B": "new_val2"}"#; + + let result = actions::gspread_cells_set::action + ( + &hub, + select_row_by_key, + json, + spreadsheet_id, + table_name, + ) + .await + .expect( "Error while updating" ); + + assert_eq!( result, "Cells were sucsessfully updated!" ) +} + +#[ tokio::test ] +async fn test_set_cells_wrong_row() +{ + let + ( + hub, + select_row_by_key, + spreadsheet_id, + table_name + ) = setup().await; + + let json = r#"{ "id": "a", "A": "new_val1", "B": "new_val2"}"#; + + let result = actions::gspread_cells_set::action + ( + &hub, + select_row_by_key, + json, + spreadsheet_id, + table_name, + ) + .await + .expect( "Error while updating" ); + + assert_eq! + ( + result, + r#"Bad Request: {"error":{"code":400,"message":"Invalid data[0]: Unable to parse range: tab7!Aa","status":"INVALID_ARGUMENT"}}"# + ) +} \ No newline at end of file diff --git a/module/move/gspread/tests/inc/header_tests.rs b/module/move/gspread/tests/inc/header_tests.rs new file mode 100644 index 0000000000..3009a63bb1 --- /dev/null +++ b/module/move/gspread/tests/inc/header_tests.rs @@ -0,0 +1,90 @@ +#[ allow( unused_imports ) ] +use super::*; + +use the_module:: +{ + hub, + Secret, + actions, + SheetsType +}; + +async fn setup() -> ( SheetsType, &'static str ) +{ + let secret = Secret::load().expect( "Failed to load secret" ); + let hub = hub( &secret ).await.expect( "Failed to create a hub" ); + let spreadsheet_id = "1EAEdegMpitv-sTuxt8mV8xQxzJE7h_J0MxQoyLH7xxU"; + + ( hub, spreadsheet_id ) +} +#[ tokio::test ] +async fn test_get_header() +{ + let ( hub, spreadsheet_id ) = setup().await; + let table_name = "tab1"; + + let result = actions::gspread_get_header::action + ( + &hub, + spreadsheet_id, + table_name + ) + .await + .expect( "Error getting header" ); + + assert_eq!( result, vec![ vec![ "Name", "Surname", "Age" ] ] ); +} + +#[ tokio::test ] +async fn test_get_header_with_spaces() +{ + let ( hub, spreadsheet_id ) = setup().await; + let table_name = "tab2"; + + let result = actions::gspread_get_header::action + ( + &hub, + spreadsheet_id, + table_name + ) + .await + .expect( "Error getting header" ); + + assert_eq!( result, vec![ vec![ "Name", "", "Age" ] ] ); +} + +#[ tokio::test ] +async fn test_get_header_empty() +{ + let ( hub, spreadsheet_id ) = setup().await; + let table_name = "tab3"; + + let result = actions::gspread_get_header::action + ( + &hub, + spreadsheet_id, + table_name + ) + .await + .expect( "Error getting header" ); + + assert_eq!( result, Vec::< Vec< String > >::new() ); +} + +#[ tokio::test ] +async fn test_get_header_with_empty_end() +{ + let ( hub, spreadsheet_id ) = setup().await; + let table_name = "tab4"; + + let result = actions::gspread_get_header::action + ( + &hub, + spreadsheet_id, + table_name + ) + .await + .expect( "Error getting header" ); + + assert_eq!( result, vec![ vec![ "Name", "Surname" ] ] ); +} \ No newline at end of file diff --git a/module/move/gspread/tests/inc/mod.rs b/module/move/gspread/tests/inc/mod.rs new file mode 100644 index 0000000000..a357c3fe4a --- /dev/null +++ b/module/move/gspread/tests/inc/mod.rs @@ -0,0 +1,14 @@ +//! +//! Here is used the +//! https://docs.google.com/spreadsheets/d/1EAEdegMpitv-sTuxt8mV8xQxzJE7h_J0MxQoyLH7xxU/edit?gid=0#gid=0 +//! test spreadsheet +//! + + +#[ allow( unused_imports ) ] +use super::*; + +mod header_tests; +mod rows_tests; +mod cell_tests; +mod cells_tests; \ No newline at end of file diff --git a/module/move/gspread/tests/inc/rows_tests.rs b/module/move/gspread/tests/inc/rows_tests.rs new file mode 100644 index 0000000000..d9032f8544 --- /dev/null +++ b/module/move/gspread/tests/inc/rows_tests.rs @@ -0,0 +1,93 @@ +#[ allow( unused_imports ) ] +use super::*; + +use the_module:: +{ + hub, + Secret, + actions, + SheetsType +}; + +async fn setup() -> ( SheetsType, &'static str ) +{ + let secret = Secret::load().expect( "Failed to load secret" ); + let hub = hub( &secret ).await.expect( "Failed to create a hub" ); + let spreadsheet_id = "1EAEdegMpitv-sTuxt8mV8xQxzJE7h_J0MxQoyLH7xxU"; + + ( hub, spreadsheet_id ) +} + +#[ tokio::test ] +async fn test_get_rows() +{ + let ( hub, spreadsheet_id ) = setup().await; + let table_name = "tab1"; + + let result = actions::gspread_get_rows::action + ( + &hub, + spreadsheet_id, + table_name + ) + .await + .expect( "Error getting rows" ); + + assert_eq! + ( + result, + vec![ + vec![ "Vsevolod", "Bakutov", "20" ], + vec![ "Victor", "Ovsyanik", "85" ], + vec![ "Olexandr", "Optimus", "28" ], + vec![ "Ivan", "Optimus", "34" ], + vec![ "Bogdan", "Optimus", "28" ], + ] + ) +} + +#[ tokio::test ] +async fn test_get_rows_with_spaces() +{ + let ( hub, spreadsheet_id ) = setup().await; + let table_name = "tab2"; + + let result = actions::gspread_get_rows::action + ( + &hub, + spreadsheet_id, + table_name + ) + .await + .expect( "Error getting rows" ); + + assert_eq! + ( + result, + vec![ + vec![ "Vsevolod", "Bakutov" ], + vec![ "Victor", "", "85" ], + vec![ "", "Optimus", "28" ], + vec![ ], + vec![ "Bogdan", "Optimus", "28" ], + ] + ) +} + +#[ tokio::test ] +async fn test_get_rows_empty() +{ + let ( hub, spreadsheet_id ) = setup().await; + let table_name = "tab3"; + + let result = actions::gspread_get_rows::action + ( + &hub, + spreadsheet_id, + table_name + ) + .await + .expect( "Error getting rows" ); + + assert_eq!( result, Vec::< Vec< String > >::new() ) +} \ No newline at end of file diff --git a/module/move/gspread/tests/smoke_test.rs b/module/move/gspread/tests/smoke_test.rs new file mode 100644 index 0000000000..c3163b32ed --- /dev/null +++ b/module/move/gspread/tests/smoke_test.rs @@ -0,0 +1,12 @@ + +#[ test ] +fn local_smoke_test() +{ + test_tools::smoke_test_for_local_run(); +} + +#[ test ] +fn published_smoke_test() +{ + test_tools::smoke_test_for_published_run(); +} \ No newline at end of file diff --git a/module/move/gspread/tests/tests.rs b/module/move/gspread/tests/tests.rs new file mode 100644 index 0000000000..201ae26926 --- /dev/null +++ b/module/move/gspread/tests/tests.rs @@ -0,0 +1,9 @@ + + +#[ allow( unused_imports ) ] +use gspread as the_module; +#[ allow( unused_imports ) ] +use test_tools::exposed::*; + +#[ cfg( feature = "enabled" ) ] +mod inc; \ No newline at end of file diff --git a/module/move/wca/Cargo.toml b/module/move/wca/Cargo.toml index da8d1227b6..06791cf645 100644 --- a/module/move/wca/Cargo.toml +++ b/module/move/wca/Cargo.toml @@ -40,12 +40,12 @@ harness = false [dependencies] ## internal -error_tools = { workspace = true, features = [ "default" ] } -strs_tools = { workspace = true, features = [ "default" ] } -mod_interface = { workspace = true, features = [ "default" ] } -iter_tools = { workspace = true, features = [ "default" ] } -former = { workspace = true, features = [ "default" ] } -# xxx : qqq : optimize set of features +error_tools = { workspace = true, features = [ "enabled", "error_typed", "error_untyped" ] } +mod_interface = { workspace = true, features = [ "enabled" ] } +iter_tools = { workspace = true, features = [ "enabled" ] } +former = { workspace = true, features = [ "enabled", "derive_former" ] } +# xxx : aaa : optimize set of features +# aaa : done. ## external log = "0.4" diff --git a/module/move/wca/Readme.md b/module/move/wca/Readme.md index b808fce2bc..ecda885b57 100644 --- a/module/move/wca/Readme.md +++ b/module/move/wca/Readme.md @@ -14,7 +14,7 @@ The tool to make CLI ( commands user interface ). It is able to aggregate extern ```rust #[ cfg( not( feature = "no_std" ) ) ] { - use wca::{ VerifiedCommand, Context, Type }; + use wca::{ VerifiedCommand, Type }; fn main() { @@ -37,7 +37,7 @@ The tool to make CLI ( commands user interface ). It is able to aggregate extern .end() .perform(); - let args = std::env::args().skip( 1 ).collect::< Vec< String > >(); + let args: Vec< String > = std::env::args().skip( 1 ).collect(); ca.perform( args ).unwrap(); } diff --git a/module/move/wca/examples/wca_custom_error.rs b/module/move/wca/examples/wca_custom_error.rs new file mode 100644 index 0000000000..47fef16985 --- /dev/null +++ b/module/move/wca/examples/wca_custom_error.rs @@ -0,0 +1,43 @@ +//! +//! # Handling Errors with CommandsAggregator +//! +//! This module provides an example of how to use `wca::CommandsAggregator` to manage error handling in a command-line interface. The `CommandsAggregator` offers a fluent interface for defining commands and associating them with various error types, making it straightforward to handle and present errors in a structured way. +//! +//! ## Purpose +//! +//! The primary goal of this example is to showcase how `CommandsAggregator` facilitates error handling, whether errors are simple strings, custom typed errors, untyped errors, or errors with additional context. This approach ensures that error management is both consistent and extensible. +//! + +#[ derive( Debug, error_tools::typed::Error )] +enum CustomError +{ + #[ error( "this is typed error" ) ] + TheError, +} + +fn main() -> error_tools::error::untyped::Result< () > +{ + let ca = wca::CommandsAggregator::former() + .command( "error.string" ) + .hint( "Returns error as a string" ) + .routine( || { Err( "this is string error" ) } ) + .end() + .command( "error.typed" ) + .hint( "Returns error as a custom error" ) + .routine( || { Err( CustomError::TheError ) } ) + .end() + .command( "error.untyped" ) + .hint( "Returns error as untyped error" ) + .routine( || { Err( error_tools::error::untyped::format_err!( "this is untyped error" ) ) } ) + .end() + .command( "error.with_context" ) + .hint( "Returns error as untyped error with context" ) + .routine( || { Err( error_tools::error::untyped::format_err!( "this is untyped error" ).context( "with context" ) ) } ) + .end() + .perform(); + + let args: Vec< String > = std::env::args().skip( 1 ).collect(); + () = ca.perform( args )?; + + Ok( () ) +} \ No newline at end of file diff --git a/module/move/wca/examples/wca_fluent.rs b/module/move/wca/examples/wca_fluent.rs index 487d6ee97d..3d5475a481 100644 --- a/module/move/wca/examples/wca_fluent.rs +++ b/module/move/wca/examples/wca_fluent.rs @@ -7,10 +7,10 @@ //! -use wca::{ Context, Handler, Type, VerifiedCommand }; +use wca::{ executor::{ Context, Handler }, Type, VerifiedCommand }; use std::sync::{ Arc, Mutex }; -fn main() +fn main() -> error_tools::error::untyped::Result< () > { let ca = wca::CommandsAggregator::former() @@ -45,7 +45,8 @@ fn main() .end() .perform(); - let args = std::env::args().skip( 1 ).collect::< Vec< String > >(); - ca.perform( args ).unwrap(); + let args: Vec< String > = std::env::args().skip( 1 ).collect(); + ca.perform( args )?; + Ok( () ) } diff --git a/module/move/wca/examples/wca_suggest.rs b/module/move/wca/examples/wca_suggest.rs index 2bb73fa111..b9b54989a8 100644 --- a/module/move/wca/examples/wca_suggest.rs +++ b/module/move/wca/examples/wca_suggest.rs @@ -22,7 +22,7 @@ use wca::{ CommandsAggregator, Type, VerifiedCommand }; -fn main() +fn main() -> error_tools::error::untyped::Result< () > { let ca = CommandsAggregator::former() @@ -34,14 +34,11 @@ fn main() { println!( "= Args\n{:?}\n\n= Properties\n{:?}\n", o.args, o.props ); }) - .end() + .end() .perform(); - let args = std::env::args().skip( 1 ).collect::< Vec< String > >(); - match ca.perform( args.join( " " ) ) - { - Ok( _ ) => {} - Err( err ) => println!( "{err}" ), - }; + let args: Vec< String > = std::env::args().skip( 1 ).collect(); + ca.perform( args.join( " " ) )?; + Ok( () ) } diff --git a/module/move/wca/examples/wca_trivial.rs b/module/move/wca/examples/wca_trivial.rs index c228e6e20a..1df6dec815 100644 --- a/module/move/wca/examples/wca_trivial.rs +++ b/module/move/wca/examples/wca_trivial.rs @@ -16,7 +16,7 @@ fn exit() std::process::exit( 0 ) } -fn main() +fn main() -> error_tools::error::untyped::Result< () > { let ca = CommandsAggregator::former() .command( "exit" ) @@ -33,7 +33,7 @@ fn main() .perform() ; - // aaa : qqq2 : for Bohdan : that should work + // aaa : aaa2 : for Bohdan : that should work // let ca = wca::CommandsAggregator::former() // .command( "echo" ) // .hint( "prints all subjects and properties" ) @@ -50,6 +50,8 @@ fn main() // ca.execute( input ).unwrap(); //aaa: works - let input = std::env::args().skip( 1 ).collect::< Vec< String > >(); - ca.perform( input ).unwrap(); + let input: Vec< String > = std::env::args().skip( 1 ).collect(); + ca.perform( input )?; + + Ok( () ) } diff --git a/module/move/wca/src/ca/aggregator.rs b/module/move/wca/src/ca/aggregator.rs index dbf912d055..fb3725ba16 100644 --- a/module/move/wca/src/ca/aggregator.rs +++ b/module/move/wca/src/ca/aggregator.rs @@ -5,7 +5,6 @@ mod private use crate::*; use ca:: { - Verifier, Executor, grammar::command:: { @@ -16,16 +15,17 @@ mod private }, help::{ HelpGeneratorFn, HelpGeneratorOptions, HelpVariants }, }; + use verifier::{ Verifier, VerificationError, VerifiedCommand }; + use parser::{ Program, Parser, ParserError }; + use grammar::Dictionary; + use executor::Context; - // qqq : group uses - use std::collections::HashSet; - use std::fmt; + use std:: + { + fmt, + collections::HashSet + }; use former::StoragePreform; - // use wtools:: - // { - // }; - // use wtools::thiserror; - #[ allow( clippy::wildcard_imports ) ] use error:: { // Result, @@ -57,11 +57,11 @@ mod private /// source of the program input : String, /// original error - error : wError, + error : ParserError, }, /// This variant represents errors that occur during grammar conversion. #[ error( "Can not identify a command.\nDetails: {0}" ) ] - Verifier( wError ), + Verifier( VerificationError ), /// This variant is used to represent errors that occur during executor conversion. #[ error( "Can not find a routine for a command.\nDetails: {0}" ) ] ExecutorConverter( wError ), @@ -73,14 +73,14 @@ mod private { /// This variant is used to represent validation errors. /// It carries a `ValidationError` payload that provides additional information about the error. - #[ error( "Validation error. {0}" ) ] + #[ error( "Validation error\n{0}" ) ] Validation( ValidationError ), /// This variant represents execution errors. - #[ error( "Execution failed. {0:?}" ) ] + #[ error( "Execution failed\n{0:?}" ) ] Execution( wError ), } - // xxx : qqq : qqq2 : for Bohdan : one level is obviously redundant + // xxx : aaa : aaa2 : for Bohdan : one level is obviously redundant // Program< Namespace< ExecutableCommand_ > > -> Program< ExecutableCommand_ > // aaa : done. The concept of `Namespace` has been removed #[ allow( clippy::type_complexity ) ] @@ -294,7 +294,7 @@ mod private callback.0( &program.join( " " ), &grammar_program ); } - self.executor.program( &self.dictionary, grammar_program ).map_err( Error::Execution ) + self.executor.program( &self.dictionary, grammar_program ).map_err( | e | Error::Execution( e.into() ) ) } } } @@ -304,8 +304,8 @@ mod private crate::mod_interface! { exposed use CommandsAggregator; - exposed use CommandsAggregatorFormer; - exposed use Error; - exposed use ValidationError; + orphan use CommandsAggregatorFormer; + orphan use Error; + orphan use ValidationError; exposed use Order; } diff --git a/module/move/wca/src/ca/executor/context.rs b/module/move/wca/src/ca/executor/context.rs index fdcc5bc220..a9611b618e 100644 --- a/module/move/wca/src/ca/executor/context.rs +++ b/module/move/wca/src/ca/executor/context.rs @@ -8,7 +8,7 @@ mod private /// # Examples: /// /// ``` - /// # use wca::{ Routine, Handler, Context, Value, Args, Props, VerifiedCommand }; + /// # use wca::{ executor::{ Routine, Handler, Args, Props, Context }, Value, VerifiedCommand }; /// # use std::sync::{ Arc, Mutex }; /// let routine = Routine::from( Handler::from /// ( @@ -34,7 +34,7 @@ mod private /// } /// assert_eq!( 1, *ctx.get::< Mutex< i32 > >().unwrap().lock().unwrap() ); /// ``` - // qqq : ? + // xxx clarification is needed qqq : поточнити #[ derive( Debug, Clone ) ] pub struct Context { @@ -93,5 +93,5 @@ mod private crate::mod_interface! { - exposed use Context; + orphan use Context; } diff --git a/module/move/wca/src/ca/executor/executor.rs b/module/move/wca/src/ca/executor/executor.rs index 1d53814e58..d6a7a3bdf1 100644 --- a/module/move/wca/src/ca/executor/executor.rs +++ b/module/move/wca/src/ca/executor/executor.rs @@ -3,13 +3,23 @@ mod private #[ allow( clippy::wildcard_imports ) ] use crate::*; - // use wtools::error::Result; - // use error::return_err; use ca::help::{ HelpGeneratorOptions, generate_help_content, LevelOfDetail }; + use verifier::VerifiedCommand; + use parser::Program; + use grammar::Dictionary; + use executor::{ Routine, Context }; // aaa : for Bohdan : how is it useful? where is it used? // aaa : `ExecutorType` has been removed + #[ derive( Debug, error::typed::Error ) ] + pub enum CommandError + { + #[ error( "Internal command: `.{}` failed with: {}", command.phrase, error ) ] + Internal { command: VerifiedCommand, error: InternalCommandError }, + #[ error( "Command: `.{}` failed with: {}", command.phrase, error ) ] + User { command: VerifiedCommand, error: error::untyped::Error }, + } /// Executor that is responsible for executing the program's commands. /// It uses the given `Context` to store and retrieve values during runtime. @@ -38,9 +48,10 @@ mod private /// A `Result` with `Ok( () )` if the execution was successful, or an `Err` containing an error message if an error occurred. /// # Errors /// qqq: doc - // qqq : use typed error + // aaa : use typed error + // aaa : done pub fn program( &self, dictionary : &Dictionary, program : Program< VerifiedCommand > ) - -> error::untyped::Result< () > + -> Result< (), CommandError > { for command in program.commands { @@ -66,18 +77,21 @@ mod private /// qqq: doc /// # Panics /// qqq: doc - // qqq : use typed error + // aaa : use typed error + // aaa : done pub fn command( &self, dictionary : &Dictionary, command : VerifiedCommand ) - -> error::untyped::Result< () > + -> Result< (), CommandError > { if command.internal_command { - _exec_internal_command( dictionary, command ) + _exec_internal_command( dictionary, command.clone() ) + .map_err( | error | CommandError::Internal { command, error } ) } else { let routine = dictionary.command( &command.phrase ).unwrap().routine.clone(); - _exec_command( command, routine, self.context.clone() ) + _exec_command( command.clone(), routine, self.context.clone() ) + .map_err( | error | CommandError::User { command, error } ) } } @@ -86,6 +100,7 @@ mod private } // qqq : use typed error + // aaa : should it be typed? it is user command with unknown error type fn _exec_command( command : VerifiedCommand, routine : Routine, ctx : Context ) -> error::untyped::Result< () > { @@ -96,10 +111,20 @@ mod private } } - // qqq : use typed error + #[ derive( Debug, error::typed::Error ) ] + pub enum InternalCommandError + { + #[ error( "Encountered an unrecognized internal command: `.{user_input}`." ) ] + UnknownInternalCommand { user_input: String }, + #[ error( "Not found command that starts with `.{user_input}`." ) ] + CommandNotFound { user_input: String }, + } + + // aaa : use typed error + // aaa : done #[ allow( clippy::needless_pass_by_value ) ] fn _exec_internal_command( dictionary : &Dictionary, command : VerifiedCommand ) - -> error::untyped::Result< () > + -> Result< (), InternalCommandError > { match command.phrase.as_str() { @@ -129,7 +154,7 @@ mod private let commands = dictionary.search( name.strip_prefix( '.' ).unwrap_or( name ) ); if commands.is_empty() { - error::untyped::return_err!( "Not found command that starts with `.{}`.", name ); + return Err( InternalCommandError::CommandNotFound { user_input : name.into() } ); } let generator_args = HelpGeneratorOptions::former() .command_prefix( "." ) @@ -158,10 +183,10 @@ mod private } else { - error::untyped::return_err!( "Not found command that starts with `.{}`.", name ); + return Err( InternalCommandError::CommandNotFound { user_input : name.into() } ); } } - unexpected => error::untyped::return_err!( "Encountered an unrecognized internal command: `.{}`.", unexpected ), + unexpected => return Err( InternalCommandError::UnknownInternalCommand { user_input: unexpected.into() }), } Ok( () ) diff --git a/module/move/wca/src/ca/executor/routine.rs b/module/move/wca/src/ca/executor/routine.rs index 845b653103..f40594af22 100644 --- a/module/move/wca/src/ca/executor/routine.rs +++ b/module/move/wca/src/ca/executor/routine.rs @@ -4,13 +4,17 @@ mod private #[ allow( clippy::wildcard_imports ) ] use crate::*; - // qqq : group + // aaa : group + // aaa : done - use std::collections::HashMap; - // use wtools::error::Result; - - use std::{ fmt::Formatter, rc::Rc }; - // use wtools::anyhow::anyhow; + use std:: + { + collections::HashMap, + fmt::Formatter, + rc::Rc, + }; + use verifier::VerifiedCommand; + use executor::Context; /// Command Args /// @@ -19,7 +23,7 @@ mod private /// # Example: /// /// ``` - /// use wca::{ Args, Value }; + /// use wca::{ executor::Args, Value }; /// /// let args = Args( vec![ Value::String( "Hello, World!".to_string() ) ] ); /// @@ -32,7 +36,7 @@ mod private /// /// ## Use case /// ``` - /// # use wca::{ Routine, Handler, VerifiedCommand }; + /// # use wca::{ executor::{ Routine, Handler }, VerifiedCommand }; /// let routine = Routine::from( Handler::from /// ( /// | o : VerifiedCommand | @@ -49,7 +53,7 @@ mod private /// Returns owned casted value by its index /// /// ``` - /// # use wca::{ Args, Value }; + /// # use wca::{ executor::Args, Value }; /// /// let args = Args( vec![ Value::String( "Hello, World!".to_string() ) ] ); /// @@ -82,7 +86,7 @@ mod private /// # Example: /// /// ``` - /// use wca::{ Props, Value }; + /// use wca::{ executor::Props, Value }; /// /// let props = Props( [ ( "hello".to_string(), Value::String( "World!".to_string() ) ) ].into() ); /// let hello_prop : &str = props.get_owned( "hello" ).unwrap(); @@ -92,7 +96,7 @@ mod private /// /// ## Use case /// ``` - /// # use wca::{ Routine, Handler, Props, VerifiedCommand }; + /// # use wca::{ executor::{ Routine, Handler, Props }, VerifiedCommand }; /// let routine = Routine::from( Handler::from /// ( /// | o : VerifiedCommand | @@ -109,7 +113,7 @@ mod private /// Returns owned casted value by its key /// /// ``` - /// # use wca::{ Props, Value }; + /// # use wca::{ executor::Props, Value }; /// /// let props = Props( [ ( "hello".to_string(), Value::String( "World!".to_string() ) ) ].into() ); /// let hello_prop : &str = props.get_owned( "hello" ).unwrap(); @@ -135,7 +139,10 @@ mod private // aaa : done. now it works with the following variants: // fn(), fn(args), fn(props), fn(args, props), fn(context), fn(context, args), fn(context, props), fn(context, args, props) - // qqq : why not public? + // aaa : why not public? // aaa : described + + // These type aliases are kept private to hide implementation details and prevent misuse. + // Exposing them would risk complicating the API and limit future refactoring flexibility. type RoutineWithoutContextFn = dyn Fn( VerifiedCommand ) -> error::untyped::Result< () >; type RoutineWithContextFn = dyn Fn( Context, VerifiedCommand ) -> error::untyped::Result< () >; @@ -143,7 +150,7 @@ mod private /// Routine handle. /// /// ``` - /// # use wca::{ Handler, Routine }; + /// # use wca::executor::{ Handler, Routine }; /// let routine = Routine::from( Handler::from /// ( /// || @@ -154,7 +161,7 @@ mod private /// ``` /// /// ``` - /// # use wca::{ Handler, Routine, VerifiedCommand }; + /// # use wca::{ executor::{ Handler, Routine }, VerifiedCommand }; /// let routine = Routine::from( Handler::from /// ( /// | o : VerifiedCommand | @@ -165,7 +172,7 @@ mod private /// ``` /// /// ``` - /// # use wca::{ Handler, Routine }; + /// # use wca::executor::{ Handler, Routine }; /// let routine = Routine::from( Handler::from /// ( /// | ctx, o | @@ -246,7 +253,7 @@ mod private /// /// - `WithoutContext`: A routine that does not require any context. /// - `WithContext`: A routine that requires a context. -// qqq : for Bohdan : instead of array of Enums, lets better have 5 different arrays of different Routine and no enum +// xxx clarification is needed : for Bohdan : instead of array of Enums, lets better have 5 different arrays of different Routine and no enum // to use statical dispatch #[ derive( Clone ) ] pub enum Routine @@ -330,15 +337,25 @@ mod private } // xxx + // aaa : This is an untyped error because we want to provide a common interface for all commands, while also allowing users to propagate their own specific custom errors. impl IntoResult for core::convert::Infallible { fn into_result( self ) -> error::untyped::Result< () > { Ok( () ) } } impl IntoResult for () { fn into_result( self ) -> error::untyped::Result< () > { Ok( () ) } } - impl< E : core::fmt::Debug > IntoResult + impl< E : core::fmt::Debug + std::fmt::Display + 'static > IntoResult for error::untyped::Result< (), E > { fn into_result( self ) -> error::untyped::Result< () > { - self.map_err( | e | error::untyped::format_err!( "{e:?}" )) - // xxx : qqq : ? + use std::any::TypeId; + // if it's anyhow error we want to have full context(debug), and if it's not(this error) we want to display + if TypeId::of::< error::untyped::Error >() == TypeId::of::< E >() + { + self.map_err( | e | error::untyped::format_err!( "{e:?}" )) + } + else + { + self.map_err( | e | error::untyped::format_err!( "{e}" )) + } + // xxx : aaa : ? } } } @@ -347,8 +364,8 @@ mod private crate::mod_interface! { - exposed use Routine; - exposed use Handler; - exposed use Args; - exposed use Props; + orphan use Routine; + orphan use Handler; + orphan use Args; + orphan use Props; } diff --git a/module/move/wca/src/ca/facade.rs b/module/move/wca/src/ca/facade.rs deleted file mode 100644 index 80fca20afc..0000000000 --- a/module/move/wca/src/ca/facade.rs +++ /dev/null @@ -1,345 +0,0 @@ -// mod private -// { -// use crate::*; -// use core::fmt; -// use ca::grammar; -// -// /// Macro for parsing WCA arguments. -// /// -// /// # Examples -// /// ```rust -// /// use wca::Value; -// /// -// /// let mut args = vec![ Value::Number( 42. ), Value::String( "Rust".into() ) ].into_iter(); -// /// wca::parse_args!( args, n : f64, name : String ); -// /// -// /// assert_eq!( n, 42. ); -// /// assert_eq!( name, "Rust" ); -// /// ``` -// #[macro_export] -// macro_rules! parse_args -// { -// ( $args : ident, mut $b : ident : $ty : ident $( $rest : tt )* ) => -// { -// let mut $b : $ty = std::convert::TryFrom::try_from( $args.next().unwrap() ).unwrap(); -// $crate::parse_args!( $args $( $rest )* ) -// }; -// ( $args : ident, $b : ident : $ty : ident $( $rest : tt )* ) => -// { -// let $b : $ty = std::convert::TryFrom::try_from( $args.next().unwrap() ).unwrap(); -// $crate::parse_args!( $args $( $rest )* ) -// }; -// ( $args : ident, $b : ident $( $rest : tt )* ) => -// { -// let $b = $args.next().unwrap(); -// $crate::parse_args!( $args $( $rest )* ) -// }; -// ( $args : ident, mut $b : ident $( $rest : tt )* ) => -// { -// let mut $b = $args.next().unwrap(); -// $crate::parse_args!( $args $( $rest )* ) -// }; -// ( $args : ident ) => -// { -// assert!( $args.next().is_none() ); -// }; -// ( $args : ident, ) => -// { -// $crate::parse_args!( $args ) -// }; -// } -// -// /// Creates a command-line interface (CLI) builder with the given initial state. -// /// -// /// This function initializes a `CommandBuilder` with the provided `state` and -// /// returns it for further configuration of the CLI. -// pub fn cui< T >( state : T ) -> CommandBuilder< T > -// { -// CommandBuilder::with_state( state ) -// } -// -// /// A struct representing a property. -// #[ derive( Debug, Clone ) ] -// pub struct Property< 'a > -// { -// /// The name of the property. -// pub name : &'a str, -// /// The hint for the property. -// pub debug : &'a str, -// /// The tag representing the property's type. -// pub tag : Type, -// } -// -// impl< 'a > Property< 'a > -// { -// /// Constructor of a property. -// pub fn new( name : &'a str, hint : &'a str, tag : Type ) -> Self { Self { name, hint, tag } } -// } -// -// /// A builder struct for constructing commands. -// #[ derive( Debug ) ] -// pub struct CommandBuilder< T > -// { -// state : T, -// commands : Vec< Command >, -// handlers : std::collections::HashMap< String, Routine >, -// } -// -// impl< T > CommandBuilder< T > -// { -// /// Constructs a `CommandBuilder` with the given state. -// pub fn with_state( state : T ) -> Self -// { -// Self { state, handlers : < _ >::default(), commands : vec![] } -// } -// } -// -// #[ derive( Debug ) ] -// pub struct Builder< F > -// { -// handler : F, -// command : Command, -// } -// -// impl< F > Builder< F > -// { -// /// Creates a new instance of the command with the provided handler function. -// /// -// /// This method takes in a handler function `handler` and creates a new instance of the command. -// /// The `handler` function is used to handle the execution logic associated with the command. -// /// -// /// # Arguments -// /// -// /// * `handler` - The handler function that will be invoked when the command is executed. -// /// -// /// # Returns -// /// -// /// A new instance of the command with the specified `handler`. -// /// -// #[ inline ] -// pub fn new( handler: F ) -> Self -// { -// let name = -// { -// use iter_tools::Itertools as _; -// -// let name = std::any::type_name::< F >(); -// let name = name.split("::").last().unwrap(); -// name.split( '_' ).join( "." ) -// }; -// -// Self { handler, command : Command::former().phrase( name ).form() } -// } -// -// /// Adds an argument to the command. -// /// -// /// This method takes in the `hint` and `tag` parameters to create a `ValueDescription` object -// /// representing an argument. The `ValueDescription` object is then appended to the command's -// /// `subjects` collection. -// /// -// /// # Arguments -// /// -// /// * `hint` - The hint for the argument, represented as a string slice (`&str`). -// /// * `tag` - The type of the argument, represented by a `Type` object from the `Type` module. -// /// -// /// # Returns -// /// -// /// The modified command instance with the argument added. -// /// -// #[ inline ] -// pub fn arg( mut self, hint : &str, tag : Type ) -> Self -// { -// self.command.subjects.push( grammar::command::ValueDescription -// { -// hint : hint.into(), -// kind : tag, -// optional : false, -// }); -// -// self -// } -// -// /// Adds a property to the command. -// /// -// /// This method takes in the `name`, `hint`, and `kind` parameters to create a `ValueDescription` -// /// object representing a property. The `ValueDescription` object is then inserted into the -// /// command's properties collection using the `name` as the key. -// /// -// /// # Example -// /// ```no_rust -// /// let ca = cui(()) -// /// .command(user.property("name", "Name property", Type::String)) -// /// .build(); -// /// ``` -// /// -// /// # Arguments -// /// -// /// * `name` - The name of the property. It should implement the `ToString` trait. -// /// * `hint` - The hint for the property. It should implement the `ToString` trait. -// /// * `kind` - The type of the property, represented by a `Type` object from the `Type` module. -// /// -// /// # Returns -// /// -// /// The modified command instance with the property added. -// /// -// #[ inline ] -// pub fn property( mut self, name : impl ToString , hint : impl ToString, kind : Type ) -> Self -// { -// self.command.properties.insert -// ( -// name.to_string(), -// grammar::command::ValueDescription -// { -// hint : hint.to_string(), -// kind, -// optional : false, -// } -// ); -// -// self -// } -// -// /// Adds multiple properties to the command. -// /// -// /// This method takes in an array of `Property` objects and adds them to the command's properties. -// /// The properties are provided in the `properties` parameter as an array of length `N`. -// /// -// /// ```without_std -// /// let ca = cui(()) -// /// .properties([ -// /// Property::new("name", "Name property", Type::String), -// /// Property::new("age", "Age property", Type::Integer), -// /// ]).build(); -// /// ``` -// /// -// /// # Arguments -// /// -// /// * `properties` - An array of `Property` objects representing the properties to be added. -// /// -// /// # Returns -// /// -// /// The modified command instance with the properties added. -// /// -// #[ inline ] -// pub fn properties< const N: usize >( mut self, properties : [ Property< '_ >; N ] ) -> Self -// { -// self.command.properties.reserve( properties.len() ); -// -// for Property { name, hint, tag } in properties -// { -// self = self.property(name, hint, tag); -// } -// -// self -// } -// } -// -// impl< T: Clone + 'static > CommandBuilder< T > -// { -// /// Adds a command to the `CommandBuilder`. -// /// ```no_rust -// /// let ca = cui( () ) // Add commands using the builder pattern -// /// .command( command ) -// /// .command( command2 ) -// /// .command( echo.arg("string", Type::String ) ) // Customize your commands by chaining methods such as properties -// /// // property, and arg to add properties and arguments. -// /// .build(); -// /// -// /// ``` -// pub fn command< F, E > -// ( -// mut self, -// command : impl IntoBuilder< F, T >, -// ) -> Self -// where -// F : Fn( T, Args, Props ) -> Result< (), E > + 'static + Copy, -// E : fmt::Debug, -// { -// let Builder { handler, command } = command.into_builder(); -// let state = self.state.clone(); -// -// let closure = closure::closure!( | ( args, props ) | -// { -// handler( state.clone(), args, props ) -// .map_err( | report | BasicError::new( format!( "{report:?}" ) ).into() ) -// }); -// -// let handler = Routine::new( closure ); -// -// self.handlers.insert( command.phrase.clone(), handler ); -// self.commands.push( command ); -// -// self -// } -// -// /// Builds and returns a `wca::CommandsAggregator` instance. -// /// -// /// This method finalizes the construction of the `CommandBuilder` by -// /// creating a `wca::CommandsAggregator` instance with the accumulated -// /// commands and handlers. -// pub fn build( self ) -> CommandsAggregator -// { -// CommandsAggregator::former().grammar( self.commands ).executor( self.handlers ).perform() -// } -// } -// -// /// An extension trait for commands. -// /// -// /// This trait provides additional methods for enhancing commands, such as -// /// adding arguments and properties. -// pub trait CommandExt< T > : Sized -// { -// /// Adds an argument to the command. -// fn arg( self, hint : &str, tag : Type ) -> Builder< Self > -// { -// Builder::new( self ).arg( hint, tag ) -// } -// -// /// Adds property to the command. -// fn property< const N: usize >( self, name : impl ToString , hint : impl ToString, kind : Type ) -> Builder< Self > -// { -// Builder::new( self ).property( name, hint, kind ) -// } -// -// /// Adds properties to the command. -// fn properties< const N: usize >( self, properties: [ Property< '_ >; N ] ) -> Builder< Self > -// { -// Builder::new( self ).properties( properties ) -// } -// } -// -// impl< F: Fn( T, Args, Props ) -> Result< (), E>, T, E > CommandExt< T > for F {} -// -// /// A trait for converting a type into a `Builder`. -// pub trait IntoBuilder< F, T > : Sized -// { -// /// Converts the type into a `Builder` instance. -// fn into_builder( self ) -> Builder< F >; -// } -// -// impl< F, T > IntoBuilder< F, T > for Builder< F > -// { -// fn into_builder( self ) -> Self -// { -// self -// } -// } -// -// impl< F: Fn( T, Args, Props ) -> Result< (), E >, T, E > IntoBuilder< F, T > for F -// { -// fn into_builder( self ) -> Builder< F > -// { -// Builder::new( self ) -// } -// } -// -// } -// -// crate::mod_interface! -// { -// exposed use cui; -// exposed use CommandBuilder; -// exposed use Property; -// prelude use IntoBuilder; -// prelude use CommandExt; -// } diff --git a/module/move/wca/src/ca/formatter.rs b/module/move/wca/src/ca/formatter.rs index 6a22e4821f..1c606532d7 100644 --- a/module/move/wca/src/ca/formatter.rs +++ b/module/move/wca/src/ca/formatter.rs @@ -5,6 +5,7 @@ mod private use crate::*; use iter_tools::Itertools; use ca::aggregator::Order; + use grammar::Dictionary; /// Enum representing the format options for generating help content. /// diff --git a/module/move/wca/src/ca/grammar/command.rs b/module/move/wca/src/ca/grammar/command.rs index b536037277..e1bc974d63 100644 --- a/module/move/wca/src/ca/grammar/command.rs +++ b/module/move/wca/src/ca/grammar/command.rs @@ -4,10 +4,11 @@ mod private #[ allow( clippy::wildcard_imports ) ] use crate::*; - use std::collections::{ HashMap }; + use std::collections::HashMap; use indexmap::IndexMap; use former::{ Former, StoragePreform }; use iter_tools::Itertools; + use executor::{ Routine, Handler }; /// A description of a Value in a command. Used to specify the expected type and provide a hint for the Value. /// @@ -37,7 +38,7 @@ mod private pub struct PropertyDescription { name : String, - // qqq : how to re-use ValueDescriptionFormer without additional end? + // xxx : how to re-use ValueDescriptionFormer without additional end? // #[subform_scalar] // value : ValueDescription, /// providing guidance to the user for entering a valid value @@ -76,7 +77,7 @@ mod private /// # Example: /// /// ``` - /// # use wca::{ Command, Type }; + /// # use wca::{ grammar::Command, Type }; /// let command = Command::former() /// .hint( "hint" ) /// .long_hint( "long_hint" ) @@ -105,7 +106,8 @@ mod private /// Map of aliases. // Aliased key -> Original key pub properties_aliases : HashMap< String, String >, - // qqq : make it usable and remove default(?) + // aaa : make it usable and remove default(?) + // aaa : it is usable /// The type `Routine` represents the specific implementation of the routine. #[ scalar( setter = false ) ] #[ former( default = Routine::from( Handler::< _, std::convert::Infallible >::from( || { panic!( "No routine available: A handler function for the command is missing" ) } ) ) ) ] @@ -252,8 +254,8 @@ mod private crate::mod_interface! { - exposed use Command; - exposed use CommandFormer; + orphan use Command; + orphan use CommandFormer; own use ValueDescription; own use CommandAsSubformer; @@ -262,4 +264,5 @@ crate::mod_interface! } -// qqq : use orphan instead of exposed for ALL files in the folder, dont use prelude for structs \ No newline at end of file +// aaa : use orphan instead of exposed for ALL files in the folder, dont use prelude for structs +// aaa : done. \ No newline at end of file diff --git a/module/move/wca/src/ca/grammar/dictionary.rs b/module/move/wca/src/ca/grammar/dictionary.rs index e2db9bb8aa..3e8e0389a5 100644 --- a/module/move/wca/src/ca/grammar/dictionary.rs +++ b/module/move/wca/src/ca/grammar/dictionary.rs @@ -6,8 +6,9 @@ mod private use former::Former; use indexmap::IndexMap; use iter_tools::Itertools; + use grammar::Command; - // qqq : `Former` does not handle this situation well + // xxx : `Former` does not handle this situation well // /// A collection of commands. // /// @@ -27,8 +28,6 @@ mod private pub( crate ) order : Order, } - // qqq : IDK how to integrate it into the `CommandsAggregatorFormer` - // impl DictionaryFormer { pub fn command( mut self, command : Command ) -> Self @@ -112,5 +111,5 @@ mod private crate::mod_interface! { - exposed use Dictionary; + orphan use Dictionary; } diff --git a/module/move/wca/src/ca/grammar/types.rs b/module/move/wca/src/ca/grammar/types.rs index 6fe3724a50..6bea357228 100644 --- a/module/move/wca/src/ca/grammar/types.rs +++ b/module/move/wca/src/ca/grammar/types.rs @@ -8,9 +8,6 @@ mod private Display, Formatter }; - // use wtools; - // use wtools::{ error::Result, err }; - // use error::err; use iter_tools::Itertools; /// Available types that can be converted to a `Value` @@ -63,7 +60,7 @@ mod private /// # Example: /// /// ``` - /// # use wca::{ VerifiedCommand, Value, Args, Props }; + /// # use wca::{ VerifiedCommand, Value, executor::{ Args, Props } }; /// # use std::collections::HashMap; /// let command = VerifiedCommand /// { @@ -123,7 +120,7 @@ mod private } Value::List( list ) => { - let list = list.iter().map( std::string::ToString::to_string ).join( "," ); // qqq : don't hardcode ", " find way to get original separator + let list = list.iter().map( std::string::ToString::to_string ).join( "," ); write!( f, "{list}" )?; } } @@ -195,11 +192,13 @@ mod private Self::Bool => Ok( Value::Bool( match value.as_str() { "1" | "true" => true, "0" | "false" => false, _ => return Err( error::untyped::format_err!( "Can not parse bool from `{}`", value ) ) } ) ), Self::List( kind, delimeter ) => { - let values = value + let values: error::untyped::Result< Vec< Value > > = value .split( *delimeter ) .map( | val | kind.try_cast( val.into() ) ) - .collect::< error::untyped::Result< Vec< Value > > >()?; - // qqq : avoid using fish notation whenever possible. review whole crate + .collect(); + let values = values?; + // aaa : avoid using fish notation whenever possible. review whole crate + // aaa : done Ok( Value::List( values ) ) }, } diff --git a/module/move/wca/src/ca/help.rs b/module/move/wca/src/ca/help.rs index efa484ea74..b48a8ed93c 100644 --- a/module/move/wca/src/ca/help.rs +++ b/module/move/wca/src/ca/help.rs @@ -5,8 +5,6 @@ mod private use crate::*; use ca:: { - Command, - Routine, Type, formatter:: { @@ -15,13 +13,17 @@ mod private }, tool::table::format_table, }; + use verifier::VerifiedCommand; + use grammar::{ Command, Dictionary }; + use executor::Routine; use iter_tools::Itertools; use std::rc::Rc; use error::untyped::format_err; use former::Former; - // qqq : for Bohdan : it should transparent mechanist which patch list of commands, not a stand-alone mechanism + // aaa : for Bohdan : it should transparent mechanist which patch list of commands, not a stand-alone mechanism + // aaa : it is /// Enum `LevelOfDetail` specifies the granularity of detail for rendering or processing: #[ derive( Debug, Default, Copy, Clone, PartialEq, Eq ) ] @@ -69,7 +71,9 @@ mod private pub order : Order, } - // qqq : for Barsik : make possible to change properties order + // aaa : for Barsik : make possible to change properties order + // aaa : order option + /// Generates help content as a formatted string based on a given dictionary and options. /// /// This function takes a `Dictionary` of terms or commands and a `HelpGeneratorOptions` @@ -375,7 +379,7 @@ mod private /// /// ``` /// # use wca::ca::help::{ HelpGeneratorOptions, HelpGeneratorFn }; - /// use wca::{ Command, Dictionary }; + /// use wca::grammar::{ Command, Dictionary }; /// /// fn my_help_generator( dictionary : &Dictionary, args : HelpGeneratorOptions< '_ > ) -> String /// { diff --git a/module/move/wca/src/ca/input.rs b/module/move/wca/src/ca/input.rs index f34861e2d8..34d57ba2c9 100644 --- a/module/move/wca/src/ca/input.rs +++ b/module/move/wca/src/ca/input.rs @@ -1,7 +1,6 @@ mod private { - use std::io; - use std::io::Write; + use std::io::{ self, Write }; /// Ask use input from standard input. #[ must_use ] @@ -79,6 +78,6 @@ mod private crate::mod_interface! { exposed use ask; - exposed use Input; - exposed use IntoInput; + orphan use Input; + orphan use IntoInput; } diff --git a/module/move/wca/src/ca/parser/command.rs b/module/move/wca/src/ca/parser/command.rs index af32ed698f..9d75b11655 100644 --- a/module/move/wca/src/ca/parser/command.rs +++ b/module/move/wca/src/ca/parser/command.rs @@ -25,7 +25,7 @@ mod private /// # Example: /// /// ``` - /// # use wca::ParsedCommand; + /// # use wca::parser::ParsedCommand; /// # use std::collections::HashMap; /// ParsedCommand /// { @@ -57,6 +57,6 @@ mod private crate::mod_interface! { - exposed use Program; - exposed use ParsedCommand; + orphan use Program; + orphan use ParsedCommand; } diff --git a/module/move/wca/src/ca/parser/parser.rs b/module/move/wca/src/ca/parser/parser.rs index d9490e5a70..4bee7321d0 100644 --- a/module/move/wca/src/ca/parser/parser.rs +++ b/module/move/wca/src/ca/parser/parser.rs @@ -4,9 +4,20 @@ mod private use crate::*; use std::collections::HashMap; + use parser::{ Program, ParsedCommand }; // use error::{ return_err }; + #[ allow( missing_docs ) ] + #[ derive( Debug, error::typed::Error ) ] + pub enum ParserError + { + #[ error( "Internal Error: {details}" ) ] + InternalError { details: String }, + #[ error( "Unexpected input. Expected: {expected}, found {input}" ) ] + UnexpectedInput { expected: String, input: String }, + } + /// `Parser` is a struct used for parsing data. #[ derive( Debug ) ] pub struct Parser; @@ -24,13 +35,14 @@ mod private /// Returns a `Result` with a `Program` containing the parsed commands if successful, or an error if parsing fails. /// # Errors /// qqq: doc - // qqq : use typed error - pub fn parse< As, A >( &self, args : As ) -> error::untyped::Result< Program< ParsedCommand > > + // aaa : use typed error + // aaa : done. + pub fn parse< As, A >( &self, args : As ) -> Result< Program< ParsedCommand >, ParserError > where As : IntoIterator< Item = A >, A : Into< String >, { - let args = args.into_iter().map( Into::into ).collect::< Vec< _ > >(); + let args: Vec< _ > = args.into_iter().map( Into::into ).collect(); let mut commands = vec![]; let mut i = 0; while i < args.len() @@ -57,18 +69,18 @@ mod private } // returns ParsedCommand and relative position of the last parsed item - // qqq : use typed error - fn parse_command( args : &[ String ] ) -> error::untyped::Result< ( ParsedCommand, usize ) > + // aaa : use typed error + fn parse_command( args : &[ String ] ) -> Result< ( ParsedCommand, usize ), ParserError > { if args.is_empty() { - error::untyped::return_err!( "Unexpected behaviour: Try to parse command without input" ); + return Err( ParserError::InternalError { details: "Try to parse command without input".into() } ); } let mut i = 0; if !Self::valid_command_name( &args[ i ] ) { - error::untyped::return_err!( "Unexpected input: Expected a command, found: `{}`", args[ i ] ); + return Err( ParserError::UnexpectedInput { expected: "command".into(), input: args[ i ].clone() } ); } let name = match args[ i ].strip_prefix( '.' ).unwrap() { @@ -94,8 +106,9 @@ mod private } // returns ( subjects, properties, relative_end_pos ) - // qqq : use typed error - fn parse_command_args( args : &[ String ] ) -> error::untyped::Result< ( Vec< String >, HashMap< String, String >, usize ) > + // aaa : use typed error + // aaa : done + fn parse_command_args( args : &[ String ] ) -> Result< ( Vec< String >, HashMap< String, String >, usize ), ParserError > { let mut i = 0; @@ -128,7 +141,7 @@ mod private // prop: else { - error::untyped::return_err!( "Unexpected input '{}': Detected a possible property key preceding the ':' character. However, no corresponding value was found.", item ); + return Err( ParserError::UnexpectedInput { expected: "property value".into(), input: "end of input".into() } ); } } // prop : value | prop :value @@ -149,11 +162,7 @@ mod private // : else { - error::untyped::return_err! - ( - "Unexpected input '{} :': Detected a possible property key preceding the ':' character. However, no corresponding value was found.", - item, - ); + return Err( ParserError::UnexpectedInput { expected: "property value".into(), input: "end of input".into() } ); } } @@ -163,7 +172,7 @@ mod private } else { - error::untyped::return_err!( "Unexpected input: Expected `command` or `property`, found: `{}`", item ); + return Err( ParserError::UnexpectedInput { expected: "`command` or `property`".into(), input: item.into() } ); } i += 1; } @@ -177,5 +186,6 @@ mod private crate::mod_interface! { - exposed use Parser; + orphan use Parser; + orphan use ParserError; } diff --git a/module/move/wca/src/ca/tool/table.rs b/module/move/wca/src/ca/tool/table.rs index 6b06be46b3..b3bce748d5 100644 --- a/module/move/wca/src/ca/tool/table.rs +++ b/module/move/wca/src/ca/tool/table.rs @@ -82,6 +82,10 @@ mod private .collect() } + #[ derive( Debug, error::typed::Error ) ] + #[ error( "Invalid table" ) ] + pub struct FormatTableError; + /// Formats a table into a readable string representation. /// /// # Arguments @@ -93,15 +97,16 @@ mod private /// * `error::untyped::Result` - A `error::untyped::Result` containing the formatted table as a `String`, or an `Error` if the table is invalid. /// # Errors /// qqq: doc - // qqq : use typed error - pub fn format_table< IntoTable >( table : IntoTable ) -> error::untyped::Result< String > + // aaa : use typed error + // aaa : done + pub fn format_table< IntoTable >( table : IntoTable ) -> Result< String, FormatTableError > where IntoTable : Into< Table >, { let table = table.into(); if !table.validate() { - return Err( error::untyped::format_err!( "Invalid table" ) ); + return Err( FormatTableError ); } let max_lengths = max_column_lengths( &table ); diff --git a/module/move/wca/src/ca/verifier/command.rs b/module/move/wca/src/ca/verifier/command.rs index bd1efe4152..f52d54c897 100644 --- a/module/move/wca/src/ca/verifier/command.rs +++ b/module/move/wca/src/ca/verifier/command.rs @@ -2,13 +2,14 @@ mod private { #[ allow( clippy::wildcard_imports ) ] use crate::*; + use executor::{ Args, Props }; /// Represents a grammatically correct command with a phrase descriptor, a list of command subjects, and a set of command options. /// /// # Example: /// /// ``` - /// # use wca::{ VerifiedCommand, Value, Args, Props }; + /// # use wca::{ VerifiedCommand, Value, executor::{ Args, Props } }; /// # use std::collections::HashMap; /// VerifiedCommand /// { @@ -47,4 +48,5 @@ crate::mod_interface! exposed use VerifiedCommand; } -// qqq : use orphan instead of exposed for ALL files in the folder, dont use prelude for structs \ No newline at end of file +// aaa : use orphan instead of exposed for ALL files in the folder, dont use prelude for structs +// aaa : done. \ No newline at end of file diff --git a/module/move/wca/src/ca/verifier/verifier.rs b/module/move/wca/src/ca/verifier/verifier.rs index 31baee9e2f..db3de37923 100644 --- a/module/move/wca/src/ca/verifier/verifier.rs +++ b/module/move/wca/src/ca/verifier/verifier.rs @@ -3,19 +3,55 @@ mod private #[ allow( clippy::wildcard_imports ) ] use crate::*; - use ca::grammar::command::ValueDescription; - // use former::Former; + use help::{ HelpGeneratorOptions, LevelOfDetail, generate_help_content }; + use grammar::{ Dictionary, Command, command::ValueDescription }; + use executor::{ Args, Props }; use std::collections::HashMap; use indexmap::IndexMap; - // use wtools::{ error, error::Result, err }; - // use error::err; - use ca::help::{ HelpGeneratorOptions, LevelOfDetail, generate_help_content }; + use verifier::VerifiedCommand; + use parser::{ Program, ParsedCommand }; + + #[ allow( missing_docs ) ] + #[ derive( Debug, error::typed::Error ) ] + pub enum VerificationError + { + #[ error + ( + "Command not found. {} {}", + if let Some( phrase ) = name_suggestion { format!( "Maybe you mean `.{phrase}`?" ) } else { "Please use `.` command to see the list of available commands.".into() }, + if let Some( info ) = command_info { format!( "Command info: `{info}`" ) } else { "".into() } + )] + CommandNotFound { name_suggestion: Option< String >, command_info: Option< String > }, + #[ error( "Fail in command `.{command_name}` while processing subjects. {error}" ) ] + Subject { command_name: String, error: SubjectError }, + #[ error( "Fail in command `.{command_name}` while processing properties. {error}" ) ] + Property { command_name: String, error: PropertyError }, + } + + #[ allow( missing_docs ) ] + #[ derive( Debug, error::typed::Error ) ] + pub enum SubjectError + { + #[ error( "Missing not optional subject" ) ] + MissingNotOptional, + #[ error( "Can not identify a subject: `{value}`" ) ] + CanNotIdentify { value: String }, + } + + #[ allow( missing_docs ) ] + #[ derive( Debug, error::typed::Error ) ] + pub enum PropertyError + { + #[ error( "Expected: {description:?}. Found: {input}" ) ] + Cast { description: ValueDescription, input: String }, + } + // xxx /// Converts a `ParsedCommand` to a `VerifiedCommand` by performing validation and type casting on values. /// /// ``` - /// # use wca::{ Command, Type, Verifier, Dictionary, ParsedCommand }; + /// # use wca::{ Type, verifier::Verifier, grammar::{ Dictionary, Command }, parser::ParsedCommand }; /// # use std::collections::HashMap; /// # fn main() -> Result< (), Box< dyn std::error::Error > > /// # { @@ -51,13 +87,15 @@ mod private dictionary : &Dictionary, raw_program : Program< ParsedCommand > ) - -> error::untyped::Result< Program< VerifiedCommand > > - // qqq : use typed error + -> Result< Program< VerifiedCommand >, VerificationError > + // aaa : use typed error + // aaa : done { - let commands = raw_program.commands + let commands: Result< Vec< VerifiedCommand >, VerificationError > = raw_program.commands .into_iter() .map( | n | self.to_command( dictionary, n ) ) - .collect::< error::untyped::Result< Vec< VerifiedCommand > > >()?; + .collect(); + let commands = commands?; Ok( Program { commands } ) } @@ -112,14 +150,15 @@ mod private if Self::is_valid_command_variant( expected_subjects_count, raw_subjects_count, possible_subjects_count ) { Some( variant ) } else { None } } - // qqq : use typed error + // aaa : use typed error + // aaa : done. fn extract_subjects( command : &Command, raw_command : &ParsedCommand, used_properties : &[ &String ] ) -> - error::untyped::Result< Vec< Value > > + Result< Vec< Value >, SubjectError > { let mut subjects = vec![]; - let all_subjects = raw_command + let all_subjects: Vec< _ > = raw_command .subjects.clone().into_iter() .chain ( @@ -127,7 +166,7 @@ mod private .filter( |( key, _ )| !used_properties.contains( key ) ) .map( |( key, value )| format!( "{key}:{value}" ) ) ) - .collect::< Vec< _ > >(); + .collect(); let mut rc_subjects_iter = all_subjects.iter(); let mut current = rc_subjects_iter.next(); @@ -137,21 +176,22 @@ mod private { Some( v ) => v, None if *optional => continue, - _ => return Err( error::untyped::format_err!( "Missing not optional subject" ) ), + _ => return Err( SubjectError::MissingNotOptional ), }; subjects.push( value ); current = rc_subjects_iter.next(); } - if let Some( value ) = current { return Err( error::untyped::format_err!( "Can not identify a subject: `{}`", value ) ) } + if let Some( value ) = current { return Err( SubjectError::CanNotIdentify { value: value.clone() } ) } Ok( subjects ) } - // qqq : use typed error + // aaa : use typed error + // aaa : done. #[ allow( clippy::manual_map ) ] fn extract_properties( command: &Command, raw_command : HashMap< String, String > ) -> - error::untyped::Result< HashMap< String, Value > > + Result< HashMap< String, Value >, PropertyError > { raw_command.into_iter() .filter_map @@ -167,9 +207,9 @@ mod private .map ( |( value_description, key, value )| - value_description.kind.try_cast( value ).map( | v | ( key.clone(), v ) ) + value_description.kind.try_cast( value.clone() ).map( | v | ( key.clone(), v ) ).map_err( | _ | PropertyError::Cast { description: value_description.clone(), input: format!( "{key}: {value}" ) } ) ) - .collect::< error::untyped::Result< HashMap< _, _ > > >() + .collect() } fn group_properties_and_their_aliases< 'a, Ks >( aliases : &'a HashMap< String, String >, used_keys : Ks ) -> Vec< &String > @@ -190,7 +230,7 @@ mod private { reverse_aliases.get( key ).into_iter().flatten().copied().chain( Some( key ) ) }) - .collect::< Vec< _ > >() + .collect() } /// Converts raw command to grammatically correct @@ -200,10 +240,11 @@ mod private /// qqq: doc /// # Panics /// qqq: doc - // qqq : use typed error + // aaa : use typed error + // aaa : done. pub fn to_command( &self, dictionary : &Dictionary, raw_command : ParsedCommand ) -> - error::untyped::Result< VerifiedCommand > + Result< VerifiedCommand, VerificationError > { if raw_command.name.ends_with( '.' ) | raw_command.name.ends_with( ".?" ) { @@ -216,32 +257,31 @@ mod private }); } let command = dictionary.command( &raw_command.name ) - .ok_or_else::< error::untyped::Error, _ > + .ok_or_else::< VerificationError, _ > ( || { #[ cfg( feature = "on_unknown_suggest" ) ] if let Some( phrase ) = Self::suggest_command( dictionary, &raw_command.name ) { - return error::untyped::format_err!( "Command not found. Maybe you mean `.{}`?", phrase ) + return VerificationError::CommandNotFound { name_suggestion: Some( phrase ), command_info: None }; } - error::untyped::format_err!( "Command not found. Please use `.` command to see the list of available commands." ) + VerificationError::CommandNotFound { name_suggestion: None, command_info: None } } )?; let Some( cmd ) = Self::check_command( command, &raw_command ) else { - error::untyped::bail! - ( - "`{}` command with specified subjects not found. Command info: `{}`", - &raw_command.name, - generate_help_content( dictionary, HelpGeneratorOptions::former().for_commands([ dictionary.command( &raw_command.name ).unwrap() ]).command_prefix( "." ).subject_detailing( LevelOfDetail::Detailed ).form() ).strip_suffix( " " ).unwrap() - ); + return Err( VerificationError::CommandNotFound + { + name_suggestion: Some( command.phrase.clone() ), + command_info: Some( generate_help_content( dictionary, HelpGeneratorOptions::former().for_commands([ dictionary.command( &raw_command.name ).unwrap() ]).command_prefix( "." ).subject_detailing( LevelOfDetail::Detailed ).form() ).strip_suffix( " " ).unwrap().into() ), + } ); }; - let properties = Self::extract_properties( cmd, raw_command.properties.clone() )?; + let properties = Self::extract_properties( cmd, raw_command.properties.clone() ).map_err( | e | VerificationError::Property { command_name: cmd.phrase.clone(), error: e } )?; let used_properties_with_their_aliases = Self::group_properties_and_their_aliases( &cmd.properties_aliases, properties.keys() ); - let subjects = Self::extract_subjects( cmd, &raw_command, &used_properties_with_their_aliases )?; + let subjects = Self::extract_subjects( cmd, &raw_command, &used_properties_with_their_aliases ).map_err( | e | VerificationError::Subject { command_name: cmd.phrase.clone(), error: e } )?; Ok( VerifiedCommand { @@ -258,7 +298,8 @@ mod private crate::mod_interface! { - exposed use Verifier; + orphan use Verifier; + orphan use VerificationError; // own use LevelOfDetail; // own use generate_help_content; diff --git a/module/move/wca/tests/inc/adapter.rs b/module/move/wca/tests/inc/adapter.rs deleted file mode 100644 index 33d5cd7e61..0000000000 --- a/module/move/wca/tests/inc/adapter.rs +++ /dev/null @@ -1,44 +0,0 @@ -use super::*; -use the_module::exposed::*; - -tests_impls! -{ - fn simple() - { - fn command( () : (), args : Args, props : Props) -> Result< (), () > - { - Ok( () ) - } - - fn command2( () : (), args : Args, props : Props ) -> Result< (), () > - { - Ok( () ) - } - - fn echo( () : (), args : Args, props : Props ) -> Result< (), () > - { - Ok( () ) - } - - let ca = the_module::cui( () ).command( command ).command( command2 ).command( echo.arg( "string", Type::String ) ).build(); - - a_id!( (), ca.perform( ".command2 .help" ).unwrap() ); - - a_id!( (), ca.perform( ".help command" ).unwrap() ); - a_id!( (), ca.perform( ".help command2" ).unwrap() ); - a_id!( (), ca.perform( ".help help" ).unwrap() ); - - a_id!( (), ca.perform( ".help.command" ).unwrap() ); - a_id!( (), ca.perform( ".help.command2" ).unwrap() ); - a_id!( (), ca.perform( ".help.help" ).unwrap() ); - - a_true!( ca.perform( ".help.help.help" ).is_err() ); - a_true!( ca.perform( ".echo 34" ).is_ok() ); - a_true!( ca.perform( ".echo" ).is_err() ); - } -} - -tests_index! -{ - simple -} diff --git a/module/move/wca/tests/inc/commands_aggregator/basic.rs b/module/move/wca/tests/inc/commands_aggregator/basic.rs index f7019bebf6..6c9ba72c09 100644 --- a/module/move/wca/tests/inc/commands_aggregator/basic.rs +++ b/module/move/wca/tests/inc/commands_aggregator/basic.rs @@ -1,5 +1,14 @@ use super::*; -use the_module::VerifiedCommand; +use the_module:: +{ + parser::Parser, + VerifiedCommand, + CommandsAggregator, + HelpVariants, + Type, + Error, + ValidationError, +}; // @@ -52,8 +61,7 @@ tests_impls! .perform(); a_id!( (), ca.perform( "." ).unwrap() ); - // qqq : this use case is disabled - // a_id!( (), ca.perform( ".cmd." ).unwrap() ); + a_id!( (), ca.perform( ".cmd." ).unwrap() ); } fn error_types() @@ -136,10 +144,10 @@ tests_impls! fn string_subject_with_colon() { - let dictionary = &the_module::Dictionary::former() + let dictionary = &the_module::grammar::Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -150,7 +158,7 @@ tests_impls! ) .perform(); let parser = Parser; - let grammar = the_module::Verifier; + let grammar = the_module::verifier::Verifier; let executor = the_module::Executor::former().form(); let raw_command = parser.parse( [ ".command", "qwe:rty", "nightly:true" ] ).unwrap().commands.remove( 0 ); @@ -163,10 +171,10 @@ tests_impls! fn no_prop_subject_with_colon() { - let dictionary = &the_module::Dictionary::former() + let dictionary = &the_module::grammar::Dictionary::former() .command ( - the_module::Command::former() + the_module::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -177,7 +185,7 @@ tests_impls! .form(); let parser = Parser; - let grammar = the_module::Verifier; + let grammar = the_module::verifier::Verifier; let executor = the_module::Executor::former().form(); let raw_command = parser.parse( [ ".command", "qwe:rty" ] ).unwrap().commands.remove( 0 ); @@ -190,10 +198,10 @@ tests_impls! fn optional_prop_subject_with_colon() { - let dictionary = &the_module::Dictionary::former() + let dictionary = &the_module::grammar::Dictionary::former() .command ( - the_module::Command::former() + the_module::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -205,7 +213,7 @@ tests_impls! .form(); let parser = Parser; - let grammar = the_module::Verifier; + let grammar = the_module::verifier::Verifier; let executor = the_module::Executor::former().form(); let raw_command = parser.parse( [ ".command", "qwe:rty" ] ).unwrap().commands.remove( 0 ); @@ -216,7 +224,8 @@ tests_impls! a_id!( (), executor.command( dictionary, grammar_command ).unwrap() ); } - // qqq : make the following test work + // aaa : make the following test work + // aaa : works fn subject_with_spaces() { let query = "SELECT title, links, MIN( published ) FROM Frames"; diff --git a/module/move/wca/tests/inc/commands_aggregator/callback.rs b/module/move/wca/tests/inc/commands_aggregator/callback.rs index 834426c32d..21910a6560 100644 --- a/module/move/wca/tests/inc/commands_aggregator/callback.rs +++ b/module/move/wca/tests/inc/commands_aggregator/callback.rs @@ -1,5 +1,6 @@ use super::*; use std::sync::{ Arc, Mutex }; +use the_module::CommandsAggregator; #[ test ] fn changes_state_of_local_variable_on_perform() diff --git a/module/move/wca/tests/inc/commands_aggregator/help.rs b/module/move/wca/tests/inc/commands_aggregator/help.rs index 1df2be062e..2ce5a0bca5 100644 --- a/module/move/wca/tests/inc/commands_aggregator/help.rs +++ b/module/move/wca/tests/inc/commands_aggregator/help.rs @@ -1,7 +1,10 @@ -use std::fs::{DirBuilder, File}; -use std::io::Write; -use std::path::Path; -use std::process::{Command, Stdio}; +use std:: +{ + io::Write, + path::Path, + fs::{ DirBuilder, File }, + process::{ Command, Stdio }, +}; pub fn start_sync< AP, Args, Arg, P > ( @@ -11,9 +14,14 @@ pub fn start_sync< AP, Args, Arg, P > ) -> String where AP : AsRef< Path >, Args : IntoIterator< Item = Arg >, Arg : AsRef< std::ffi::OsStr >, P : AsRef< Path >, { let ( application, path ) = ( application.as_ref(), path.as_ref() ); - let args = args.into_iter().map( | a | a.as_ref().into() ).collect::< Vec< std::ffi::OsString > >(); + let args: Vec< std::ffi::OsString > = args.into_iter().map( | a | a.as_ref().into() ).collect(); let child = Command::new( application ).args( &args ).stdout( Stdio::piped() ).stderr( Stdio::piped() ).current_dir( path ).spawn().unwrap(); let output = child.wait_with_output().unwrap(); + + if !output.status.success() + { + println!( "{}", String::from_utf8( output.stderr ).unwrap() ); + } String::from_utf8( output.stdout ).unwrap() } diff --git a/module/move/wca/tests/inc/commands_aggregator/mod.rs b/module/move/wca/tests/inc/commands_aggregator/mod.rs index ca0cdc4b5a..fedda3d681 100644 --- a/module/move/wca/tests/inc/commands_aggregator/mod.rs +++ b/module/move/wca/tests/inc/commands_aggregator/mod.rs @@ -1,16 +1,5 @@ use super::*; -use the_module:: -{ - Parser, - - CommandsAggregator, - HelpVariants, - Type, - Error, - ValidationError, -}; - mod basic; mod callback; mod help; diff --git a/module/move/wca/tests/inc/executor/command.rs b/module/move/wca/tests/inc/executor/command.rs index b1dcf7ac12..e489b90764 100644 --- a/module/move/wca/tests/inc/executor/command.rs +++ b/module/move/wca/tests/inc/executor/command.rs @@ -1,5 +1,15 @@ use super::*; -use the_module::VerifiedCommand; +use the_module:: +{ + parser::Parser, + VerifiedCommand, + executor::Context, Type, + grammar::Dictionary, + verifier::Verifier, + + Executor, + // wtools +}; // @@ -14,7 +24,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -42,7 +52,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -78,7 +88,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -121,7 +131,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "check" ) @@ -137,7 +147,7 @@ tests_impls! ) .form(); let verifier = Verifier; - let mut ctx = wca::Context::new( Mutex::new( 1 ) ); + let mut ctx = wca::executor::Context::new( Mutex::new( 1 ) ); // init executor let executor = Executor::former() .context( ctx ) @@ -160,7 +170,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) diff --git a/module/move/wca/tests/inc/executor/mod.rs b/module/move/wca/tests/inc/executor/mod.rs index 7c84cbf8a3..617cf69b75 100644 --- a/module/move/wca/tests/inc/executor/mod.rs +++ b/module/move/wca/tests/inc/executor/mod.rs @@ -1,17 +1,4 @@ use super::*; -// qqq : rid of global uses in tests -use the_module:: -{ - Parser, - - Context, Type, - Dictionary, - Verifier, - - Executor, - // wtools -}; - mod command; mod program; diff --git a/module/move/wca/tests/inc/executor/program.rs b/module/move/wca/tests/inc/executor/program.rs index de33330259..ef0f63940a 100644 --- a/module/move/wca/tests/inc/executor/program.rs +++ b/module/move/wca/tests/inc/executor/program.rs @@ -1,5 +1,15 @@ use super::*; -use the_module::VerifiedCommand; +use the_module:: +{ + parser::Parser, + VerifiedCommand, + executor::Context, Type, + grammar::Dictionary, + verifier::Verifier, + + Executor, + // wtools +}; // @@ -14,7 +24,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -47,7 +57,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "inc" ) @@ -63,7 +73,7 @@ tests_impls! ) .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "eq" ) @@ -91,7 +101,7 @@ tests_impls! let verifier = Verifier; // starts with 0 - let ctx = wca::Context::new( Mutex::new( 0 ) ); + let ctx = wca::executor::Context::new( Mutex::new( 0 ) ); // init simple executor let executor = Executor::former() .context( ctx ) diff --git a/module/move/wca/tests/inc/grammar/from_command.rs b/module/move/wca/tests/inc/grammar/from_command.rs index 9823236c0c..343cde7ffb 100644 --- a/module/move/wca/tests/inc/grammar/from_command.rs +++ b/module/move/wca/tests/inc/grammar/from_command.rs @@ -1,5 +1,14 @@ use super::*; +use the_module:: +{ + parser::Parser, + + Type, Value, + grammar::Dictionary, + verifier::Verifier, +}; + // tests_impls! @@ -13,7 +22,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -45,7 +54,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -92,7 +101,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -121,7 +130,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -156,7 +165,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -184,7 +193,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -223,7 +232,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -268,7 +277,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -297,7 +306,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -328,7 +337,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) @@ -369,7 +378,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command" ) diff --git a/module/move/wca/tests/inc/grammar/from_program.rs b/module/move/wca/tests/inc/grammar/from_program.rs index 670eaf178c..256fd6dcd9 100644 --- a/module/move/wca/tests/inc/grammar/from_program.rs +++ b/module/move/wca/tests/inc/grammar/from_program.rs @@ -1,5 +1,14 @@ use super::*; +use the_module:: +{ + parser::Parser, + + Type, Value, + grammar::Dictionary, + verifier::Verifier, +}; + // tests_impls! @@ -12,7 +21,7 @@ tests_impls! let dictionary = &Dictionary::former() .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command1" ) @@ -21,7 +30,7 @@ tests_impls! ) .command ( - wca::Command::former() + wca::grammar::Command::former() .hint( "hint" ) .long_hint( "long_hint" ) .phrase( "command2" ) diff --git a/module/move/wca/tests/inc/grammar/mod.rs b/module/move/wca/tests/inc/grammar/mod.rs index 38c94dc114..454495c496 100644 --- a/module/move/wca/tests/inc/grammar/mod.rs +++ b/module/move/wca/tests/inc/grammar/mod.rs @@ -1,12 +1,4 @@ use super::*; -use the_module:: -{ - Parser, - - Type, Value, - Dictionary, - Verifier, -}; mod from_command; mod from_program; diff --git a/module/move/wca/tests/inc/grammar/types.rs b/module/move/wca/tests/inc/grammar/types.rs index 7421fce48f..b04ab6c346 100644 --- a/module/move/wca/tests/inc/grammar/types.rs +++ b/module/move/wca/tests/inc/grammar/types.rs @@ -1,5 +1,5 @@ use super::*; -use wca::TryCast; +use the_module::{ TryCast, Type, Value }; // @@ -134,7 +134,7 @@ tests_impls! let string = Type::List( Type::String.into(), ',' ).try_cast( origin_string.into() ).unwrap(); a_id!( origin_string, string.to_string() ); - // xxx : qqq : that fails now. suggest solution + // xxx clarification is needed : qqq : that fails now. suggest solution // let origin_string = "100;3.14"; // let string = Type::List( Type::Number.into(), ';' ).try_cast( origin_string.into() ).unwrap(); // a_id!( origin_string, string.to_string() ); diff --git a/module/move/wca/tests/inc/mod.rs b/module/move/wca/tests/inc/mod.rs index c2617e9035..b51887947e 100644 --- a/module/move/wca/tests/inc/mod.rs +++ b/module/move/wca/tests/inc/mod.rs @@ -1,10 +1,6 @@ #[ allow( unused_imports ) ] use super::*; -#[ allow( unused_imports ) ] -use the_module::tool::*; -#[ allow( unused_imports ) ] -use std::collections::HashMap; #[ cfg( not( feature = "no_std" ) ) ] mod parser; @@ -15,6 +11,5 @@ mod executor; #[ cfg( not( feature = "no_std" ) ) ] mod commands_aggregator; -// qqq : for Bohdan : why commented out? resolve -// #[ cfg( not( feature = "no_std" ) ) ] -// mod adapter; +// aaa : for Bohdan : why commented out? resolve +// aaa : no longer relevant, so removed diff --git a/module/move/wca/tests/inc/parser/command.rs b/module/move/wca/tests/inc/parser/command.rs index 986ab1d0c0..7f5c1aecf4 100644 --- a/module/move/wca/tests/inc/parser/command.rs +++ b/module/move/wca/tests/inc/parser/command.rs @@ -1,4 +1,5 @@ use super::*; +use the_module::parser::{ ParsedCommand, Parser }; // diff --git a/module/move/wca/tests/inc/parser/mod.rs b/module/move/wca/tests/inc/parser/mod.rs index 456679d11a..617cf69b75 100644 --- a/module/move/wca/tests/inc/parser/mod.rs +++ b/module/move/wca/tests/inc/parser/mod.rs @@ -1,10 +1,4 @@ use super::*; -use wca:: -{ - Program, ParsedCommand, - - Parser, -}; mod command; mod program; diff --git a/module/move/wca/tests/inc/parser/program.rs b/module/move/wca/tests/inc/parser/program.rs index 081f8cc3e8..04b07c322f 100644 --- a/module/move/wca/tests/inc/parser/program.rs +++ b/module/move/wca/tests/inc/parser/program.rs @@ -1,4 +1,5 @@ use super::*; +use the_module::parser::{ Program, ParsedCommand, Parser }; // diff --git a/module/move/willbe/src/action/list.rs b/module/move/willbe/src/action/list.rs index de477fa617..5bd66e940c 100644 --- a/module/move/willbe/src/action/list.rs +++ b/module/move/willbe/src/action/list.rs @@ -465,17 +465,32 @@ mod private .package_find_by_manifest( manifest_file ) .ok_or_else( || format_err!( "Package not found in the workspace" ) ) .err_with_report( report )?; + let version = if args.info.contains( &PackageAdditionalInfo::Version ) + { + Some( package.version().to_string() ) + } + else + { + None + }; + let crate_dir = if args.info.contains( &PackageAdditionalInfo::Path ) + { + Some( package.crate_dir() ).transpose() + } + else + { + Ok( None ) + } + .err_with_report( report )?; let mut package_report = tool::ListNodeReport { name : package.name().to_string(), - // qqq : for Bohdan : too long lines - version : if args.info.contains( &PackageAdditionalInfo::Version ) { Some( package.version().to_string() ) } else { None }, - // qqq : for Bohdan : don't put multiline if into struct constructor - crate_dir : if args.info.contains( &PackageAdditionalInfo::Path ) - { Some( package.crate_dir() ).transpose() } - else - { Ok( None ) } - .err_with_report( report )?, + // aaa : for Bohdan : too long lines + // aaa : moved out + version, + // aaa : for Bohdan : don't put multiline if into struct constructor + // aaa : moved out + crate_dir, duplicate : false, normal_dependencies : vec![], dev_dependencies : vec![], diff --git a/module/move/willbe/src/action/test.rs b/module/move/willbe/src/action/test.rs index 76d832a893..2e23df5108 100644 --- a/module/move/willbe/src/action/test.rs +++ b/module/move/willbe/src/action/test.rs @@ -79,7 +79,7 @@ mod private // -> Result< TestsReport, ( TestsReport, Error ) > { - // qqq : incapsulate progress bar logic into some function of struct. don't keep it here + // aaa : incapsulate progress bar logic into some function of struct. don't keep it here // aaa : done let mut report = TestsReport::default(); @@ -173,7 +173,7 @@ Try to install it with `rustup install {}` command(-s)", ).err_with_report( &report )?; println!( "{plan}" ); - // aaa : split on two functions for create plan and for execute + // aaa : split on two functions for create plan and for execute // aaa : it's already separated, look line: 203 : let result = tests_run( &options ); let temp_path = if temp diff --git a/module/move/willbe/src/command/list.rs b/module/move/willbe/src/command/list.rs index 7198b4ee70..d474c313b1 100644 --- a/module/move/willbe/src/command/list.rs +++ b/module/move/willbe/src/command/list.rs @@ -102,10 +102,10 @@ mod private Ok( () ) } - impl TryFrom< wca::Props > for ListProperties + impl TryFrom< wca::executor::Props > for ListProperties { type Error = error::untyped::Error; - fn try_from( value : wca::Props ) -> Result< Self, Self::Error > + fn try_from( value : wca::executor::Props ) -> Result< Self, Self::Error > { let mut this = Self::former(); diff --git a/module/move/willbe/src/command/publish.rs b/module/move/willbe/src/command/publish.rs index 0bf4a45d1d..5b3afd8930 100644 --- a/module/move/willbe/src/command/publish.rs +++ b/module/move/willbe/src/command/publish.rs @@ -103,10 +103,10 @@ mod private } } - impl TryFrom< wca::Props > for PublishProperties + impl TryFrom< wca::executor::Props > for PublishProperties { type Error = error::untyped::Error; - fn try_from( value : wca::Props ) -> Result< Self, Self::Error > + fn try_from( value : wca::executor::Props ) -> Result< Self, Self::Error > { let mut this = Self::former(); diff --git a/module/move/willbe/src/command/publish_diff.rs b/module/move/willbe/src/command/publish_diff.rs index c123b09791..a35b453b2e 100644 --- a/module/move/willbe/src/command/publish_diff.rs +++ b/module/move/willbe/src/command/publish_diff.rs @@ -57,10 +57,10 @@ mod private Ok( () ) } - impl TryFrom< wca::Props > for PublishDiffProperties + impl TryFrom< wca::executor::Props > for PublishDiffProperties { type Error = error::untyped::Error; - fn try_from( value : wca::Props ) -> Result< Self, Self::Error > + fn try_from( value : wca::executor::Props ) -> Result< Self, Self::Error > { let mut this = Self::former(); diff --git a/module/move/willbe/src/command/test.rs b/module/move/willbe/src/command/test.rs index ab58380216..118f44f8d6 100644 --- a/module/move/willbe/src/command/test.rs +++ b/module/move/willbe/src/command/test.rs @@ -147,10 +147,10 @@ Set at least one of them to true." ); } } - impl TryFrom< wca::Props > for TestsProperties + impl TryFrom< wca::executor::Props > for TestsProperties { type Error = error::untyped::Error; - fn try_from( value : wca::Props ) -> Result< Self, Self::Error > + fn try_from( value : wca::executor::Props ) -> Result< Self, Self::Error > { let mut this = Self::former(); @@ -192,4 +192,4 @@ crate::mod_interface! { /// run tests in specified crate exposed use test; -} \ No newline at end of file +} diff --git a/module/move/willbe/src/command/workspace_renew.rs b/module/move/willbe/src/command/workspace_renew.rs index 06a00ac80f..a77254accd 100644 --- a/module/move/willbe/src/command/workspace_renew.rs +++ b/module/move/willbe/src/command/workspace_renew.rs @@ -37,11 +37,11 @@ mod private .context( "Fail to create workspace" ) } - impl TryFrom< wca::Props > for WorkspaceNewProperties + impl TryFrom< wca::executor::Props > for WorkspaceNewProperties { type Error = error::untyped::Error; - fn try_from( value : wca::Props ) -> std::result::Result< Self, Self::Error > + fn try_from( value : wca::executor::Props ) -> std::result::Result< Self, Self::Error > { let mut this = Self::former(); diff --git a/module/move/willbe/src/entity/dependency.rs b/module/move/willbe/src/entity/dependency.rs index 48fba3b5a7..128a946061 100644 --- a/module/move/willbe/src/entity/dependency.rs +++ b/module/move/willbe/src/entity/dependency.rs @@ -274,8 +274,13 @@ mod private } DependenciesSort::Topological => { - // qqq : too long line - graph::toposort( graph::construct( &graph ) ).map_err( | err | format_err!( "{}", err ) )?.into_iter().filter( | x | x != &root ).collect() + // aaa : too long line + // aaa : splited + graph::toposort( graph::construct( &graph ) ) + .map_err( | err | format_err!( "{}", err ) )? + .into_iter() + .filter( | x | x != &root ) + .collect() }, }; diff --git a/module/move/willbe/src/entity/files/either.rs b/module/move/willbe/src/entity/files/either.rs index 34565ae615..0caa927f4f 100644 --- a/module/move/willbe/src/entity/files/either.rs +++ b/module/move/willbe/src/entity/files/either.rs @@ -17,7 +17,7 @@ use std:: // Result, // }; -/// Wrapper over `data_type::Either< CrateDir, ManifestFile >` with utils methods. +/// Wrapper over `data_type::Either< CrateDir, ManifestFile >` with util methods. #[ derive( Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug ) ] pub struct EitherDirOrFile( data_type::Either< CrateDir, ManifestFile > ); diff --git a/module/move/willbe/src/entity/test.rs b/module/move/willbe/src/entity/test.rs index 7cadfd83ca..4ec5fe190d 100644 --- a/module/move/willbe/src/entity/test.rs +++ b/module/move/willbe/src/entity/test.rs @@ -40,12 +40,12 @@ mod private /// Represents the optimization setting for the test variant. optimization : optimization::Optimization, /// Contains additional features or characteristics of the test variant. - features : collection::BTreeSet, + features : collection::BTreeSet< String >, } impl fmt::Display for TestVariant { - fn fmt( &self, f : &mut fmt::Formatter< '_ >) -> fmt::Result + fn fmt( &self, f : &mut fmt::Formatter< '_ > ) -> fmt::Result { let features = if self.features.is_empty() { " ".to_string() } else { self.features.iter().join( " " ) }; writeln!( f, "{} {} {}", self.optimization, self.channel, features )?; @@ -62,7 +62,7 @@ mod private impl fmt::Display for TestPlan { - fn fmt( &self, f : &mut fmt::Formatter< '_ >) -> std::fmt::Result + fn fmt( &self, f : &mut fmt::Formatter< '_ > ) -> std::fmt::Result { writeln!( f, "Plan: " )?; for plan in &self.packages_plan diff --git a/module/move/willbe/src/tool/template.rs b/module/move/willbe/src/tool/template.rs index db5fce6c94..c8ac11af89 100644 --- a/module/move/willbe/src/tool/template.rs +++ b/module/move/willbe/src/tool/template.rs @@ -182,7 +182,7 @@ mod private { /// Extracts template values from props for parameters required for this template. #[ must_use ] - pub fn values_from_props( &self, props : &wca::Props ) -> TemplateValues + pub fn values_from_props( &self, props : &wca::executor::Props ) -> TemplateValues { let values = self.descriptors .iter() diff --git a/module/move/willbe/tests/inc/action_tests/test.rs b/module/move/willbe/tests/inc/action_tests/test.rs index 16a4e8cd6a..67c926cb89 100644 --- a/module/move/willbe/tests/inc/action_tests/test.rs +++ b/module/move/willbe/tests/inc/action_tests/test.rs @@ -1,21 +1,13 @@ use super::*; -// use the_module::*; // qqq : for Bohdan : bad. don't import the_module::* use inc::helper:: { ProjectBuilder, WorkspaceBuilder, - // BINARY_NAME, }; use collection::BTreeSet; -// use std:: -// { -// fs::{ self, File }, -// io::Write, -// }; -// use path::{ Path, PathBuf }; use assert_fs::TempDir; use the_module::action::test::{ test, TestsCommandOptions }; diff --git a/module/move/willbe/tests/inc/mod.rs b/module/move/willbe/tests/inc/mod.rs index ca9dbda05d..0b456a9b87 100644 --- a/module/move/willbe/tests/inc/mod.rs +++ b/module/move/willbe/tests/inc/mod.rs @@ -14,6 +14,8 @@ mod action_tests; mod helper; +mod package; + // aaa : for Petro : for Bohdan : for Nikita : sort out test files to be consistent with src files // sorted diff --git a/module/move/willbe/tests/inc/package.rs b/module/move/willbe/tests/inc/package.rs index 8a5fb2a2f0..bc83b38d89 100644 --- a/module/move/willbe/tests/inc/package.rs +++ b/module/move/willbe/tests/inc/package.rs @@ -1,3 +1,310 @@ +use std::*; +use std::io::Write; + +use crate::the_module::{ action, channel, package }; + +enum Dependency +{ + Normal { name: String, path: Option< path::PathBuf >, is_macro: bool }, + Dev { name: String, path: Option< path::PathBuf >, is_macro: bool }, +} + +impl Dependency +{ + fn as_toml( &self ) -> String + { + match self + { + Dependency::Normal { name, path, is_macro } if !is_macro => + if let Some( path ) = path + { + format!( "[dependencies.{name}]\npath = \"../{}\"", path.display().to_string().replace( "\\", "/" ) ) + } + else + { + format!( "[dependencies.{name}]\nversion = \"*\"" ) + } + Dependency::Normal { name, .. } => format!( "[dependencies.{name}]\nworkspace = true" ), + Dependency::Dev { name, path, is_macro } if !is_macro => + if let Some( path ) = path + { + format!( "[dev-dependencies.{name}]\npath = \"../{}\"", path.display().to_string().replace( "\\", "/" ) ) + } + else + { + format!( "[dev-dependencies.{name}]\nversion = \"*\"" ) + } + Dependency::Dev { name, .. } => format!( "[dev-dependencies.{name}]\nworkspace = true" ), + } + } +} + +struct TestPackage +{ + name: String, + dependencies: Vec< Dependency >, + path: Option< path::PathBuf >, +} + +impl TestPackage +{ + pub fn new( name: impl Into< String > ) -> Self + { + Self { name: name.into(), dependencies: vec![], path: None } + } + + pub fn dependency( mut self, name: impl Into< String > ) -> Self + { + self.dependencies.push( Dependency::Normal { name: name.into(), path: None, is_macro: false } ); + self + } + + pub fn macro_dependency( mut self, name: impl Into< String > ) -> Self + { + self.dependencies.push( Dependency::Normal { name: name.into(), path: None, is_macro: true } ); + self + } + + pub fn dev_dependency( mut self, name: impl Into< String > ) -> Self + { + self.dependencies.push( Dependency::Dev { name: name.into(), path: None, is_macro: false } ); + self + } + + pub fn macro_dev_dependency( mut self, name: impl Into< String > ) -> Self + { + self.dependencies.push( Dependency::Dev { name: name.into(), path: None, is_macro: true } ); + self + } + + pub fn create( &mut self, path: impl AsRef< path::Path > ) -> io::Result< () > + { + let path = path.as_ref().join( &self.name ); + + () = fs::create_dir_all( path.join( "src" ) )?; + () = fs::write( path.join( "src" ).join( "lib.rs" ), &[] )?; + + let cargo = format! + ( + r#"[package] +name = "{}" +version = "0.1.0" +edition = "2021" +{}"#, + self.name, + self.dependencies.iter().map( Dependency::as_toml ).fold( String::new(), | acc, d | + { + format!( "{acc}\n\n{d}" ) + }) + ); + () = fs::write( path.join( "Cargo.toml" ), cargo.as_bytes() )?; + + self.path = Some( path ); + + Ok( () ) + } +} + +impl Drop for TestPackage +{ + fn drop( &mut self ) + { + if let Some( path ) = &self.path + { + _ = fs::remove_dir_all( path ).ok(); + } + } +} + +struct TestWorkspace +{ + packages: Vec< TestPackage >, + path: path::PathBuf, +} + +impl TestWorkspace +{ + fn new( path: impl AsRef< path::Path > ) -> io::Result< Self > + { + let path = path.as_ref(); + () = fs::create_dir_all( path )?; + + let cargo = r#"[workspace] +resolver = "2" +members = [ + "members/*", +] +"#; + () = fs::write( path.join( "Cargo.toml" ), cargo.as_bytes() )?; + + Ok(Self { packages: vec![], path: path.into() }) + } + + fn find( &self, package_name: impl AsRef< str > ) -> Option< &TestPackage > + { + let name = package_name.as_ref(); + self.packages.iter().find( | p | p.name == name ) + } + + fn with_package( mut self, mut package: TestPackage ) -> io::Result< Self > + { + let mut macro_deps = collections::HashMap::new(); + for dep in &mut package.dependencies + { + match dep + { + Dependency::Normal { name, is_macro, .. } if *is_macro => + { + if let Some( package ) = self.find( &name ) + { + if let Some( path ) = &package.path + { + macro_deps.insert( name.clone(), path.clone() ); + continue; + } + } + eprintln!( "macro dependency {} not found. required for {}", name, package.name ); + } + Dependency::Normal { name, path, .. } => + { + if let Some( package ) = self.find( &name ) + { + if let Some( real_path ) = &package.path + { + let real_path = real_path.strip_prefix( self.path.join( "members" ) ).unwrap_or( real_path ); + *path = Some( real_path.into() ); + } + } + } + Dependency::Dev { name, is_macro, .. } if *is_macro => + { + if let Some( package ) = self.find( &name ) + { + if let Some( path ) = &package.path + { + macro_deps.insert( name.clone(), path.clone() ); + continue; + } + } + eprintln!( "macro dev-dependency {} not found. required for {}", name, package.name ); + } + Dependency::Dev { name, path, .. } => + { + if let Some( package ) = self.find( &name ) + { + if let Some( real_path ) = &package.path + { + let real_path = real_path.strip_prefix( self.path.join( "members" ) ).unwrap_or( real_path ); + *path = Some( real_path.into() ); + } + } + } + } + } + let mut cargo = fs::OpenOptions::new().append( true ).open( self.path.join( "Cargo.toml" ) )?; + for ( name, _ ) in macro_deps + { + writeln!( cargo, + r#"[workspace.dependencies.{name}] +version = "*" +path = "members/{name}""#, + )?; + } + package.create( self.path.join( "members" ) )?; + self.packages.push( package ); + + Ok( self ) + } + + fn with_packages( mut self, packages: impl IntoIterator< Item = TestPackage > ) -> io::Result< Self > + { + for package in packages { self = self.with_package( package )?; } + + Ok( self ) + } +} + +impl Drop for TestWorkspace +{ + fn drop( &mut self ) + { + _ = fs::remove_dir_all( &self.path ).ok(); + } +} + +#[ test ] +fn kos_plan() +{ + let tmp_folder = env::temp_dir().join( "publish_plan_kos_plan" ); + _ = fs::remove_dir_all( &tmp_folder ).ok(); + + let workspace = TestWorkspace::new( tmp_folder ).unwrap() + .with_packages( + [ + TestPackage::new( "a" ), + TestPackage::new( "b" ).dependency( "a" ), + TestPackage::new( "c" ).dependency( "a" ), + TestPackage::new( "d" ).dependency( "a" ), + TestPackage::new( "e" ).dependency( "b" ).macro_dev_dependency( "c" ),//.macro_dependency( "c" ), + ]).unwrap(); + let the_patterns: Vec< String > = workspace + .packages + .iter() + .flat_map( | p | p.path.as_ref().map( | p | p.to_string_lossy().into_owned() ) ) + .collect(); + dbg!(&the_patterns); + + let plan = action::publish_plan + ( + the_patterns, + channel::Channel::Stable, + false, + false, + true, + false, + ) + .unwrap(); + + let queue: Vec< &package::PackageName > = plan.plans.iter().map( | i | &i.package_name ).collect(); + dbg!(&queue); + + // We don’t consider dev dependencies when constructing the project graph, which results in this number of variations. + // If you'd like to modify this behavior, please check `entity/workspace_graph.rs` in the `module_dependency_filter`. + let expected_one_of= + [ + [ "a", "b", "d", "c", "e" ], + [ "a", "b", "c", "d", "e" ], + [ "a", "d", "b", "c", "e" ], + [ "a", "c", "b", "d", "e" ], + [ "a", "d", "c", "b", "e" ], + [ "a", "c", "d", "b", "e" ], + [ "a", "b", "d", "e", "c" ], + [ "a", "d", "b", "e", "c" ], + [ "a", "b", "e", "d", "c" ], + [ "a", "e", "b", "d", "c" ], + [ "a", "d", "e", "b", "c" ], + [ "a", "e", "d", "b", "c" ], + [ "a", "b", "c", "e", "d" ], + [ "a", "c", "b", "e", "d" ], + [ "a", "b", "e", "c", "d" ], + [ "a", "e", "b", "c", "d" ], + [ "a", "c", "e", "b", "d" ], + [ "a", "e", "c", "b", "d" ], + ]; + + let mut fail = true; + 'sequences: for sequence in expected_one_of + { + for index in 0 .. 5 + { + if *queue[ index ] != sequence[ index ].to_string().into() { continue 'sequences; } + } + fail = false; + break; + } + assert!( !fail ); +} + // use super::*; // use the_module:: // {