Doc grid generation

This commit is contained in:
AlexanderHD27
2025-05-25 19:17:33 +02:00
parent 877cf71413
commit dccb09cefe
10 changed files with 803 additions and 110 deletions

54
src/configotron/conf.rs Normal file
View File

@@ -0,0 +1,54 @@
use serde::{Deserialize, Serialize};
use std::{fs};
#[derive(Deserialize, Serialize, Debug)]
pub enum UserConfigItem {
String {
name: String,
max_length: u64
},
IPv4 {
name: String,
},
Boolean {
name: String
},
Number {
name: String,
max: Option<i32>,
min: Option<i32>
},
Submenu(UserConfigData)
}
#[derive(Deserialize, Serialize, Debug)]
pub struct UserConfigData {
pub name: String,
pub config_items: Vec<UserConfigItem>
}
#[derive(thiserror::Error, Debug)]
pub enum ConfigotronError {
#[error("IO Operation failed: {}", ._0.to_string())]
IoError(#[from] std::io::Error),
#[error("Failed to parse YAML File: {}", ._0.to_string())]
YamlParsingError(#[from] serde_yml::Error),
#[error("Missing field {} (line {} col {}) {}", .missing_filed, .line, .col, .context)]
MissingFieldError{
col: i32,
line: i32,
missing_filed: Box<str>,
context: Box<str>
}
}
pub fn read_and_parse_yaml_file(input_file: &str) -> Result<UserConfigData, ConfigotronError> {
let file_content = fs::read_to_string(&input_file)?;
let config: UserConfigData = serde_yml::from_str(&file_content)?;
return Ok(config);
}

575
src/configotron/doc/grid.rs Normal file
View File

@@ -0,0 +1,575 @@
use std::{fmt::format, usize};
use crate::configotron::{conf::{UserConfigData, UserConfigItem}, doc::grid};
#[derive(Clone, PartialEq, Debug)]
pub enum DocGridCellType{
Boarder,
Empty,
SubMenu { name: String },
Item { name: String },
ItemJoin
}
impl DocGridCellType {
fn to_debug_string(&self) -> String {
match self {
DocGridCellType::Empty => "E".to_string(),
DocGridCellType::Item { .. } => "I".to_string(),
DocGridCellType::ItemJoin => "i".to_string(),
DocGridCellType::SubMenu { .. } => "S".to_string(),
DocGridCellType::Boarder => "B".to_string()
}
}
}
pub enum ResultingGridCell {
Grid(DocGrid),
Cell(DocGridCellType)
}
fn user_config_item_to_grid_cells(item: &UserConfigItem) -> ResultingGridCell {
match item {
UserConfigItem::String { name, max_length } => ResultingGridCell::Cell(DocGridCellType::Item { name: name.clone() }),
UserConfigItem::IPv4 { name } => ResultingGridCell::Cell(DocGridCellType::Item { name: name.clone() }),
UserConfigItem::Boolean { name } => ResultingGridCell::Cell(DocGridCellType::Item { name: name.clone() }),
UserConfigItem::Number { name, max, min } => ResultingGridCell::Cell(DocGridCellType::Item { name: name.clone() }),
UserConfigItem::Submenu(user_config_data) => ResultingGridCell::Grid(DocGrid::from(user_config_data)),
}
}
#[derive(Clone)]
pub struct DocGrid {
rows: usize,
cols: usize,
array_cells: Vec<DocGridCellType>
}
impl DocGrid {
pub fn new() -> Self {
Self { rows: 0, cols: 2, array_cells: Vec::new() }
}
pub fn len(&self) -> usize {
self.cols * self.rows
}
pub fn get_row(&self) -> usize {
self.rows
}
pub fn set_title(&mut self, cell: DocGridCellType) {
self.array_cells.resize(self.len() + self.cols, DocGridCellType::Empty);
self.array_cells[0] = cell;
}
pub fn append_item(&mut self, item: ResultingGridCell) {
match item {
ResultingGridCell::Cell(cell_type) => {
self.array_cells.resize(self.len() + self.cols, DocGridCellType::Empty);
self.array_cells[self.rows*self.cols + 1] = cell_type;
self.rows += 1;
},
ResultingGridCell::Grid(grid) => {
let new_col_size = std::cmp::max(self.cols, grid.cols + 1);
let new_row_size = self.rows + grid.rows;
self.array_cells.resize(new_col_size * new_row_size, DocGridCellType::Empty);
let row_offset = self.rows;
let old_cols = self.cols;
self.cols = new_col_size;
self.rows = new_row_size;
for row_index in (0..new_row_size).rev() {
for col_index in (0..new_col_size).rev() {
let new_index = row_index * new_col_size + col_index;
if col_index < old_cols && row_index < self.rows {
// Reshuffel Stuff Old Stuff
let old_index = row_index * old_cols + col_index;
self.array_cells[new_index] = self.array_cells[old_index].clone();
} else {
// Make Cells left to inset empty
self.array_cells[new_index] = DocGridCellType::Empty;
}
}
}
// Copy in new stuff
for row_index in 0..grid.rows {
for col_index in 0..grid.cols {
let (target_row_index, target_col_index) = (
row_index + row_offset,
col_index + 1
);
let target_index = target_col_index + target_row_index * self.cols;
let src_index = row_index * grid.cols + col_index;
self.array_cells[target_index] = grid.array_cells[src_index].clone();
}
}
}
};
}
pub fn to_debug_string(&self) -> String {
let mut s = String::new();
for row_index in 0..self.rows {
s.push_str("| ");
for col_index in 0..self.cols {
s.push_str(&format!("{} ", self.array_cells[row_index * self.cols + col_index].to_debug_string()));
}
s.push_str("|\n")
}
s
}
}
impl From<&UserConfigData> for DocGrid {
fn from(value: &UserConfigData) -> Self {
let mut res = DocGrid::new();
res.set_title(DocGridCellType::SubMenu { name: value.name.clone() });
value.config_items.iter().for_each(|i| {
res.append_item(user_config_item_to_grid_cells(i));
});
// Add ItemJoins
for row_index in 0..res.rows {
for col_index in 2..res.cols {
let index = col_index + row_index * res.cols;
match res.array_cells[col_index-1 + row_index * res.cols] {
DocGridCellType::Item { .. } => {
res.array_cells[index] = DocGridCellType::ItemJoin;
},
DocGridCellType::ItemJoin => {
res.array_cells[index] = DocGridCellType::ItemJoin;
}
_ => {}
}
}
}
res
}
}
#[derive(Clone)]
pub enum DocGridLineType {
NONE, VERTICAL, HORIZONTAL
}
#[derive(Clone)]
pub enum DocGridLineCrossType {
NONE, FULL, VERTICAL, HORIZONTAL
}
pub struct DocGridLayout {
grid: DocGrid,
lines_hort: Vec<DocGridLineType>,
lines_vert: Vec<DocGridLineType>,
lines_cross: Vec<DocGridLineCrossType>,
}
impl DocGridLayout {
fn get_vert_line (&self, r: i32, c: i32) -> &DocGridLineType {
if r < 0 || r >= (self.grid.rows as i32) || c < 0 || c > (self.grid.cols as i32) {
&DocGridLineType::NONE
} else {
&self.lines_vert[r as usize * (self.grid.cols + 1) + c as usize]
}
}
fn set_vert_line(&mut self, r: usize, c: usize, v: DocGridLineType) {
self.lines_vert[r as usize * (self.grid.cols + 1) + c as usize] = v;
}
fn get_hort_line (&self, r: i32, c: i32) -> &DocGridLineType {
if r < 0 || r > (self.grid.rows as i32) || c < 0 || c >= (self.grid.cols as i32) {
&DocGridLineType::NONE
} else {
&self.lines_hort[(r as usize) * self.grid.cols + (c as usize)]
}
}
fn set_hort_line(&mut self, r: usize, c: usize, v: DocGridLineType) {
self.lines_hort[r as usize * (self.grid.cols) + c as usize] = v;
}
fn get_cross (&self, r: i32, c: i32) -> &DocGridLineCrossType {
if r < 0 || r > (self.grid.rows as i32) || c < 0 || c > (self.grid.cols as i32) {
&DocGridLineCrossType::NONE
} else {
&self.lines_cross[(r as usize) * (self.grid.cols + 1) + (c as usize)]
}
}
fn set_cross (&mut self, r: usize, c: usize, v: DocGridLineCrossType) {
self.lines_cross[r * (self.grid.cols + 1) + c] = v;
}
pub fn new<FLineV, FLineH, FCross>(grid: &DocGrid, vertical_mapper: FLineV, horizontal_mapper: FLineH, cross_mapper: FCross) -> DocGridLayout
where FLineV: Fn(&DocGridCellType, &DocGridCellType) -> DocGridLineType,
FLineH: Fn(&DocGridCellType, &DocGridCellType) -> DocGridLineType,
FCross: Fn(&DocGridLineType, &DocGridLineType, &DocGridLineType, &DocGridLineType) -> DocGridLineCrossType
{
let mut lines_hort = Vec::new();
let mut lines_vert = Vec::new();
let mut crosses = Vec::new();
lines_hort.resize(grid.cols * (1 + grid.rows), DocGridLineType::NONE);
lines_vert.resize((grid.cols + 1) * grid.rows , DocGridLineType::NONE );
crosses.resize((grid.rows + 1) * (grid.cols + 1), DocGridLineCrossType::NONE);
let mut res: DocGridLayout = DocGridLayout {
grid: grid.clone(),
lines_cross: crosses,
lines_hort: lines_hort,
lines_vert: lines_vert
};
let get_cell = |r: i32, c: i32| -> &DocGridCellType {
if r < 0 || (r as usize) >= grid.rows || c < 0 || (c as usize) >= grid.cols {
&DocGridCellType::Boarder
} else {
&grid.array_cells[r as usize * grid.cols + c as usize]
}
};
// Calculating Lines Horizontal lines
for row in 0..(grid.rows + 1) {
for col in 0..grid.cols {
let top = get_cell(row as i32 - 1 , col as i32);
let bottom = get_cell(row as i32 , col as i32);
res.set_hort_line(row, col, horizontal_mapper(top, bottom));
}
}
// Calculating Lines Vertical lines
for row in 0..grid.get_row() {
for col in 0..(grid.cols + 1) {
let left = get_cell(row as i32, col as i32 - 1);
let right = get_cell(row as i32 , col as i32);
res.set_vert_line(row, col, vertical_mapper(left, right));
}
}
// Calculating Crossings
for row in 0..(grid.rows + 1) {
for col in 0..(grid.cols + 1) {
let north = res.get_vert_line((row as i32) - 1, col as i32);
let south = res.get_vert_line(row as i32, col as i32);
let east = res.get_hort_line(row as i32, col as i32);
let west = res.get_hort_line(row as i32, (col as i32) - 1);
res.set_cross(row, col, cross_mapper(north, east, south, west));
}
}
res
}
pub fn to_debug_string(&self) -> String {
let mut s = String::new();
fn map_line_to_char(l: &DocGridLineType) -> String {
match l {
DocGridLineType::HORIZONTAL => "-".to_string(),
DocGridLineType::NONE => " ".to_string(),
DocGridLineType::VERTICAL => "|".to_string()
}
}
fn map_cross_to_chr(c: &DocGridLineCrossType) -> String {
match c {
DocGridLineCrossType::NONE => " ".to_string(),
DocGridLineCrossType::FULL => "+".to_string(),
DocGridLineCrossType::VERTICAL => "|".to_string(),
DocGridLineCrossType::HORIZONTAL => "-".to_string(),
}
}
for r in 0..self.grid.rows {
for c in 0..self.grid.cols {
s.push_str(&format!("{}{}",
&map_cross_to_chr(self.get_cross(r as i32, c as i32)),
&map_line_to_char(self.get_hort_line(r as i32, c as i32))
));
}
s.push_str(&format!("{}\n", &map_cross_to_chr(self.get_cross(r as i32, self.grid.cols as i32))));
for c in 0..self.grid.cols {
s.push_str(&format!("{}{}",
&map_line_to_char(self.get_vert_line(r as i32, c as i32)),
&self.grid.array_cells[self.grid.cols * r + c].to_debug_string()
));
}
s.push_str(&format!("{}\n",
&map_line_to_char(self.get_vert_line(r as i32, self.grid.cols as i32))));
};
for c in 0..self.grid.cols {
s.push_str(&format!("{}{}",
&map_cross_to_chr(self.get_cross(self.grid.rows as i32, c as i32)),
&map_line_to_char(self.get_hort_line(self.grid.rows as i32, c as i32))
));
}
s.push_str(&format!("{}\n", &map_cross_to_chr(self.get_cross(self.grid.rows as i32, self.grid.cols as i32))));
s
}
}
mod DocGridLayoutMappers {
use super::{DocGridLineType, DocGridLineCrossType, DocGridCellType};
pub fn vert_line_ascii(left: &DocGridCellType, right: &DocGridCellType) -> DocGridLineType {
use DocGridCellType::{ItemJoin, Item};
match (left, right) {
(ItemJoin, Item { .. }) => DocGridLineType::NONE,
(Item { .. }, ItemJoin) => DocGridLineType::NONE,
(_, _) => DocGridLineType::VERTICAL
}
}
pub fn hort_line_ascii(top: &DocGridCellType, bottom: &DocGridCellType) -> DocGridLineType {
use DocGridCellType::{Boarder, Empty, ItemJoin, Item, SubMenu};
match (top, bottom) {
(Empty, Empty) => DocGridLineType::NONE,
// Sub Menu "Joins"
(Empty, SubMenu { .. }) => DocGridLineType::HORIZONTAL,
(SubMenu { .. }, Empty) => DocGridLineType::NONE,
(SubMenu { .. }, SubMenu { .. }) => DocGridLineType::HORIZONTAL,
// Horizontal Rules
(Boarder, _) => DocGridLineType::HORIZONTAL,
(_, Boarder) => DocGridLineType::HORIZONTAL,
(Item { .. }, _) => DocGridLineType::HORIZONTAL,
(_, Item { .. }) => DocGridLineType::HORIZONTAL,
(ItemJoin, _) => DocGridLineType::HORIZONTAL,
(_, ItemJoin) => DocGridLineType::HORIZONTAL,
}
}
pub fn cross_ascii(n: &DocGridLineType, e: &DocGridLineType, s: &DocGridLineType, w: &DocGridLineType) -> DocGridLineCrossType{
use DocGridLineType::{NONE, HORIZONTAL, VERTICAL};
match (n, e, s, w) {
(VERTICAL, NONE, VERTICAL, NONE) => DocGridLineCrossType::VERTICAL,
(NONE, HORIZONTAL, NONE, HORIZONTAL) => DocGridLineCrossType::HORIZONTAL,
(_, _, _, _) => DocGridLineCrossType::FULL,
}
}
}
pub fn create_ascii_doc_string(grid: &DocGrid) -> String {
let layout = DocGridLayout::new(grid,
DocGridLayoutMappers::vert_line_ascii,
DocGridLayoutMappers::hort_line_ascii,
DocGridLayoutMappers::cross_ascii
);
return layout.to_debug_string();
}
#[cfg(test)]
mod tests {
use crate::configotron::{conf::{UserConfigData, UserConfigItem}};
use super::{DocGrid, DocGridCellType, ResultingGridCell};
// Helper functino for creating elements for testing
fn create_item() -> UserConfigItem {
UserConfigItem::Boolean { name: "Test".to_string() }
}
fn create_submenu(v: Vec<UserConfigItem>) -> UserConfigItem {
UserConfigItem::Submenu(UserConfigData { name: "SubMenu".to_string(), config_items: v })
}
fn create_menu(v: Vec<UserConfigItem>) -> UserConfigData {
UserConfigData { name: "TestMenu".to_string(), config_items: v }
}
// Actuall Test
#[test]
fn test_grid_extend() {
let mut grid = DocGrid::new();
grid.set_title(DocGridCellType::SubMenu { name: "name".to_string() });
grid.append_item(ResultingGridCell::Cell(DocGridCellType::Item { name: "name".to_string() }));
grid.append_item(ResultingGridCell::Cell(DocGridCellType::ItemJoin));
grid.append_item(ResultingGridCell::Cell(DocGridCellType::Item { name: "name".to_string() }));
grid.append_item(ResultingGridCell::Cell(DocGridCellType::Item { name: "name".to_string() }));
grid.append_item(ResultingGridCell::Cell(DocGridCellType::ItemJoin));
grid.append_item(ResultingGridCell::Cell(DocGridCellType::SubMenu { name: "name".to_string() }));
assert_eq!(grid.to_debug_string(), r#"| S I |
| E i |
| E I |
| E I |
| E i |
| E S |
"#);
}
#[test]
fn test_grid_assembly_from_data() {
let data = create_menu(vec![
create_item(),
create_item(),
create_submenu(vec![
create_item(),
create_item(),
create_item(),
]),
create_item(),
create_item(),
create_item()
]);
let grid = DocGrid::from(&data);
assert_eq!(grid.to_debug_string(), r#"| S I i |
| E I i |
| E S I |
| E E I |
| E E I |
| E I i |
| E I i |
| E I i |
"#);
}
#[test]
fn test_grid_deep_assembly_from_data() {
let data = create_menu(vec![
create_submenu(vec![
create_submenu(vec![
create_submenu(vec![
create_item()
]),
]),
]),
]);
let grid = DocGrid::from(&data);
assert_eq!(grid.to_debug_string(), r#"| S S S S I |
"#);
}
#[test]
fn test_grid_complex_assembly_from_data() {
let data = create_menu(vec![
create_item(),
create_item(),
create_submenu(vec![
create_item(),
create_item(),
create_submenu(vec![
create_item(),
create_item(),
create_submenu(vec![
create_item(),
create_item(),
]),
create_item(),
]),
create_item(),
create_submenu(vec![
create_item(),
create_item(),
]),
create_item(),
create_item(),
]),
create_item(),
create_item(),
]);
let grid = DocGrid::from(&data);
println!("{}", grid.to_debug_string());
assert_eq!(grid.to_debug_string(), r#"| S I i i i |
| E I i i i |
| E S I i i |
| E E I i i |
| E E S I i |
| E E E I i |
| E E E S I |
| E E E E I |
| E E E I i |
| E E I i i |
| E E S I i |
| E E E I i |
| E E I i i |
| E E I i i |
| E I i i i |
| E I i i i |
"#);
}
mod ascii_docs {
use crate::configotron::{conf::{UserConfigData, UserConfigItem}, doc::grid::create_ascii_doc_string};
use super::{DocGrid, create_item, create_menu, create_submenu};
#[test]
fn ascii_docs_simple() {
let data = create_menu(vec![
create_item(),
create_item(),
create_submenu(vec![
create_item(),
create_item(),
create_item(),
]),
create_item(),
create_item(),
create_item()
]);
let grid = DocGrid::from(&data);
println!("{}", grid.to_debug_string());
let s = create_ascii_doc_string(&grid);
println!("{}", s);
assert_eq!(s, r#"+-+---+
|S|I i|
| +---+
|E|I i|
| +-+-+
|E|S|I|
| | +-+
|E|E|I|
| | +-+
|E|E|I|
| +-+-+
|E|I i|
| +---+
|E|I i|
| +---+
|E|I i|
+-+---+
"#);
}
}
}

View File

@@ -0,0 +1 @@
pub mod grid;

2
src/configotron/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod conf;
pub mod doc;

View File

@@ -1,111 +1,7 @@
use serde::{Deserialize, Serialize};
use serde_yml;
mod configotron;
use std::fs;
use pad::PadStr;
#[derive(Deserialize, Serialize, Debug)]
enum UserConfigItem {
String {
name: String,
max_length: u64
},
IPv4 {
name: String,
},
Boolean {
name: String
},
Number {
name: String,
max: Option<i32>,
min: Option<i32>
},
Submenu(UserConfigData)
}
#[derive(Deserialize, Serialize, Debug)]
struct UserConfigData {
name: String,
config_items: Vec<UserConfigItem>
}
#[derive(thiserror::Error, Debug)]
pub enum ConfigotronError {
#[error("IO Operation failed: {}", ._0.to_string())]
IoError(#[from] std::io::Error),
#[error("Failed to parse YAML File: {}", ._0.to_string())]
YamlParsingError(#[from] serde_yml::Error),
#[error("Missing field {} (line {} col {}) {}", .missing_filed, .line, .col, .context)]
MissingFieldError{
col: i32,
line: i32,
missing_filed: Box<str>,
context: Box<str>
}
}
fn read_and_parse_yaml_file(input_file: &str) -> Result<UserConfigData, ConfigotronError> {
let file_content = fs::read_to_string(&input_file)?;
let config: UserConfigData = serde_yml::from_str(&file_content)?;
return Ok(config);
}
fn generate_text_report_from_item(item: &UserConfigItem) -> Vec<String> {
return match item {
UserConfigItem::Boolean {name} => vec![String::from(name), String::from("[y/n]")] ,
UserConfigItem::IPv4 { name } => vec![String::from(name), format!("[ipv4]")],
UserConfigItem::String { name, max_length } => vec![String::from(name), format!("[text, max length {}]", max_length)],
UserConfigItem::Number { name, min, max } => match (min, max) {
(None, None) => vec![String::from(name), String::from("[number]")],
(None, Some(maximum)) => vec![String::from(name), format!("[number, ..{}]", maximum)],
(Some(minmum), None) => vec![String::from(name), format!("[number, {}..]", minmum)],
(Some(minmum), Some(maximum)) => vec![String::from(name), format!("[number, {}..{}]", minmum, maximum)],
},
UserConfigItem::Submenu(c) => generate_text_report(c)
}
}
fn generate_text_report(config: &UserConfigData) -> Vec<String> {
let lines: Vec<Vec<String>> = config.config_items
.iter()
.map(|i| generate_text_report_from_item(i))
.collect();
let block_width = lines
.iter()
.map(|l: &Vec<String>|
l
.iter()
.map(|s: &String| s.chars().count())
.max().unwrap_or(0)
)
.max().unwrap_or(0);
let line_seperator = format!("+{}+", "-".repeat(block_width + 2));
let config_name_width = config.name.chars().count() + 2;
let mut lines_flattend: Vec<String> = lines
.iter()
.map(|i| i
.iter()
.map(|s| format!("| {} |", s.pad_to_width(block_width)))
.collect::<Vec<String>>()
)
.flat_map(|seg| [seg, vec![String::from(&line_seperator)]])
.flatten()
.into_iter()
.map(|e| format!("{}{}", " ".repeat(config_name_width), e))
.collect();
lines_flattend.insert(0, format!("{}{}", config.name.pad_to_width(config_name_width), line_seperator));
return lines_flattend;
}
use configotron::conf::{read_and_parse_yaml_file};
use configotron::doc::grid::{DocGrid};
fn main() {
@@ -116,8 +12,9 @@ fn main() {
return;
}
};
let res = generate_text_report(&data).join("\n");
println!("{}", res);
println!("Hello, world!");
let grid = DocGrid::from(&data);
println!("{}", grid.to_debug_string());
println!("Done!");
}