colon_db 0.1.2

rust databasing key value storing with multiple columns crate
Documentation
use indexmap::IndexMap;
use std::fs::{OpenOptions, File};
use std::io::{self, Write, BufReader, BufRead};
use std::path::Path;
use thiserror::Error;
use anyhow::{Context, Result};




#[derive(Error, Debug)]
pub enum DataStoreError {

    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),

    #[error("the data for key `{0}` is not available")]
    Redaction(String),

    #[error("header length doesnt match row length")]
    InvalidRowLength,

    #[error("unknown data store error")]
    Unknown,
     
    #[error("could not find column and/or key in database")]
    CoordinatesNotFound,   

    #[error("invalid selection range (expected {expected:?}, found {found:?})")]
    InvalidSelectionRange {
        expected: String,
        found: String,
    },

    #[error("tried to load data from a filename that does not exists")]
    Filename,

    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader {
        expected: String,
        found: String,
    },

    #[error("duplicate column name `{0}`")]
    DuplicateColumn(String),
}




pub struct ColonDB {
    pub data: IndexMap<String, Vec<String>>, 
    filename: String, 
}




impl ColonDB {

    pub fn find_database(file_name: &str) -> Self {
        let mut data_base = ColonDB {
            data: IndexMap::new(),
            filename: file_name.to_string(),
        };
        let _ = data_base.load_data_from_file();
        data_base
    }


    fn column_name_toindex(&self, column: &str, header: &[String]) -> Option<usize> {
        if column == "key" {
            return Some(0); 
        }

        header.iter().position(|col| col == column).map(|idx| idx + 1)
    }


    fn load_data_from_file(&mut self) -> Result<(), DataStoreError> {
        if !Path::new(&self.filename).exists() {
            return Err(DataStoreError::Filename);
        }
        let file = File::open(&self.filename).expect("Couldn't open File...");
        let file_reader = BufReader::new(file); 

        for row in file_reader.lines() {
            if let Ok(entry) = row {
                let sep_keyvalue: Vec<&str> = entry.splitn(2, ':').collect(); 
                
                if sep_keyvalue.len() == 2 {
                    let key = sep_keyvalue[0].to_string();
                    let values_str = sep_keyvalue[1];
                    let values: Vec<String> = values_str.split(',').map(|s| s.to_string()).collect();
                    
                    self.data.insert(key, values);
                }
            }
        }
    return Ok(())
    }

    
    pub fn save_data_to_file(&self) -> Result<()> {
       
        let mut file = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .open(&self.filename)
            .with_context(|| format!("Failed to open file for writing: {}", &self.filename))?;
        
        for (key, values) in &self.data {
            let values_str = values.join(",");
            writeln!(file, "{}:{}", key, values_str)
                .with_context(|| format!("Failed to write key-values pair to file: {}:{}", key, values_str))?;
        }
        return Ok(())
    }


    pub fn set_header(&mut self, key: String, values: Vec<String>) -> Result<(), DataStoreError> {
        if values.is_empty() {
            return Err(DataStoreError::InvalidHeader {
                expected: "non-empty values".to_string(),
                found: "empty vector".to_string(),
            });
        }

        if self.data.contains_key(&key) {
            self.data.remove(&key);
        }

        let mut new_data = IndexMap::new();
        new_data.insert(key.clone(), values);

        for (existing_key, existing_values) in &self.data {
            new_data.insert(existing_key.clone(), existing_values.clone());
        }

        self.data = new_data;

        self.save_data_to_file();
        Ok(())
    }


    pub fn insert_item_into_db(
        &mut self, 
        key: String, 
        column: String, 
        value: String
    ) -> Result<(), DataStoreError> {
        let header: Vec<String> = self.data.values().next().cloned().unwrap_or_default();
        let index = match self.column_name_toindex(&column, &header) {
            Some(idx) => idx,
            None => {
                println!("Column '{}' not found in header!", column);
                return Err(DataStoreError::CoordinatesNotFound);
            }
        };

        let row = self.data.entry(key).or_insert_with(|| vec![String::new(); header.len()]);

        if index-1 < row.len() {
            row[index-1] = value;
        } else {
            println!("Index out of bounds: column '{}'", column);
            return Err(DataStoreError::CoordinatesNotFound);
        }

        self.save_data_to_file();
        Ok(())
    }


