use crate::store::model::{FieldKind, Model}; use indexmap::map::IndexMap; use json_dotpath::DotPaths; use serde::Serialize; use serde_json::Value; use std::collections::HashMap; use std::collections::{BTreeMap, BTreeSet}; use std::fs::{File, OpenOptions}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; pub mod form; pub mod model; /// Store instance #[derive(Debug)] pub struct Store { path: PathBuf, pub model: Model, freeform_fields: FreeformFieldsOfInterest, pub data: Cards, pub index: Indexes, } /// Indexes loaded from the indexes file #[derive(Serialize, Deserialize, Debug, Default)] pub struct Indexes { pub free_enums: HashMap>, pub free_tags: HashMap>, } /// Struct loaded from the repositroy config file #[derive(Deserialize, Debug)] struct RepositoryConfig { pub model: Model, } #[derive(Serialize, Deserialize, Debug)] pub struct Cards { #[serde(default)] pub cards: BTreeMap, #[serde(default)] pub counter: usize, } const REPO_CONFIG_FILE: &'static str = "repository.yaml"; const REPO_DATA_FILE: &'static str = "data.json"; const REPO_INDEX_FILE: &'static str = "index.json"; impl Store { pub fn new(path: impl AsRef) -> Self { let file = load_file(path.as_ref().join(REPO_CONFIG_FILE)); let repository_config: RepositoryConfig = serde_yaml::from_str(&file).expect("Error parsing repository config file."); let items = load_file_or(path.as_ref().join(REPO_DATA_FILE), "{}"); let indexes = load_file_or(path.as_ref().join(REPO_INDEX_FILE), "{}"); Store { path: path.as_ref().into(), freeform_fields: Self::get_fields_for_freeform_indexes(&repository_config.model), model: repository_config.model, data: serde_json::from_str(&items).expect("Error parsing data file."), index: serde_json::from_str(&indexes).unwrap_or_default(), } } /// Handle a data change. /// If a card was modified, selectively update the tags index fn on_change(&mut self, changed_card: Option) { if let Some(id) = changed_card { // this needs to be so ugly because of lifetimes - we need a mutable reference to the index Self::index_card( &mut self.index, &self.freeform_fields, self.data.cards.get(&id).unwrap(), ); } self.persist(); } pub fn persist(&mut self) { let data_json = serde_json::to_string_pretty(&self.data).expect("Error serialize data"); write_file(self.path.join(REPO_DATA_FILE), data_json.as_bytes()); let index_json = serde_json::to_string_pretty(&self.index).expect("Error serialize index"); write_file(self.path.join(REPO_INDEX_FILE), index_json.as_bytes()); } pub fn add_card(&mut self, values: IndexMap) -> usize { let packed = serde_json::to_value(values).expect("Error serialize"); if let p @ Value::Object(_) = packed { let id = self.data.counter; self.data.counter += 1; self.data.cards.insert(id, p); self.on_change(Some(id)); id } else { panic!("Packing did not produce a map."); } } pub fn update_card(&mut self, id: usize, values: IndexMap) { let packed = serde_json::to_value(values).expect("Error serialize"); if !self.data.cards.contains_key(&id) { panic!("No such card."); } if let p @ Value::Object(_) = packed { self.data.cards.insert(id, p); } else { panic!("Packing did not produce a map."); } self.on_change(Some(id)) } pub fn delete_card(&mut self, id: usize) { self.data.cards.remove(&id); self.on_change(None) } /// Get a list of free_tags and free_enum fields /// /// Returns (free_tags, free_enums), where both members are vecs of (field_key, index_group) fn get_fields_for_freeform_indexes(model: &Model) -> FreeformFieldsOfInterest { // tuples (key, group) let mut free_enum_fields: Vec = vec![]; let mut free_tag_fields: Vec = vec![]; for (key, field) in &model.fields { match &field.kind { FieldKind::FreeEnum { enum_group } => { let enum_group = enum_group.as_ref().unwrap_or(key); free_enum_fields.push(KeyAndGroup(key.to_string(), enum_group.to_string())); } FieldKind::FreeTags { tag_group } => { let tag_group = tag_group.as_ref().unwrap_or(key); free_tag_fields.push(KeyAndGroup(key.to_string(), tag_group.to_string())); } _ => {} } } FreeformFieldsOfInterest { free_tag_fields, free_enum_fields, } } /// This is an associated function to split the lifetimes fn index_card<'a>( index: &mut Indexes, freeform_fields: &FreeformFieldsOfInterest, card: &'a Value, ) { for KeyAndGroup(key, group) in &freeform_fields.free_enum_fields { if !index.free_enums.contains_key(key.as_str()) { index.free_enums.insert(key.to_string(), Default::default()); } let group = index.free_enums.get_mut(group.as_str()).unwrap(); if let Some(value) = card.dot_get::(&key).unwrap_or_default() { if !value.is_empty() { group.insert(value.to_string()); } } } for KeyAndGroup(key, group) in &freeform_fields.free_tag_fields { if !index.free_tags.contains_key(key.as_str()) { index.free_tags.insert(key.to_string(), Default::default()); } let group = index.free_tags.get_mut(group.as_str()).unwrap(); group.extend(card.dot_get_or_default::>(&key).unwrap()); } } pub fn rebuild_indexes(&mut self) { self.index.free_enums.clear(); self.index.free_tags.clear(); for (_, card) in &self.data.cards { Self::index_card(&mut self.index, &self.freeform_fields, card); } } } #[derive(Debug)] struct KeyAndGroup(String, String); #[derive(Debug)] struct FreeformFieldsOfInterest { pub free_tag_fields: Vec, pub free_enum_fields: Vec, } fn write_file(path: impl AsRef, bytes: &[u8]) { let mut file = OpenOptions::new() .write(true) .create(true) .truncate(true) .open(path) .expect("Error opening data file for writing."); file.write(bytes).expect("Error write data file"); } fn load_file(path: impl AsRef) -> String { let mut file = File::open(&path).expect(&format!("Error opening file {}", path.as_ref().display())); let mut buf = String::new(); file.read_to_string(&mut buf) .expect(&format!("Error reading file {}", path.as_ref().display())); buf } fn load_file_or(file: impl AsRef, def: impl Into) -> String { let mut file = match File::open(file) { Ok(file) => file, Err(_) => return def.into(), }; let mut buf = String::new(); if file.read_to_string(&mut buf).is_err() { return def.into(); } buf }