You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
238 lines
7.4 KiB
238 lines
7.4 KiB
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<String, BTreeSet<String>>,
|
|
pub free_tags: HashMap<String, BTreeSet<String>>,
|
|
}
|
|
|
|
/// 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<usize, Value>,
|
|
#[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<Path>) -> 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<usize>) {
|
|
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<String, Value>) -> 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<String, Value>) {
|
|
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<KeyAndGroup> = vec![];
|
|
let mut free_tag_fields: Vec<KeyAndGroup> = 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::<String>(&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::<Vec<String>>(&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<KeyAndGroup>,
|
|
pub free_enum_fields: Vec<KeyAndGroup>,
|
|
}
|
|
|
|
fn write_file(path: impl AsRef<Path>, 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<Path>) -> 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<Path>, def: impl Into<String>) -> 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
|
|
}
|
|
|