-
Notifications
You must be signed in to change notification settings - Fork 0
/
config.rs
371 lines (330 loc) · 12 KB
/
config.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
//! Configuration types used for managing and interacting with mods/modpacks on
//! the system
mod loader;
mod modpack;
mod mods;
mod project_with_version;
mod serde;
// Use attribute with newlines so mod docs aren't merged on the same line
#[doc = "Types relating to [profile data](profile::ProfileData)\n\n"]
pub mod profile;
use std::{collections::BTreeSet, path::Path, sync::LazyLock};
use ::serde::{Deserialize, Serialize};
#[doc(inline)]
pub use self::profile::Profile;
use self::profile::ProfileByPath;
pub use self::{loader::*, modpack::*, mods::*, project_with_version::*};
use crate::{
fs_util::{FsUtil, FsUtils},
ErrorKind, PathAbsolute, Result, CONF_DIR,
};
/// Full path to the default config file
pub static DEFAULT_CONFIG_PATH: LazyLock<PathAbsolute> = LazyLock::new(|| CONF_DIR.join("config.json"));
type ProfilesList = BTreeSet<ProfileByPath>;
/// Global config object containing a list of profile names and their path
///
/// The actual [profile data] is stored externally at the path associated with
/// the profile.
///
/// [profile data]: profile::ProfileData
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[serde(default, from = "ConfigDe")]
pub struct Config {
/// Will only be [None] when [profiles](Config::profiles) is empty
#[serde(skip_serializing_if = "Option::is_none")]
active: Option<PathAbsolute>,
#[serde(
skip_serializing_if = "ProfilesList::is_empty",
serialize_with = "self::serde::profiles::serialize"
)]
profiles: ProfilesList,
}
/// Workaround for no support of split borrowing of `self` behind method calls
macro_rules! get {
($self:ident.active) => {
$self.active.as_ref().ok_or(ErrorKind::NoProfiles)?
};
($self:ident.profile_mut($path:expr)) => {
$self
.profiles
.get($path)
.map(ProfileByPath::force_mut)
.ok_or(ErrorKind::UnknownProfile.into())
};
}
// Profile
impl Config {
/// Returns the path of the active profile if set
pub fn active(&self) -> Option<&PathAbsolute> {
self.active.as_ref()
}
/// Returns `true` if an [active profile](Self::active_profile) is set
///
/// Will only be `false` when [profiles](Config::get_profiles) is empty
pub fn has_active(&self) -> bool {
self.active.is_some()
}
/// Sets the [active profile] to `path` and returns the previous
/// [profile data] if it was loaded and `path` changed.
///
/// The current [active profile] should be [saved](Profile::save) before
/// changing, otherwise any modifications will be lost
///
/// [active profile]: Self::active_profile
/// [profile data]: profile::ProfileData
/// # Errors
///
/// [`ErrorKind::UnknownProfile`]: if `path` is not present in list of known
/// profiles
pub fn set_active(&mut self, path: impl AsRef<PathAbsolute>) -> Result<()> {
let path = path.as_ref();
if !self.profiles.contains(path) {
return Err(ErrorKind::UnknownProfile)?;
}
if self.active.as_ref().is_some_and(|ap| ap != path) {
self.active.replace(path.to_owned());
}
Ok(())
}
/// Returns the currently active [profile]
///
/// This is the [profile] that actions that don't take an explicit profile
/// will be applied to
///
/// # Errors
///
/// [`ErrorKind::NoProfiles`]: if [profiles](Profile) is empty
///
/// [profiles]: Self::profiles
pub fn active_profile(&self) -> Result<&Profile> {
self.profile(get!(self.active))
}
/// See [`active_profile`](Self::active_profile)
pub fn active_profile_mut(&mut self) -> Result<&mut Profile> {
get!(self.profile_mut(get!(self.active)))
}
/// Return the profile associated with the given `path`
///
/// # Errors
///
/// [`ErrorKind::UnknownProfile`] if `path` is not present in list of
/// known [profiles]
///
/// [profiles]: Self::get_profiles
pub fn profile(&self, path: impl AsRef<Path>) -> Result<&Profile> {
self.profiles
.get(path.as_ref())
.map(AsRef::as_ref)
.ok_or(ErrorKind::UnknownProfile.into())
}
/// See [`profile`](Self::profile)
pub fn profile_mut(&mut self, path: impl AsRef<Path>) -> Result<&mut Profile> {
get!(self.profile_mut(path.as_ref()))
}
/// The list of [profiles](Profile) sorted by `path`
pub fn get_profiles(&self) -> Vec<&Profile> {
self.profiles.iter().map(AsRef::as_ref).collect()
}
/// See [`get_profiles`](Self::get_profiles)
pub fn get_profiles_mut(&mut self) -> Vec<&mut Profile> {
self.profiles.iter().map(ProfileByPath::force_mut).collect()
}
/// Add the [profile](Profile) to this config if not already present
///
/// # Errors
///
/// This function will return an error containing the passed in profile
/// if a profile with the same path is already present in the config
pub fn add_profile(&mut self, profile: Profile) -> std::result::Result<(), Profile> {
if self.profiles.contains(&*profile.path) {
Err(profile)
} else {
self.profiles.insert(profile.into());
Ok(())
}
}
/// Remove and return the [profile](Profile) for `path`
///
/// If the removed profile was the currently [active profile], then the
/// active profile will be switched to the first profile
///
/// # Errors
///
/// [`ErrorKind::UnknownProfile`]: if no profile exists for `path`
///
/// [active profile]: Self::active_profile
pub fn remove_profile(&mut self, path: impl AsRef<Path>) -> Result<Profile> {
let removed: Profile = self.profiles.take(path.as_ref()).map(Into::into).ok_or(ErrorKind::UnknownProfile)?;
if self.active.as_ref().is_some_and(|a| a == &removed.path) {
self.active = self.profiles.first().map(|p| p.as_absolute().to_owned());
}
Ok(removed)
}
}
// Load/Save
impl Config {
/// Load a [config](Config) from the file located at the
/// [default config path]
///
/// [default config path]: DEFAULT_CONFIG_PATH
/// # Errors
///
/// Will return any IO or parse errors encountered while attempting to read
/// the config
pub async fn load() -> Result<Self> {
Self::load_from(&*DEFAULT_CONFIG_PATH).await
}
/// Load a [config](Config) from the file located at `path`
///
/// # Errors
///
/// Will return any IO or parse errors encountered while attempting to read
/// the config
pub async fn load_from(path: impl AsRef<Path>) -> Result<Self> {
FsUtil::load_file(path.as_ref()).await
}
/// Save this [config](Config) and the active [profile] to the file located
/// at the [default config path]
///
///
/// [profile]: profile::ProfileData::save_to
/// [default config path]: DEFAULT_CONFIG_PATH
/// # Errors
///
/// Will return any IO errors encountered while attempting to save to the
/// filesystem
pub async fn save(&mut self) -> Result<()> {
self.save_to(&*DEFAULT_CONFIG_PATH).await
}
/// Save this [config](Config) and the active [profile] to the file located
/// at `path`
///
/// [profile]: Profile::save
/// # Errors
///
/// Will return any IO errors encountered while attempting to save to the
/// filesystem
pub async fn save_to(&mut self, path: impl AsRef<Path>) -> Result<()> {
FsUtil::save_file(self, path.as_ref()).await?;
for profile in self.profiles.iter().map(ProfileByPath::force_mut) {
profile.save().await?;
}
Ok(())
}
}
/// Proxy deserialize object for [Config] to ensure data validity
#[derive(Deserialize, Default)]
#[serde(default)]
struct ConfigDe {
active: Option<PathAbsolute>,
#[serde(deserialize_with = "self::serde::profiles::deserialize")]
profiles: ProfilesList,
}
impl From<ConfigDe> for Config {
fn from(de: ConfigDe) -> Self {
Self {
active: de
.active
// Require active_profile to be present in profiles
.and_then(|p| if de.profiles.contains(&p) { Some(p) } else { None })
// Activate first profile from list if present and not already set
.or_else(|| de.profiles.first().map(ProfileByPath::as_absolute).map(ToOwned::to_owned)),
profiles: de.profiles,
}
}
}
#[cfg(test)]
mod tests {
use std::iter::zip;
use serde_test::{assert_de_tokens, assert_ser_tokens, Token};
use super::*;
static PATHS: LazyLock<[PathAbsolute; 3]> = LazyLock::new(|| {
[
PathAbsolute::new("/test/profile/path/1").unwrap(),
PathAbsolute::new("/test/profile/path/2").unwrap(),
PathAbsolute::new("/test/profile/path/3").unwrap(),
]
});
const NAMES: &[&str] = &["Profile 1", "Profile 2", "Profile 3"];
impl PartialEq for Config {
fn eq(&self, other: &Self) -> bool {
self.active == other.active && self.profiles == other.profiles
}
}
fn test_config() -> Config {
Config {
active: Some(PATHS[2].clone()),
profiles: zip(NAMES, &*PATHS)
.map(|(name, path)| Profile::new((*name).to_string(), path.clone()))
.map(Into::into)
.collect(),
}
}
fn test_ser_data() -> (Config, Vec<Token>) {
let config = test_config();
let mut tokens = vec![
Token::Struct { name: "Config", len: 2 },
Token::Str("active"),
Token::Some,
Token::NewtypeStruct { name: "PathAbsolute" },
Token::Str(PATHS[2].to_str().unwrap()),
Token::Str("profiles"),
Token::Map { len: Some(PATHS.len()) },
];
tokens.extend(zip(&*PATHS, NAMES).flat_map(|(p, n)| {
[
Token::NewtypeStruct { name: "PathAbsolute" },
Token::Str(p.to_str().unwrap()),
Token::Str(n),
]
}));
tokens.extend([Token::MapEnd, Token::StructEnd]);
(config, tokens)
}
fn test_de_data() -> (Config, Vec<Token>) {
let (config, mut tokens) = test_ser_data();
if let Token::Struct { name, .. } = tokens.first_mut().unwrap() {
*name = "ConfigDe";
}
tokens.retain_mut(|t| !matches!(t, Token::NewtypeStruct { .. }));
(config, tokens)
}
#[test]
fn serialize() {
let (config, tokens) = test_ser_data();
eprintln!("{}", serde_json::to_string_pretty(&config).unwrap());
assert_ser_tokens(&config, &tokens);
}
#[test]
fn deserialize_all() {
let (config, tokens) = test_de_data();
assert_de_tokens(&config, &tokens);
}
/// When no `active_profile` is set, then it should get set to the first
/// listed profile automatically
#[test]
fn deserialize_no_active() {
let (mut config, mut tokens) = test_de_data();
config.active.replace(PATHS[0].clone()); // Set active_profile to first path
tokens.drain(1..=3); // Remove active_profile from tokens
assert_de_tokens(&config, &tokens);
}
/// When the `active_profile` is set to a value not in the list of profiles,
/// then it should get set to the first listed profile automatically
#[test]
fn deserialize_bad_active() {
let (mut config, mut tokens) = test_de_data();
config.active.replace(PATHS[0].clone()); // Set active_profile to first path
tokens[3] = Token::Str("/some/invalid/path"); // Set active_profile token to path not in profiles
assert_de_tokens(&config, &tokens);
}
#[test]
fn set_active_invalid() {
let mut c = test_config();
let res = c.set_active(PathAbsolute::new("/some/invalid/path").unwrap());
assert!(
matches!(res, Err(ref e) if matches!(e.kind(), ErrorKind::UnknownProfile)),
"set_active should fail with correct error given a path not in profiles: {res:?}"
);
}
}