diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2023-09-01 00:00:00 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2023-09-13 09:24:49 +0000 |
| commit | 6423baebd92e45c9be85157c443dff42e65d8d14 (patch) | |
| tree | 6548edfd80d0cd9d1267378ebe816ec95e394137 /src/config.rs | |
| parent | 5c5feaa732363e32e2a980a887fa42b4394b1a5e (diff) | |
refactor: rebuild app skeleton
Create skeleton for a complete rebuild of the prototype as a production
ready product.
Includes design patterns for:
- dependency injection
- unit testing with dependency mocking
- integration testing
- error handling
- config storage
BREAKING-CHANGE: ground-up redesign with incompatible protocol standards
Diffstat (limited to 'src/config.rs')
| -rw-r--r-- | src/config.rs | 152 |
1 files changed, 152 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..b26dea0 --- /dev/null +++ b/src/config.rs | |||
| @@ -0,0 +1,152 @@ | |||
| 1 | use std::{fs::File, io::BufReader}; | ||
| 2 | |||
| 3 | use anyhow::{anyhow, Context, Result}; | ||
| 4 | use directories::ProjectDirs; | ||
| 5 | #[cfg(test)] | ||
| 6 | use mockall::*; | ||
| 7 | use serde::{self, Deserialize, Serialize}; | ||
| 8 | |||
| 9 | #[derive(Default)] | ||
| 10 | #[allow(clippy::module_name_repetitions)] | ||
| 11 | pub struct ConfigManager; | ||
| 12 | |||
| 13 | #[cfg_attr(test, automock)] | ||
| 14 | #[allow(clippy::module_name_repetitions)] | ||
| 15 | pub trait ConfigManagement { | ||
| 16 | fn load(&self) -> Result<MyConfig>; | ||
| 17 | fn save(&self, cfg: &MyConfig) -> Result<()>; | ||
| 18 | } | ||
| 19 | |||
| 20 | pub fn get_dirs() -> Result<ProjectDirs> { | ||
| 21 | ProjectDirs::from("", "CodeCollaboration", "ngit").ok_or(anyhow!( | ||
| 22 | "should find operating system home directories with rust-directories crate" | ||
| 23 | )) | ||
| 24 | } | ||
| 25 | |||
| 26 | impl ConfigManagement for ConfigManager { | ||
| 27 | fn load(&self) -> Result<MyConfig> { | ||
| 28 | let config_path = get_dirs()?.config_dir().join("config.json"); | ||
| 29 | if config_path.exists() { | ||
| 30 | let file = | ||
| 31 | File::open(config_path).context("should open application configuration file")?; | ||
| 32 | let reader = BufReader::new(file); | ||
| 33 | let config: MyConfig = serde_json::from_reader(reader) | ||
| 34 | .context("should read config from config file with serde_json")?; | ||
| 35 | Ok(config) | ||
| 36 | } else { | ||
| 37 | Ok(MyConfig::default()) | ||
| 38 | } | ||
| 39 | } | ||
| 40 | fn save(&self, cfg: &MyConfig) -> Result<()> { | ||
| 41 | let dirs = get_dirs()?; | ||
| 42 | let config_path = dirs.config_dir().join("config.json"); | ||
| 43 | let file = if config_path.exists() { | ||
| 44 | std::fs::OpenOptions::new() | ||
| 45 | .create(true) | ||
| 46 | .write(true) | ||
| 47 | .truncate(true) | ||
| 48 | .open(config_path) | ||
| 49 | .context( | ||
| 50 | "should open application configuration file with write and truncate options", | ||
| 51 | )? | ||
| 52 | } else { | ||
| 53 | std::fs::create_dir_all(dirs.config_dir()) | ||
| 54 | .context("should create application config directories")?; | ||
| 55 | std::fs::File::create(config_path).context("should create application config file")? | ||
| 56 | }; | ||
| 57 | serde_json::to_writer_pretty(file, cfg) | ||
| 58 | .context("should write configuration to config file with serde_json") | ||
| 59 | } | ||
| 60 | } | ||
| 61 | |||
| 62 | #[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)] | ||
| 63 | #[allow(clippy::module_name_repetitions)] | ||
| 64 | pub struct MyConfig { | ||
| 65 | pub version: u8, | ||
| 66 | pub users: Vec<UserRef>, | ||
| 67 | } | ||
| 68 | |||
| 69 | #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] | ||
| 70 | pub struct UserRef { | ||
| 71 | pub nsec: String, | ||
| 72 | } | ||
| 73 | |||
| 74 | #[cfg(test)] | ||
| 75 | mod tests { | ||
| 76 | use anyhow::Result; | ||
| 77 | use serial_test::serial; | ||
| 78 | use test_utils::*; | ||
| 79 | |||
| 80 | use super::*; | ||
| 81 | |||
| 82 | mod load { | ||
| 83 | use super::*; | ||
| 84 | |||
| 85 | #[test] | ||
| 86 | #[serial] | ||
| 87 | fn when_config_file_doesnt_exist_defaults_are_returned() -> Result<()> { | ||
| 88 | with_fresh_config(|| { | ||
| 89 | assert_eq!(ConfigManager.load()?, MyConfig::default()); | ||
| 90 | |||
| 91 | Ok(()) | ||
| 92 | }) | ||
| 93 | } | ||
| 94 | |||
| 95 | #[test] | ||
| 96 | #[serial] | ||
| 97 | fn when_config_file_exists_it_is_returned() -> Result<()> { | ||
| 98 | with_fresh_config(|| { | ||
| 99 | let c = ConfigManager; | ||
| 100 | let new_config = MyConfig { | ||
| 101 | version: 255, | ||
| 102 | ..MyConfig::default() | ||
| 103 | }; | ||
| 104 | c.save(&new_config)?; | ||
| 105 | assert_eq!(c.load()?, new_config); | ||
| 106 | |||
| 107 | Ok(()) | ||
| 108 | }) | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | mod save { | ||
| 113 | use super::*; | ||
| 114 | |||
| 115 | #[test] | ||
| 116 | #[serial] | ||
| 117 | fn when_config_file_doesnt_config_is_saved() -> Result<()> { | ||
| 118 | with_fresh_config(|| { | ||
| 119 | let c = ConfigManager; | ||
| 120 | let new_config = MyConfig { | ||
| 121 | version: 255, | ||
| 122 | ..MyConfig::default() | ||
| 123 | }; | ||
| 124 | c.save(&new_config)?; | ||
| 125 | assert_eq!(c.load()?, new_config); | ||
| 126 | |||
| 127 | Ok(()) | ||
| 128 | }) | ||
| 129 | } | ||
| 130 | |||
| 131 | #[test] | ||
| 132 | #[serial] | ||
| 133 | fn when_config_file_exists_new_config_is_saved() -> Result<()> { | ||
| 134 | with_fresh_config(|| { | ||
| 135 | let c = ConfigManager; | ||
| 136 | let config = MyConfig { | ||
| 137 | version: 255, | ||
| 138 | ..MyConfig::default() | ||
| 139 | }; | ||
| 140 | c.save(&config)?; | ||
| 141 | let new_config = MyConfig { | ||
| 142 | version: 254, | ||
| 143 | ..MyConfig::default() | ||
| 144 | }; | ||
| 145 | c.save(&new_config)?; | ||
| 146 | assert_eq!(c.load()?, new_config); | ||
| 147 | |||
| 148 | Ok(()) | ||
| 149 | }) | ||
| 150 | } | ||
| 151 | } | ||
| 152 | } | ||