1use std::{
16 any,
17 fmt::{self, Debug},
18 fs::{self, File, OpenOptions},
19 io::{self, BufReader, BufWriter},
20 os::unix::fs::OpenOptionsExt,
21 path::PathBuf,
22};
23
24use serde::{de::DeserializeOwned, Serialize};
25use thiserror::Error as ThisError;
26
27mod environment;
28
29pub struct Config<TConfigData: Serialize + DeserializeOwned + Default> {
37 config_data: TConfigData,
38 config_file_key: String, }
40
41impl<TConfigData: Serialize + DeserializeOwned + Default + Debug> Debug for Config<TConfigData> {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write!(
45 f,
46 "Config<{}> {{ config_data: {:?}, config_file_key: {} }}",
47 any::type_name::<TConfigData>(),
48 self.config_data,
49 self.config_file_key,
50 )
51 }
52}
53
54impl<TConfigData: Serialize + DeserializeOwned + Default> Config<TConfigData> {
55 pub fn load(config_file_key: &str) -> Result<Self, ConfigError> {
60 let config_path = Self::get_config_path(config_file_key)?;
61
62 let config_data = if config_path.is_file() {
63 let file = File::open(&config_path)
64 .map_err(|e| ConfigError::ConfigFileLoadError(config_path.clone(), e))?;
65 let reader = BufReader::new(file);
66 serde_json::from_reader(reader)
67 .map_err(|e| ConfigError::ConfigFileParseError(config_path, e))?
68 } else {
69 TConfigData::default()
70 };
71
72 Ok(Self {
73 config_data,
74 config_file_key: config_file_key.to_string(),
75 })
76 }
77
78 pub fn save(&self) -> Result<(), ConfigError> {
80 let config_root = Self::get_config_root()?;
82 match config_root.try_exists() {
83 Ok(true) => (),
84 Ok(false) => {
85 fs::create_dir_all(config_root.clone())
86 .map_err(|e| ConfigError::ConfigRootCreateError(config_root, e))?;
87 }
88 Err(e) => {
89 return Err(ConfigError::ConfigRootLoadError(config_root, e));
90 }
91 }
92
93 let config_path = Self::get_config_path(&self.config_file_key)?;
94 match config_path.try_exists() {
95 Ok(exists) => {
96 let mut options = OpenOptions::new();
97 options.create(true).write(true).truncate(true);
98
99 #[cfg(unix)]
101 {
102 if !exists {
103 options.mode(0o600);
104 }
105 }
106
107 match options.open(config_path.clone()) {
108 Ok(f) => {
109 let writer = BufWriter::new(f);
110 serde_json::to_writer_pretty(writer, &self.config_data)
111 .map_err(ConfigError::ConfigFileSerializeError)
112 }
113 Err(e) => Err(ConfigError::ConfigFileWriteError(config_path, e)),
114 }
115 }
116 Err(e) => Err(ConfigError::ConfigFileWriteError(config_path, e)),
117 }
118 }
119
120 #[inline]
121 pub fn data(&self) -> &TConfigData {
122 &self.config_data
123 }
124
125 #[inline]
126 pub fn data_mut(&mut self) -> &mut TConfigData {
127 &mut self.config_data
128 }
129
130 fn get_config_root() -> Result<PathBuf, ConfigError> {
131 let environment = environment::load_env();
132 let config_root = environment
133 .ilo_config_home
134 .as_deref()
135 .map(PathBuf::from)
136 .or(home::home_dir().map(|d| d.join(".config").join("ilo")));
137
138 match config_root {
139 None => Err(ConfigError::NoHome),
140 Some(root) => Ok(root),
141 }
142 }
143
144 fn get_config_path(config_file_key: &str) -> Result<PathBuf, ConfigError> {
145 Self::get_config_root().map(|root| root.join(format!("{}.json", config_file_key)))
146 }
147}
148
149#[derive(ThisError, Debug)]
150pub enum ConfigError {
151 #[error("$ILO_CONFIG_HOME is not set and user home directory could not be determined")]
152 NoHome,
153
154 #[error("Config root dir {0} could not be loaded: {1}")]
155 ConfigRootLoadError(PathBuf, io::Error),
156
157 #[error("Config root dir does not exist at {0} and could not be created: {1}")]
158 ConfigRootCreateError(PathBuf, io::Error),
159
160 #[error("Config path exists at {0} but config could not be loaded: {1}")]
161 ConfigFileLoadError(PathBuf, io::Error),
162
163 #[error("Config path exists at {0} but JSON could not be parsed: {1}")]
164 ConfigFileParseError(PathBuf, serde_json::Error),
165
166 #[error("Config path location {0} could not be opened for writing: {1}")]
167 ConfigFileWriteError(PathBuf, io::Error),
168
169 #[error("There was an error serializing config to disk: {0}")]
170 ConfigFileSerializeError(serde_json::Error),
171}