    pub fn append_row_to_db(
        &mut self, 
        key: String, 
        entry_vec: Vec<String>
        ) -> Result<(), DataStoreError> {

        self.data.insert(key, entry_vec);
        let _ = self.save_data_to_file();
        return Ok(())
    }

    
    pub fn delete_row(&mut self, key: &str) -> Result<(), DataStoreError>  {
        if self.data.remove(key).is_some() {
            let _ = self.save_data_to_file();
        } else {
            eprintln!("Key '{}' not found in database!", key);
            return Err(DataStoreError::CoordinatesNotFound)
        }
        return Ok(())
    }
    

    pub fn get_item(
        &self, 
        key: &str, 
        column: &str 
    ) -> Result<String, DataStoreError> {

        let header: Vec<String> = self.data.values().next().cloned().unwrap_or_default();
        
        if let Some(index) = self.column_name_toindex(column, &header) {
            if let Some(row) = self.data.get(key) {
                if index < row.len() {
                    return Ok(row[index].clone());
                } else {
                    eprintln!("Key '{}' not found in database!", key);
                    return Err(DataStoreError::CoordinatesNotFound)
                }

            } else {
                eprintln!("Column '{}' not found in header!", column);
                return Err(DataStoreError::CoordinatesNotFound)
            }
        }
        Ok("".to_string())
    }
    

    pub fn append_column(&mut self, column_name: String, default_value: String) -> Result<(), DataStoreError> {
        let mut header = self.data.values().next().cloned().unwrap_or_default();
        
        if header.contains(&column_name) {
            return Err(DataStoreError::DuplicateColumn(column_name));
        }

        header.push(column_name.clone());
        
        for row in self.data.values_mut() {
            row.push(default_value.clone());
        }

        let mut comp_data = self.data.clone();

        if let Some((key, _)) = comp_data.iter_mut().next() {
            self.data.insert(key.clone(), header);
        }

        self.save_data_to_file();

        Ok(())
    }


    pub fn select_data(
        &self,
        row_range: Option<std::ops::Range<usize>>,
        column_range: Option<Vec<String>>,
    ) -> Result<ColonDB, DataStoreError> {
        let header: Vec<String> = self.data.values().next().cloned().unwrap_or_default();
        let mut result = Vec::new();

        let col_range = column_range.clone();
        let column_indices: Vec<Option<usize>> = if let Some(columns) = column_range {
            columns
                .iter()
                .map(|column| self.column_name_toindex(column, &header))
                .collect()
        } else {
            (0..header.len()).map(Some).collect()
        };

        if column_indices.contains(&None) {
            let missing_columns: Vec<String> = col_range
                .unwrap_or_default()
                .into_iter()
                .filter(|column| self.column_name_toindex(column, &header).is_none())
                .collect();

            return Err(DataStoreError::InvalidSelectionRange {
                expected: format!("Valid column names from header: {:?}", header),
                found: format!("Missing columns: {:?}", missing_columns),
            });
        }

        let column_indices: Vec<usize> = column_indices
            .into_iter()
            .flatten()
            .filter(|&index| index > 0) 
            .map(|index| index - 1)
            .collect();


        let rows: Vec<(String, Vec<String>)> = self
            .data
            .iter()
            .map(|(key, row)| (key.clone(), row.clone()))
            .collect();

        let row_range = row_range.unwrap_or(0..rows.len());

        if row_range.end > rows.len() {
            return Err(DataStoreError::InvalidSelectionRange {
                expected: format!("row range within 0..{}", rows.len()),
                found: format!("end of range {}", row_range.end),
            });
        }

        for (key, row) in rows.iter().skip(row_range.start).take(row_range.len()) {
            let selected_columns: Vec<String> = column_indices
                .iter()
                .filter_map(|&index| row.get(index).cloned())
                .collect();

            result.push((key.clone(), selected_columns));
        }

        let mut new_db = ColonDB {
            data: IndexMap::new(),
            filename: self.filename.clone(),
        };

        for (key, row) in result {
            new_db.data.insert(key, row);
        }

        Ok(new_db)
    }

    
    pub fn print_database(&self) {
        for (key, row) in &self.data {
            let mut line = format!("{key} || "); 
            
            for value in row.iter() {
                if value.is_empty() {
                    line.push_str("empty | "); 
                } else {
                    line.push_str(&format!("{value} | ")); 
                }
            }
            
            println!("{}", line);
            
            let separator = "-".repeat(line.len()); 
            println!("{}", separator);
        }
    }
}