nts 0.1.0

A set of utilities
Documentation
pub mod cli {
    #[cfg(feature = "cli-table")]
    pub mod table {
        use std::fmt;
        use std::fmt::Formatter;

        pub struct Table {
            headers: Vec<String>,
            rows: Vec<Vec<String>>,
        }

        impl Table {
            const CELL_PADDING: usize = 2;
            const ROW_OUTER_BORDERS: usize = 2;

            pub fn new(headers: Vec<String>, data: Vec<Vec<String>>) -> Result<Self, &'static str> {
                if data.iter().any(|row| row.len() != headers.len()) {
                    return Err("Rows should have same length as headers");
                }

                Ok(Table {
                    headers,
                    rows: data,
                })
            }

            fn all_rows(&self) -> Vec<Vec<String>> {
                let mut result = Vec::new();
                result.push(self.headers.to_owned());
                self.rows.iter().for_each(|row| result.push(row.to_owned()));
                result
            }

            fn row_width(&self) -> usize {
                let result = self.row_inner_width();
                result + Self::ROW_OUTER_BORDERS
            }

            fn row_inner_width(&self) -> usize {
                let mut result = 0;
                for column_index in 0..self.headers.len() {
                    result = result + self.cell_width(column_index) + Self::CELL_PADDING;
                }
                result + self.row_inner_borders()
            }

            fn row_inner_borders(&self) -> usize {
                self.headers.len() - 1
            }

            fn cell_width(&self, column: usize) -> usize {
                self.all_rows()
                    .iter()
                    .map(|row| row.get(column).expect("column out of bounds"))
                    .map(|cell| cell.chars().count())
                    .max()
                    .unwrap_or(1)
            }

            fn format_row(&self, row_content: &Vec<String>) -> String {
                let mut row = String::new();
                let mut column_index = 0;
                for cell_content in row_content {
                    if column_index == 0 {
                        row.push('#');
                    }
                    row.push(' ');
                    row.push_str(cell_content);
                    row.push_str(&self.filling(column_index, cell_content.chars().count()));
                    row.push(' ');

                    column_index = column_index + 1;

                    if column_index == row_content.len() {
                        row.push('#');
                    } else {
                        row.push('|');
                    }
                }

                format!("{row}\n")
            }

            fn filling(&self, cell_index: usize, content_len: usize) -> String {
                " ".repeat(self.cell_width(cell_index) - content_len)
            }
        }

        impl fmt::Display for Table {
            fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
                let outer_border = &format!("{}\n", &"#".repeat(self.row_width()));
                let header_row_border = &format!("#{}#\n", &"=".repeat(self.row_inner_width()));
                let row_border = &format!("#{}#\n", &"-".repeat(self.row_inner_width()));

                let mut result = Ok(());
                result = result.and(formatter.write_str(outer_border));

                result = result.and(formatter.write_str(&self.format_row(&self.headers)));
                result = result.and(formatter.write_str(header_row_border));

                for row_index in 0..self.rows.len() {
                    let row = self.rows.get(row_index).expect("Programming error: wrong loop index");
                    result = result.and(formatter.write_str(&self.format_row(&row)));

                    if row_index == self.rows.len() - 1 {
                        result = result.and(formatter.write_str(outer_border));
                    } else {
                        result = result.and(formatter.write_str(row_border));
                    }
                }

                result
            }
        }

        #[cfg(test)]
        mod tests {
            use super::*;

            #[test]
            fn table_creation_is_successful() {
                let headers = vec!["Name".to_string(), "Age".to_string()];
                let data = vec![
                    vec!["Alice".to_string(), "25".to_string()],
                    vec!["Bob".to_string(), "30".to_string()],
                ];

                let table = Table::new(headers, data).unwrap();
                assert_eq!(table.headers, vec!["Name".to_string(), "Age".to_string()]);
                assert_eq!(table.rows.len(), 2);
                assert_eq!(table.rows[0], vec!["Alice".to_string(), "25".to_string()]);
                assert_eq!(table.rows[1], vec!["Bob".to_string(), "30".to_string()]);
            }

            #[test]
            fn table_formats_well() {
                let headers = vec!["Name".to_string(), "Age".to_string()];
                let data = vec![
                    vec!["Alice".to_string(), "25".to_string()],
                    vec!["Bob".to_string(), "30".to_string()],
                ];

                let table = Table::new(headers, data).unwrap();
                let display_output = format!("{}", table);

                assert_eq!(display_output, "###############\n\
                                            # Name  | Age #\n\
                                            #=============#\n\
                                            # Alice | 25  #\n\
                                            #-------------#\n\
                                            # Bob   | 30  #\n\
                                            ###############\n");
            }
        }
    }
}