use chrono::{DateTime, Local};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::sync::{Mutex, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LibraryType {
SubstanceBase,
AllKeysSubstance,
Elements,
Reactbase,
DictReaction,
}
impl LibraryType {
pub fn as_str(&self) -> &'static str {
match self {
LibraryType::SubstanceBase => "substance_base",
LibraryType::AllKeysSubstance => "all_keys_substance",
LibraryType::Elements => "elements",
LibraryType::Reactbase => "reactbase",
LibraryType::DictReaction => "dict_reaction",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LibraryConfig {
pub substance_base: String,
pub all_keys_substance: String,
pub elements: String,
pub reactbase: String,
pub dict_reaction: String,
pub problems_folder: String,
}
impl Default for LibraryConfig {
fn default() -> Self {
Self {
substance_base: "substance_base_v2.json".to_string(),
all_keys_substance: "all_keys_substance.json".to_string(),
elements: "elements.json".to_string(),
reactbase: "Reactbase.json".to_string(),
dict_reaction: "dict_reaction.json".to_string(),
problems_folder: "problems".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct LibraryManager {
config: LibraryConfig,
config_file: String,
}
impl Default for LibraryManager {
fn default() -> Self {
Self::new()
}
}
impl LibraryManager {
pub fn new() -> Self {
let config_file = "library_config.json".to_string();
let config = Self::load_config(&config_file).unwrap_or_default();
let manager = Self {
config,
config_file,
};
let _ = manager.ensure_problems_folder();
manager
}
pub fn with_config_file(config_file: &str) -> Self {
let config = Self::load_config(config_file).unwrap_or_default();
Self {
config,
config_file: config_file.to_string(),
}
}
fn load_config(config_file: &str) -> Result<LibraryConfig, Box<dyn std::error::Error>> {
if Path::new(config_file).exists() {
let content = fs::read_to_string(config_file)?;
let config: LibraryConfig = serde_json::from_str(&content)?;
Ok(config)
} else {
Ok(LibraryConfig::default())
}
}
pub fn save_config(&self) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(test)]
{
return Ok(());
}
#[cfg(not(test))]
{
let content = serde_json::to_string_pretty(&self.config)?;
fs::write(&self.config_file, content)?;
Ok(())
}
}
pub fn substance_base_path(&self) -> &str {
&self.config.substance_base
}
pub fn all_keys_substance_path(&self) -> &str {
&self.config.all_keys_substance
}
pub fn reactbase_path(&self) -> &str {
&self.config.reactbase
}
pub fn dict_reaction_path(&self) -> &str {
&self.config.dict_reaction
}
pub fn elements_path(&self) -> &str {
&self.config.elements
}
pub fn problems_folder_path(&self) -> &str {
&self.config.problems_folder
}
pub fn get_library_filename(&self, library_type: LibraryType) -> &str {
match library_type {
LibraryType::SubstanceBase => &self.config.substance_base,
LibraryType::AllKeysSubstance => &self.config.all_keys_substance,
LibraryType::Elements => &self.config.elements,
LibraryType::Reactbase => &self.config.reactbase,
LibraryType::DictReaction => &self.config.dict_reaction,
}
}
pub fn set_substance_base(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(path).exists() {
self.config.substance_base = path.to_string();
self.save_config()?;
Ok(())
} else {
Err(format!("File does not exist: {}", path).into())
}
}
pub fn set_all_keys_substance(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(path).exists() {
self.config.all_keys_substance = path.to_string();
self.save_config()?;
Ok(())
} else {
Err(format!("File does not exist: {}", path).into())
}
}
pub fn set_reactbase(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(path).exists() {
self.config.reactbase = path.to_string();
self.save_config()?;
Ok(())
} else {
Err(format!("File does not exist: {}", path).into())
}
}
pub fn set_elements(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(path).exists() {
self.config.elements = path.to_string();
self.save_config()?;
Ok(())
} else {
Err(format!("File does not exist: {}", path).into())
}
}
pub fn set_dict_reaction(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
if Path::new(path).exists() {
self.config.dict_reaction = path.to_string();
self.save_config()?;
Ok(())
} else {
Err(format!("File does not exist: {}", path).into())
}
}
pub fn update_libraries(
&mut self,
updates: HashMap<&str, &str>,
) -> Result<(), Box<dyn std::error::Error>> {
for (key, path) in &updates {
if !Path::new(path).exists() {
return Err(format!("File does not exist: {}", path).into());
}
}
for (key, path) in updates.clone() {
match key {
"substance_base" => self.config.substance_base = path.to_string(),
"all_keys_substance" => self.config.all_keys_substance = path.to_string(),
"elements" => self.config.elements = path.to_string(),
"reactbase" => self.config.reactbase = path.to_string(),
"dict_reaction" => self.config.dict_reaction = path.to_string(),
"problems_folder" => {
if !Path::new(path).exists() {
fs::create_dir_all(path)?;
}
self.config.problems_folder = path.to_string();
}
_ => return Err(format!("Unknown library key: {}", key).into()),
}
}
self.save_config()?;
Ok(())
}
pub fn get_config(&self) -> &LibraryConfig {
&self.config
}
pub fn set_problems_folder(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
if !Path::new(path).exists() {
fs::create_dir_all(path)?;
}
self.config.problems_folder = path.to_string();
self.save_config()?;
Ok(())
}
pub fn ensure_problems_folder(&self) -> Result<(), Box<dyn std::error::Error>> {
if !Path::new(&self.config.problems_folder).exists() {
fs::create_dir_all(&self.config.problems_folder)?;
}
Ok(())
}
pub fn reset_to_defaults(&mut self) -> Result<(), Box<dyn std::error::Error>> {
self.config = LibraryConfig::default();
self.save_config()?;
Ok(())
}
pub fn log_library_update(
&self,
library_type: LibraryType,
content: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let log_file = "library_log.txt";
let timestamp: DateTime<Local> = Local::now();
let date_str = timestamp.format("%d.%m.%Y").to_string();
let actual_filename = match library_type {
LibraryType::SubstanceBase => &self.config.substance_base,
LibraryType::AllKeysSubstance => &self.config.all_keys_substance,
LibraryType::Elements => &self.config.elements,
LibraryType::Reactbase => &self.config.reactbase,
LibraryType::DictReaction => &self.config.dict_reaction,
};
let log_entry = format!(
"{} in {} \"{}\"
",
date_str, actual_filename, content
);
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(log_file)?;
file.write_all(log_entry.as_bytes())?;
Ok(())
}
pub fn add_library_key(
&self,
library_type: LibraryType,
key: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let filename = self.get_library_filename(library_type);
let mut json_data: serde_json::Value = if Path::new(filename).exists() {
let content = fs::read_to_string(filename)?;
serde_json::from_str(&content)?
} else {
serde_json::json!({})
};
if let Some(obj) = json_data.as_object_mut() {
obj.insert(key.to_string(), serde_json::json!({}));
}
let content = serde_json::to_string_pretty(&json_data)?;
fs::write(filename, content)?;
Ok(())
}
pub fn add_secondary_key(
&self,
library_type: LibraryType,
primary_key: &str,
secondary_key: &str,
value: serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
let filename = self.get_library_filename(library_type);
let mut json_data: serde_json::Value = if Path::new(filename).exists() {
let content = fs::read_to_string(filename)?;
serde_json::from_str(&content)?
} else {
return Err(format!("Library file does not exist: {}", filename).into());
};
if let Some(obj) = json_data.as_object_mut() {
if let Some(primary_obj) = obj.get_mut(primary_key) {
if let Some(primary_map) = primary_obj.as_object_mut() {
primary_map.insert(secondary_key.to_string(), value);
} else {
return Err(format!("Primary key '{}' is not an object", primary_key).into());
}
} else {
return Err(format!("Primary key '{}' not found", primary_key).into());
}
}
let content = serde_json::to_string_pretty(&json_data)?;
fs::write(filename, content)?;
Ok(())
}
}
static GLOBAL_LIBRARY_MANAGER: OnceLock<Mutex<LibraryManager>> = OnceLock::new();
#[cfg(test)]
static TEST_MANAGER: std::sync::Mutex<Option<LibraryManager>> = std::sync::Mutex::new(None);
#[cfg(test)]
pub fn set_test_manager(manager: LibraryManager) {
*TEST_MANAGER.lock().unwrap() = Some(manager);
}
#[cfg(test)]
pub fn clear_test_manager() {
*TEST_MANAGER.lock().unwrap() = None;
}
pub fn get_library_manager() -> std::sync::MutexGuard<'static, LibraryManager> {
#[cfg(test)]
{
if let Some(ref manager) = *TEST_MANAGER.lock().unwrap() {
let _ = GLOBAL_LIBRARY_MANAGER.set(Mutex::new(manager.clone()));
}
}
GLOBAL_LIBRARY_MANAGER
.get_or_init(|| Mutex::new(LibraryManager::new()))
.lock()
.unwrap()
}
pub fn with_library_manager<F, R>(f: F) -> R
where
F: FnOnce(&LibraryManager) -> R,
{
let manager = get_library_manager();
f(&*manager)
}
pub fn with_library_manager_mut<F, R>(f: F) -> R
where
F: FnOnce(&mut LibraryManager) -> R,
{
let mut manager = get_library_manager();
f(&mut *manager)
}
pub fn log_library_update(
library_type: LibraryType,
content: &str,
) -> Result<(), Box<dyn std::error::Error>> {
with_library_manager(|manager| manager.log_library_update(library_type, content))
}
pub fn get_library_filename(library_type: LibraryType) -> String {
with_library_manager(|manager| manager.get_library_filename(library_type).to_string())
}
pub fn add_library_key(
library_type: LibraryType,
key: &str,
) -> Result<(), Box<dyn std::error::Error>> {
with_library_manager(|manager| manager.add_library_key(library_type, key))
}
pub fn add_secondary_key(
library_type: LibraryType,
primary_key: &str,
secondary_key: &str,
value: serde_json::Value,
) -> Result<(), Box<dyn std::error::Error>> {
with_library_manager(|manager| {
manager.add_secondary_key(library_type, primary_key, secondary_key, value)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn setup_test_files() -> (NamedTempFile, NamedTempFile, NamedTempFile, NamedTempFile) {
let mut substance_file = NamedTempFile::new().unwrap();
let mut keys_file = NamedTempFile::new().unwrap();
let mut react_file = NamedTempFile::new().unwrap();
let mut dict_file = NamedTempFile::new().unwrap();
substance_file.write_all(b"{}").unwrap();
keys_file.write_all(b"[]").unwrap();
react_file.write_all(b"{}").unwrap();
dict_file.write_all(b"{}").unwrap();
(substance_file, keys_file, react_file, dict_file)
}
#[test]
fn test_library_manager_new() {
let manager = LibraryManager::new();
assert_eq!(manager.all_keys_substance_path(), "all_keys_substance.json");
assert_eq!(manager.substance_base_path(), "substance_base_v2.json");
assert_eq!(manager.reactbase_path(), "Reactbase.json");
assert_eq!(manager.dict_reaction_path(), "dict_reaction.json");
}
#[test]
fn test_library_manager_with_config() {
let mut temp_config = NamedTempFile::new().unwrap();
let mut temp_substance = NamedTempFile::new().unwrap();
let mut temp_keys = NamedTempFile::new().unwrap();
let mut temp_react = NamedTempFile::new().unwrap();
let mut temp_dict = NamedTempFile::new().unwrap();
temp_substance.write_all(b"{}").unwrap();
temp_keys.write_all(b"[]").unwrap();
temp_react.write_all(b"{}").unwrap();
temp_dict.write_all(b"{}").unwrap();
let config = LibraryConfig {
substance_base: temp_substance.path().to_str().unwrap().to_string(),
all_keys_substance: temp_keys.path().to_str().unwrap().to_string(),
elements: temp_keys.path().to_str().unwrap().to_string(),
reactbase: temp_react.path().to_str().unwrap().to_string(),
dict_reaction: temp_dict.path().to_str().unwrap().to_string(),
problems_folder: "problems".to_string(),
};
let config_json = serde_json::to_string_pretty(&config).unwrap();
temp_config.write_all(config_json.as_bytes()).unwrap();
let manager = LibraryManager::with_config_file(temp_config.path().to_str().unwrap());
assert_eq!(
manager.substance_base_path(),
temp_substance.path().to_str().unwrap()
);
assert_eq!(
manager.all_keys_substance_path(),
temp_keys.path().to_str().unwrap()
);
}
#[test]
fn test_update_libraries() {
let mut temp_config = NamedTempFile::new().unwrap();
let mut temp_substance = NamedTempFile::new().unwrap();
let mut temp_keys = NamedTempFile::new().unwrap();
temp_substance.write_all(b"{}").unwrap();
temp_keys.write_all(b"[]").unwrap();
let mut manager = LibraryManager::with_config_file(temp_config.path().to_str().unwrap());
let mut updates = HashMap::new();
updates.insert("substance_base", temp_substance.path().to_str().unwrap());
updates.insert("all_keys_substance", temp_keys.path().to_str().unwrap());
let result = manager.update_libraries(updates);
assert!(result.is_ok());
assert_eq!(
manager.substance_base_path(),
temp_substance.path().to_str().unwrap()
);
assert_eq!(
manager.all_keys_substance_path(),
temp_keys.path().to_str().unwrap()
);
}
}